Initial commit: site code, layouts, content (images added separately)

Fixes duplicate placeholder cover image on the 3 newest blog posts,
broken aspect-ratio classes on the albums listing page, and a
non-responsive fixed sidebar on the chat page.
This commit is contained in:
Dr. Jones
2026-07-08 06:18:17 +00:00
commit d9961a76ad
80 changed files with 9438 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
public/
resources/
.hugo_build.lock
hugo_stats.json

138
IMPLEMENTATION_SUMMARY.md Normal file
View File

@@ -0,0 +1,138 @@
# Membership Paywall System - Implementation Summary
## Files Created
### Backend
- `scripts/membership_api.py` - Flask API server for membership management
- `requirements.txt` - Python dependencies
### Frontend
- `static/js/membership.js` - Client-side membership functionality
- `layouts/membership/single.html` - Membership signup/login page
- `layouts/testimonials/single.html` - Testimonials page with paywall
- `layouts/admin/single.html` - Admin approval panel
- `layouts/contact/single.html` - Contact page layout
### Content
- `content/membership.md` - Membership page content
- `content/testimonials.md` - Testimonials page content
- `content/admin.md` - Admin panel page
### Documentation
- `MEMBERSHIP_SETUP.md` - Setup and configuration guide
## Files Modified
- `layouts/_default/baseof.html` - Added membership.js, auth UI, navigation updates
- `layouts/library/list.html` - Added paywall and download tracking
- `content/contact.md` - Paywalled email, secure form
- `hugo.yaml` - Added membership and testimonials to menu
## Key Features Implemented
1. **User Registration & Authentication**
- Email/password registration
- JWT-based session management
- Password hashing with bcrypt
2. **Download Tracking**
- IP-based tracking (1 free download)
- Unlimited downloads for members
- Automatic tracking on download clicks
3. **Paywall System**
- Library downloads (1 free, then paywall)
- Contact form (members only)
- Email address (members only)
- Testimonials page (members only)
4. **Admin Panel**
- Secure admin login
- Approve/deny memberships
- Moderate testimonials
- View pending members
5. **Email Notifications**
- Signup notifications to admin
- Approval confirmations to users
- Contact form submissions
6. **Buy Me a Coffee Integration**
- Prominent $5 donation button
- Users donate first, then sign up
- Manual verification by admin
## Configuration Required
1. **Environment Variables** (set before running API):
```
ADMIN_PASSWORD=your-strong-password
ADMIN_EMAIL=indiana.holmes8@gmail.com
JWT_SECRET=random-secret-key
MEMBERSHIP_PORT=5001
SMTP_HOST=smtp.gmail.com (optional)
SMTP_PORT=587 (optional)
SMTP_USER=your-email@gmail.com (optional)
SMTP_PASS=your-app-password (optional)
```
2. **API URL Configuration** (update for production):
- `static/js/membership.js` line 4: `API_BASE`
- `layouts/admin/single.html` line 186: `ADMIN_API_BASE`
3. **Start the API Server**:
```bash
python3 /root/hydro-sterile/scripts/membership_api.py
```
## Database
SQLite database automatically created at:
`/root/hydro-sterile/data/members.db`
Tables:
- `users` - User accounts and status
- `downloads` - Download tracking
- `testimonials` - User testimonials
- `sessions` - User sessions
- `admin_sessions` - Admin sessions
- `contact_submissions` - Contact form submissions
## Security Features
- Password hashing (bcrypt)
- JWT tokens with expiration
- Rate limiting on all endpoints
- Input sanitization
- SQL injection prevention (parameterized queries)
- CSRF protection ready
- Secure admin authentication
- Email privacy (not exposed in frontend)
## Next Steps
1. Set environment variables
2. Install dependencies: `pip3 install -r requirements.txt`
3. Start API server: `python3 scripts/membership_api.py`
4. Test registration flow
5. Test admin panel
6. Update API URLs for production
7. Configure SMTP for email notifications
8. Test email sending
## Testing Checklist
- [ ] User can register after donation
- [ ] Admin receives signup notification
- [ ] Admin can approve/deny members
- [ ] Approved users receive confirmation email
- [ ] Users can log in
- [ ] Download tracking works (IP-based)
- [ ] Paywall shows after 1 download
- [ ] Members get unlimited downloads
- [ ] Contact form only works for members
- [ ] Email address hidden for non-members
- [ ] Testimonials page paywalled
- [ ] Members can submit testimonials
- [ ] Admin can moderate testimonials

122
MEMBERSHIP_SETUP.md Normal file
View File

@@ -0,0 +1,122 @@
# Membership System Setup Guide
## Overview
This membership system provides:
- User registration and authentication
- IP-based download limiting (1 free download, unlimited for members)
- Paywall for library downloads, contact form, and testimonials
- Admin panel for membership approval
- Buy Me a Coffee integration ($5 minimum donation)
## Prerequisites
1. Python 3.7+ with pip
2. Flask and other dependencies (see requirements.txt)
3. SMTP server access for email notifications (optional but recommended)
## Installation
1. Install Python dependencies:
```bash
cd /root/hydro-sterile
pip3 install -r requirements.txt
```
2. Set environment variables:
```bash
export ADMIN_PASSWORD="your-very-secure-password-here"
export ADMIN_EMAIL="indiana.holmes8@gmail.com"
export JWT_SECRET="$(openssl rand -hex 32)"
export MEMBERSHIP_PORT=5001
# Optional: SMTP configuration for email notifications
export SMTP_HOST="smtp.gmail.com"
export SMTP_PORT=587
export SMTP_USER="your-email@gmail.com"
export SMTP_PASS="your-app-password"
```
3. Create a systemd service or run manually:
```bash
python3 /root/hydro-sterile/scripts/membership_api.py
```
## Configuration
### API Endpoints
The membership API runs on port 5001 by default. Update the API_BASE URL in:
- `/root/hydro-sterile/static/js/membership.js` (line 4)
- `/root/hydro-sterile/layouts/admin/single.html` (line 186)
For production, change from `http://10.30.20.243:5001` to your production domain.
### Admin Panel
Access the admin panel at `/admin/` on your Hugo site.
The admin password is set via the `ADMIN_PASSWORD` environment variable. Use a strong password!
### Database
The SQLite database is automatically created at:
`/root/hydro-sterile/data/members.db`
Backup this file regularly!
## User Flow
1. User visits paywalled page (library, contact, testimonials)
2. Non-members see paywall message directing to membership page
3. User completes Buy Me a Coffee donation ($5 minimum)
4. User signs up with email/password on membership page
5. System sends email notification to admin
6. Admin logs into admin panel and approves/denies membership
7. Approved user receives confirmation email
8. User can now log in and access all paywalled content
## Security Notes
- Admin password should be very strong (20+ characters recommended)
- JWT_SECRET should be a random string (use `openssl rand -hex 32`)
- Never commit ADMIN_PASSWORD or JWT_SECRET to version control
- Database file should have restricted permissions (600)
- SMTP credentials should use app passwords, not main account password
## Email Configuration
For Gmail:
1. Enable 2-factor authentication
2. Generate an app password
3. Use the app password as SMTP_PASS
## Troubleshooting
### API not starting
- Check port 5001 is not in use: `netstat -tuln | grep 5001`
- Check Python dependencies: `pip3 list | grep -E "flask|bcrypt|jwt"`
### Database errors
- Ensure `/root/hydro-sterile/data/` directory exists and is writable
- Check file permissions: `chmod 600 /root/hydro-sterile/data/members.db`
### Email not sending
- Check SMTP credentials
- Verify SMTP_HOST and SMTP_PORT are correct
- Check firewall allows outbound SMTP connections
- Email sending failures won't break the system, but admin won't get notifications
### Frontend not connecting to API
- Check API_BASE URL in membership.js matches your server
- Check CORS settings in membership_api.py
- Check browser console for errors
- Verify API is running: `curl http://localhost:5001/api/health`
## Maintenance
- Regularly backup `/root/hydro-sterile/data/members.db`
- Monitor API logs for errors
- Review pending memberships regularly
- Clean up old sessions periodically (optional, sessions expire automatically)

29
README.md Normal file
View File

@@ -0,0 +1,29 @@
# Pure Hydroponics Site
## Setup
1. **Install Dependencies:**
```bash
npm install
```
2. **Run Development Server:**
```bash
# Uses local hugo binary via hugo-bin
./node_modules/.bin/hugo server
```
3. **Build for Production:**
```bash
./node_modules/.bin/hugo --minify
```
## Stack
- **Hugo Extended** (v0.152+)
- **Tailwind CSS** (v4.0) via PostCSS
- **Alpine.js** for interactions
## Deployment
- **Output Directory:** `public/`
- **Cloudflare Pages:** Set build command to `npm run build` (add script to package.json) or just run hugo.
- **Netlify:** Similar setup.

36
RECOVERY.md Normal file
View File

@@ -0,0 +1,36 @@
# Website Service Management Guide
This document lists the services that run the website and how to manage them.
## Services
| Service Name | Description | Port |
|--------------|-------------|------|
| `hugo-hydrolord.service` | Main Hugo Website | 7575 |
| `hugo-membership-api.service` | Membership & Static API | 5001 |
| `hugo-chat-api.service` | AI Chat API | 5000 |
| `hugo-blog-generator.timer` | Auto Blog Generator | N/A |
## Management Commands
### Check Status
```bash
systemctl status \"hugo-*\"
```
### View Logs
```bash
# Website logs
journalctl -u hugo-hydrolord.service -f
# API logs
journalctl -u hugo-membership-api.service -f
```
### Restart All
```bash
systemctl restart hugo-hydrolord.service hugo-membership-api.service hugo-chat-api.service
```
## Persistence
All services are set to `Restart=always` and are `enabled` to start at boot automatically.

5
archetypes/default.md Normal file
View File

@@ -0,0 +1,5 @@
---
date: '{{ .Date }}'
draft: true
title: '{{ replace .File.ContentBaseName "-" " " | title }}'
---

21
assets/css/main.css Normal file
View File

@@ -0,0 +1,21 @@
@import "tailwindcss";
@theme {
--color-primary: #059669;
--color-secondary: #0ea5e9;
--color-surface: #1f2937;
--color-background: #111827;
--font-sans: 'Inter', 'system-ui', 'sans-serif';
}
@layer base {
body {
background-color: var(--color-background);
color: #e5e7eb;
font-family: var(--font-sans);
}
h1, h2, h3, h4 {
font-weight: 700;
color: #f9fafb;
}
}

40
content/about.md Normal file
View File

@@ -0,0 +1,40 @@
---
title: "About Me"
date: 2024-12-24T12:00:00Z
draft: false
description: "Sterile hydro nerd with 20 years of dialing in award-winning cannabis."
---
# About Me
I'm 35, 20 years deep growing legal weed, and I'm a laid-back stoner who treats hydro like a lab. Every photo here is cannabis because that's my playground—clean water, clean salts, no funk.
Most growers swear you need microbes and that root rot is just part of the game.
**They're wrong.**
I've spent two decades perfecting something most people think is impossible: **completely sterile hydroponics**. No bacteria. No "beneficials." No pathogens. Just roots ripping through clean solution.
Stop listening to randoms on Reddit. You won't find this on YouTube or in standard grow guides. What I teach are the protocols that make buds so clean people ask if they're even real.
## My Philosophy: No Excuses Hydro or Go Home
**No excuses hydro or go home.** Precision beats fairy tales. If something drifts, we fix the variable and keep moving.
Hydroponics isn't about excuses. It's about control—eliminating every variable that can go wrong so your plants hit their genetic ceiling.
My background is in clinical microbiology. In the lab, contamination equals failure. I pulled that same mindset into growing and watched plants thrive once every biological hitchhiker was gone.
The methods I use come from medical sterility protocols. They aren't theories. They're battle-tested and tuned over thousands of hours.
## Two Decades of Results
Over the last 20 years, I've helped small home setups, basement grows, and big commercial rooms spanning thousands of square feet. Large gardens, small gardens—I dial them all in.
I've watched basement growers ditch slime and harvest jars that make commercial facilities jealous. I've seen large operations eliminate whole categories of problems they thought were "just part of the business" and start pulling competition-grade flower that sells itself.
When commercial operations need results, they call me. When home growers want to level up, they call me.
If you follow the sterile protocols I've honed over the past two decades, you **will** see the difference: no root rot, faster growth, bigger yields, clean white roots, and plants that aren't fighting anything but the trellis.
No excuses. Award-winning results. Ready?

7
content/admin.md Normal file
View File

@@ -0,0 +1,7 @@
---
title: "Admin Panel"
date: 2024-12-24T12:00:00Z
draft: false
layout: "admin"
---

4
content/albums/_index.md Normal file
View File

@@ -0,0 +1,4 @@
---
title: "Photo Albums"
description: "A visual journey through sterile hydroponic systems."
---

View File

@@ -0,0 +1,30 @@
---
title: "Commercial NFT Lettuce"
description: "A 2000 sq ft facility designed for zero-waste production."
cover: "/images/IMG_2871.jpeg"
images:
- url: "/images/IMG_9463.jpeg"
caption: "IMG 9463.jpeg"
- url: "/images/69844719615__6435DF53-250E-4D10-B453-4DBE2ECC44F1.jpeg"
caption: "69844719615 6435DF53 250E 4D10 B453 4DBE2ECC44F1.jpeg"
- url: "/images/IMG_0081.jpeg"
caption: "IMG 0081.jpeg"
- url: "/images/IMG_9313.jpeg"
caption: "IMG 9313.jpeg"
- url: "/images/IMG_8923.jpeg"
caption: "IMG 8923.jpeg"
- url: "/images/IMG_8405.jpeg"
caption: "IMG 8405.jpeg"
- url: "/images/IMG_9198.jpeg"
caption: "IMG 9198.jpeg"
- url: "/images/IMG_1859.jpeg"
caption: "IMG 1859.jpeg"
- url: "/images/IMG_1436.jpeg"
caption: "IMG 1436.jpeg"
- url: "/images/433320B3-7D36-4965-9E58-DEC07BE6827C.jpg"
caption: "433320B3 7D36 4965 9E58 DEC07BE6827C.jpg"
- url: "/images/IMG_1837.jpeg"
caption: "IMG 1837.jpeg"
- url: "/images/IMG_8894.jpeg"
caption: "IMG 8894.jpeg"
---

View File

@@ -0,0 +1,30 @@
---
title: "Medical RDWC Build"
description: "High-precision Recirculating Deep Water Culture for medicinal crops."
cover: "/images/IMG_0165 2.jpeg"
images:
- url: "/images/IMG_1439.jpeg"
caption: "IMG 1439.jpeg"
- url: "/images/IMG_1436.jpeg"
caption: "IMG 1436.jpeg"
- url: "/images/IMG_0081.jpeg"
caption: "IMG 0081.jpeg"
- url: "/images/IMG_1534.jpeg"
caption: "IMG 1534.jpeg"
- url: "/images/IMG_8377.jpeg"
caption: "IMG 8377.jpeg"
- url: "/images/IMG_1126.jpeg"
caption: "IMG 1126.jpeg"
- url: "/images/IMG_7684.jpeg"
caption: "IMG 7684.jpeg"
- url: "/images/IMG_2838.jpeg"
caption: "IMG 2838.jpeg"
- url: "/images/IMG_9328 3.jpeg"
caption: "IMG 9328 3.jpeg"
- url: "/images/IMG_9328.jpeg"
caption: "IMG 9328.jpeg"
- url: "/images/IMG_6475.jpeg"
caption: "IMG 6475.jpeg"
- url: "/images/E142B0A8-8F28-4288-9B10-770518A3C43B.webp"
caption: "E142B0A8 8F28 4288 9B10 770518A3C43B.webp"
---

338
content/albums/my-garden.md Normal file
View File

@@ -0,0 +1,338 @@
---
title: "My Hydroponic Garden"
date: 2024-03-25T12:00:00-05:00
description: "A comprehensive gallery of my hydroponic plants, setups, and results."
images:
- url: "/images/13AC76BC-B45E-42F1-A9C3-A9E961B3C496.webp"
caption: "13AC76BC B45E 42F1 A9C3 A9E961B3C496.webp"
- url: "/images/20012743-FFB5-4AFB-9DE6-5BE122236EA8.webp"
caption: "20012743 FFB5 4AFB 9DE6 5BE122236EA8.webp"
- url: "/images/433320B3-7D36-4965-9E58-DEC07BE6827C.jpg"
caption: "433320B3 7D36 4965 9E58 DEC07BE6827C.jpg"
- url: "/images/54d957ebf6c9447c8c3214fe371c303a.MOV"
caption: "54d957ebf6c9447c8c3214fe371c303a.MOV"
- url: "/images/64634090387__02F28F99-0128-4628-9F5A-AA3BF5043B30.jpeg"
caption: "64634090387 02F28F99 0128 4628 9F5A AA3BF5043B30.jpeg"
- url: "/images/64792744957__B54FD5A8-4237-4141-8090-E2BED845456C.jpeg"
caption: "64792744957 B54FD5A8 4237 4141 8090 E2BED845456C.jpeg"
- url: "/images/65331164721__D4F8AED9-9B4A-4E75-905D-5027A2D8E440.jpeg"
caption: "65331164721 D4F8AED9 9B4A 4E75 905D 5027A2D8E440.jpeg"
- url: "/images/65804214832__CCBE50A0-8F17-4B36-817F-D1FBBA1A4B3F.jpeg"
caption: "65804214832 CCBE50A0 8F17 4B36 817F D1FBBA1A4B3F.jpeg"
- url: "/images/65804215588__E8E13980-3188-4726-AFE4-0C038A6422C8.jpeg"
caption: "65804215588 E8E13980 3188 4726 AFE4 0C038A6422C8.jpeg"
- url: "/images/69844719615__6435DF53-250E-4D10-B453-4DBE2ECC44F1.jpeg"
caption: "69844719615 6435DF53 250E 4D10 B453 4DBE2ECC44F1.jpeg"
- url: "/images/69844720921__44164E36-77D1-4DBA-BD06-26F8BF86C541.jpeg"
caption: "69844720921 44164E36 77D1 4DBA BD06 26F8BF86C541.jpeg"
- url: "/images/707F7B39-B98D-44CC-AE45-9EF2126F5887.webp"
caption: "707F7B39 B98D 44CC AE45 9EF2126F5887.webp"
- url: "/images/7D196C8E-A924-4600-922D-52FDB61C41D2.png"
caption: "7D196C8E A924 4600 922D 52FDB61C41D2.png"
- url: "/images/BF9E2340-832A-49CD-BED3-4A1801C35E97.jpg"
caption: "BF9E2340 832A 49CD BED3 4A1801C35E97.jpg"
- url: "/images/BF9F1085-AB99-47FA-97E4-56B5658E42FE.webp"
caption: "BF9F1085 AB99 47FA 97E4 56B5658E42FE.webp"
- url: "/images/C56C173F-1C51-4F3D-9E86-AA24B06EEC19.webp"
caption: "C56C173F 1C51 4F3D 9E86 AA24B06EEC19.webp"
- url: "/images/ComfyUI_00012_.png"
caption: "ComfyUI 00012 .png"
- url: "/images/ComfyUI_00013_.png"
caption: "ComfyUI 00013 .png"
- url: "/images/ComfyUI_00014_.png"
caption: "ComfyUI 00014 .png"
- url: "/images/ComfyUI_00015_.png"
caption: "ComfyUI 00015 .png"
- url: "/images/ComfyUI_00023_.png"
caption: "ComfyUI 00023 .png"
- url: "/images/D57F541F-52FA-4B45-9716-758F67F55D79.webp"
caption: "D57F541F 52FA 4B45 9716 758F67F55D79.webp"
- url: "/images/E142B0A8-8F28-4288-9B10-770518A3C43B.webp"
caption: "E142B0A8 8F28 4288 9B10 770518A3C43B.webp"
- url: "/images/E8211B06-A9E4-4A51-A941-757CE191BC57.jpg"
caption: "E8211B06 A9E4 4A51 A941 757CE191BC57.jpg"
- url: "/images/E9DFB924-05F8-4079-9AFD-269567B514D8.jpg"
caption: "E9DFB924 05F8 4079 9AFD 269567B514D8.jpg"
- url: "/images/IMG_0009.jpeg"
caption: "IMG 0009.jpeg"
- url: "/images/IMG_0081.jpeg"
caption: "IMG 0081.jpeg"
- url: "/images/IMG_0113.jpeg"
caption: "IMG 0113.jpeg"
- url: "/images/IMG_0165 2.jpeg"
caption: "IMG 0165 2.jpeg"
- url: "/images/IMG_0165.jpeg"
caption: "IMG 0165.jpeg"
- url: "/images/IMG_0172.jpeg"
caption: "IMG 0172.jpeg"
- url: "/images/IMG_0179 2.jpeg"
caption: "IMG 0179 2.jpeg"
- url: "/images/IMG_0179 3.jpeg"
caption: "IMG 0179 3.jpeg"
- url: "/images/IMG_0179 4.jpeg"
caption: "IMG 0179 4.jpeg"
- url: "/images/IMG_0179.jpeg"
caption: "IMG 0179.jpeg"
- url: "/images/IMG_0332.jpeg"
caption: "IMG 0332.jpeg"
- url: "/images/IMG_0333.jpeg"
caption: "IMG 0333.jpeg"
- url: "/images/IMG_0334.jpeg"
caption: "IMG 0334.jpeg"
- url: "/images/IMG_0599.jpeg"
caption: "IMG 0599.jpeg"
- url: "/images/IMG_0784.jpeg"
caption: "IMG 0784.jpeg"
- url: "/images/IMG_0801.jpeg"
caption: "IMG 0801.jpeg"
- url: "/images/IMG_0904.jpeg"
caption: "IMG 0904.jpeg"
- url: "/images/IMG_0CFF7F7D-C8F8-4051-BCA5-B98C40D9D387.jpeg"
caption: "IMG 0CFF7F7D C8F8 4051 BCA5 B98C40D9D387.jpeg"
- url: "/images/IMG_1011 2.HEIC"
caption: "IMG 1011 2.HEIC"
- url: "/images/IMG_1011.HEIC"
caption: "IMG 1011.HEIC"
- url: "/images/IMG_1020 2.jpeg"
caption: "IMG 1020 2.jpeg"
- url: "/images/IMG_1020.jpeg"
caption: "IMG 1020.jpeg"
- url: "/images/IMG_1021.jpeg"
caption: "IMG 1021.jpeg"
- url: "/images/IMG_1058.jpeg"
caption: "IMG 1058.jpeg"
- url: "/images/IMG_1062 2.jpeg"
caption: "IMG 1062 2.jpeg"
- url: "/images/IMG_1062.jpeg"
caption: "IMG 1062.jpeg"
- url: "/images/IMG_1104 2.jpeg"
caption: "IMG 1104 2.jpeg"
- url: "/images/IMG_1104.jpeg"
caption: "IMG 1104.jpeg"
- url: "/images/IMG_1126.jpeg"
caption: "IMG 1126.jpeg"
- url: "/images/IMG_1160.jpeg"
caption: "IMG 1160.jpeg"
- url: "/images/IMG_1435.jpeg"
caption: "IMG 1435.jpeg"
- url: "/images/IMG_1436.jpeg"
caption: "IMG 1436.jpeg"
- url: "/images/IMG_1439.jpeg"
caption: "IMG 1439.jpeg"
- url: "/images/IMG_1534.jpeg"
caption: "IMG 1534.jpeg"
- url: "/images/IMG_1767.jpeg"
caption: "IMG 1767.jpeg"
- url: "/images/IMG_1768.jpeg"
caption: "IMG 1768.jpeg"
- url: "/images/IMG_1837.jpeg"
caption: "IMG 1837.jpeg"
- url: "/images/IMG_1859.jpeg"
caption: "IMG 1859.jpeg"
- url: "/images/IMG_1864.jpeg"
caption: "IMG 1864.jpeg"
- url: "/images/IMG_2822.jpeg"
caption: "IMG 2822.jpeg"
- url: "/images/IMG_2826.jpeg"
caption: "IMG 2826.jpeg"
- url: "/images/IMG_2828.jpeg"
caption: "IMG 2828.jpeg"
- url: "/images/IMG_2835.jpeg"
caption: "IMG 2835.jpeg"
- url: "/images/IMG_2838.jpeg"
caption: "IMG 2838.jpeg"
- url: "/images/IMG_2870.jpeg"
caption: "IMG 2870.jpeg"
- url: "/images/IMG_2871.jpeg"
caption: "IMG 2871.jpeg"
- url: "/images/IMG_3528.jpeg"
caption: "IMG 3528.jpeg"
- url: "/images/IMG_4029.jpeg"
caption: "IMG 4029.jpeg"
- url: "/images/IMG_4674.JPG"
caption: "IMG 4674.JPG"
- url: "/images/IMG_5008.jpeg"
caption: "IMG 5008.jpeg"
- url: "/images/IMG_5218.jpeg"
caption: "IMG 5218.jpeg"
- url: "/images/IMG_5261.jpeg"
caption: "IMG 5261.jpeg"
- url: "/images/IMG_5262.jpeg"
caption: "IMG 5262.jpeg"
- url: "/images/IMG_5374.jpeg"
caption: "IMG 5374.jpeg"
- url: "/images/IMG_5698.jpeg"
caption: "IMG 5698.jpeg"
- url: "/images/IMG_6108.jpeg"
caption: "IMG 6108.jpeg"
- url: "/images/IMG_6180.jpeg"
caption: "IMG 6180.jpeg"
- url: "/images/IMG_6389.jpeg"
caption: "IMG 6389.jpeg"
- url: "/images/IMG_6426.jpeg"
caption: "IMG 6426.jpeg"
- url: "/images/IMG_6475.jpeg"
caption: "IMG 6475.jpeg"
- url: "/images/IMG_6477.jpeg"
caption: "IMG 6477.jpeg"
- url: "/images/IMG_6501.jpeg"
caption: "IMG 6501.jpeg"
- url: "/images/IMG_6574.jpeg"
caption: "IMG 6574.jpeg"
- url: "/images/IMG_6919.jpeg"
caption: "IMG 6919.jpeg"
- url: "/images/IMG_6920.jpeg"
caption: "IMG 6920.jpeg"
- url: "/images/IMG_7077.jpeg"
caption: "IMG 7077.jpeg"
- url: "/images/IMG_7078.jpeg"
caption: "IMG 7078.jpeg"
- url: "/images/IMG_7159.jpeg"
caption: "IMG 7159.jpeg"
- url: "/images/IMG_7374.jpeg"
caption: "IMG 7374.jpeg"
- url: "/images/IMG_7447.jpeg"
caption: "IMG 7447.jpeg"
- url: "/images/IMG_7677.jpeg"
caption: "IMG 7677.jpeg"
- url: "/images/IMG_7684.jpeg"
caption: "IMG 7684.jpeg"
- url: "/images/IMG_7712.JPG"
caption: "IMG 7712.JPG"
- url: "/images/IMG_7999.jpeg"
caption: "IMG 7999.jpeg"
- url: "/images/IMG_8093.jpeg"
caption: "IMG 8093.jpeg"
- url: "/images/IMG_8095.jpeg"
caption: "IMG 8095.jpeg"
- url: "/images/IMG_8377.jpeg"
caption: "IMG 8377.jpeg"
- url: "/images/IMG_8405.jpeg"
caption: "IMG 8405.jpeg"
- url: "/images/IMG_8483.jpeg"
caption: "IMG 8483.jpeg"
- url: "/images/IMG_8586.jpeg"
caption: "IMG 8586.jpeg"
- url: "/images/IMG_8594.jpeg"
caption: "IMG 8594.jpeg"
- url: "/images/IMG_8894.jpeg"
caption: "IMG 8894.jpeg"
- url: "/images/IMG_8923.jpeg"
caption: "IMG 8923.jpeg"
- url: "/images/IMG_8926.jpeg"
caption: "IMG 8926.jpeg"
- url: "/images/IMG_9103.jpeg"
caption: "IMG 9103.jpeg"
- url: "/images/IMG_9104.jpeg"
caption: "IMG 9104.jpeg"
- url: "/images/IMG_9112.jpeg"
caption: "IMG 9112.jpeg"
- url: "/images/IMG_9129.jpeg"
caption: "IMG 9129.jpeg"
- url: "/images/IMG_9135.jpeg"
caption: "IMG 9135.jpeg"
- url: "/images/IMG_9166.jpeg"
caption: "IMG 9166.jpeg"
- url: "/images/IMG_9167.jpeg"
caption: "IMG 9167.jpeg"
- url: "/images/IMG_9195.jpeg"
caption: "IMG 9195.jpeg"
- url: "/images/IMG_9196 2.jpeg"
caption: "IMG 9196 2.jpeg"
- url: "/images/IMG_9196 3.jpeg"
caption: "IMG 9196 3.jpeg"
- url: "/images/IMG_9196 4.jpeg"
caption: "IMG 9196 4.jpeg"
- url: "/images/IMG_9196.jpeg"
caption: "IMG 9196.jpeg"
- url: "/images/IMG_9198.jpeg"
caption: "IMG 9198.jpeg"
- url: "/images/IMG_9202.MOV"
caption: "IMG 9202.MOV"
- url: "/images/IMG_9242.jpeg"
caption: "IMG 9242.jpeg"
- url: "/images/IMG_9255.jpeg"
caption: "IMG 9255.jpeg"
- url: "/images/IMG_9269.jpeg"
caption: "IMG 9269.jpeg"
- url: "/images/IMG_9308.jpeg"
caption: "IMG 9308.jpeg"
- url: "/images/IMG_9313.jpeg"
caption: "IMG 9313.jpeg"
- url: "/images/IMG_9316.jpeg"
caption: "IMG 9316.jpeg"
- url: "/images/IMG_9328 2.jpeg"
caption: "IMG 9328 2.jpeg"
- url: "/images/IMG_9328 3.jpeg"
caption: "IMG 9328 3.jpeg"
- url: "/images/IMG_9328.jpeg"
caption: "IMG 9328.jpeg"
- url: "/images/IMG_9335.jpeg"
caption: "IMG 9335.jpeg"
- url: "/images/IMG_9357.PNG"
caption: "IMG 9357.PNG"
- url: "/images/IMG_9367.jpeg"
caption: "IMG 9367.jpeg"
- url: "/images/IMG_9385.jpeg"
caption: "IMG 9385.jpeg"
- url: "/images/IMG_9410.jpeg"
caption: "IMG 9410.jpeg"
- url: "/images/IMG_9411.jpeg"
caption: "IMG 9411.jpeg"
- url: "/images/IMG_9413.jpeg"
caption: "IMG 9413.jpeg"
- url: "/images/IMG_9419.jpeg"
caption: "IMG 9419.jpeg"
- url: "/images/IMG_9428.jpeg"
caption: "IMG 9428.jpeg"
- url: "/images/IMG_9429.jpeg"
caption: "IMG 9429.jpeg"
- url: "/images/IMG_9432.jpeg"
caption: "IMG 9432.jpeg"
- url: "/images/IMG_9446.jpeg"
caption: "IMG 9446.jpeg"
- url: "/images/IMG_9448.jpeg"
caption: "IMG 9448.jpeg"
- url: "/images/IMG_9449.jpeg"
caption: "IMG 9449.jpeg"
- url: "/images/IMG_9454.jpeg"
caption: "IMG 9454.jpeg"
- url: "/images/IMG_9463.jpeg"
caption: "IMG 9463.jpeg"
- url: "/images/IMG_9519.jpeg"
caption: "IMG 9519.jpeg"
- url: "/images/IMG_9520.jpeg"
caption: "IMG 9520.jpeg"
- url: "/images/IMG_9564.jpeg"
caption: "IMG 9564.jpeg"
- url: "/images/IMG_9612 2.jpeg"
caption: "IMG 9612 2.jpeg"
- url: "/images/IMG_9612.jpeg"
caption: "IMG 9612.jpeg"
- url: "/images/IMG_9613 2.jpeg"
caption: "IMG 9613 2.jpeg"
- url: "/images/IMG_9613 3.jpeg"
caption: "IMG 9613 3.jpeg"
- url: "/images/IMG_9613.jpeg"
caption: "IMG 9613.jpeg"
- url: "/images/IMG_9768.jpeg"
caption: "IMG 9768.jpeg"
- url: "/images/IMG_9769.jpeg"
caption: "IMG 9769.jpeg"
- url: "/images/Screenshot 2024-03-25 195708.png"
caption: "Screenshot 2024 03 25 195708.png"
- url: "/images/WBYS8305.JPEG"
caption: "WBYS8305.JPEG"
- url: "/images/f130260032.jpeg"
caption: "f130260032.jpeg"
- url: "/images/f130303808.jpeg"
caption: "f130303808.jpeg"
- url: "/images/f131874624.jpeg"
caption: "f131874624.jpeg"
- url: "/images/f139364160.jpeg"
caption: "f139364160.jpeg"
- url: "/images/f153047488.jpeg"
caption: "f153047488.jpeg"
- url: "/images/f157200512.jpeg"
caption: "f157200512.jpeg"
---

4
content/blog/_index.md Normal file
View File

@@ -0,0 +1,4 @@
---
title: "Grow Journal"
description: "Notes from the field."
---

12
content/blog/post-1.md Normal file
View File

@@ -0,0 +1,12 @@
---
title: "Why I Stopped Using Organic Nutrients in Hydro"
date: 2024-01-10T10:00:00Z
image: "/images/69844720921__44164E36-77D1-4DBA-BD06-26F8BF86C541.jpeg"
summary: "Biofilm, clogs, and unstable pH. Why organics belong in soil, not in your reservoir."
---
I wanted to believe in "organic hydroponics." Clean water plus natural nutrients sounded dreamy. After three straight failures, I had to face the music.
Organic nutrients are liquid compost. In soil, microbes break them down. In a sterile reservoir they just turn to slime—clogged emitters, coated roots, suffocated plants.
Switching back to mineral salts wasn't giving up; it was waking up. Yields doubled and cleaning went from hours to minutes.

22
content/blog/post-10.md Normal file
View File

@@ -0,0 +1,22 @@
---
title: "Alright folks, let’s talk hydro! After two decades wrestling with cannabis in sterile environment..."
date: 2025-12-26T10:50:30Z
image: "/images/IMG_1534.jpeg"
summary: "First off – “sterile” matters when we're talking hydroponics with cannabis because root health IS everything and disease spreads fast in water based s..."
---
First off – “sterile” matters when we're talking hydroponics with cannabis because root health IS everything and disease spreads fast in water based systems without proper controls.. We want to minimize the microbial load so plants focus on *growing*, not fighting stuff off constantly . Think consistent sterilization of reservoirs, tubing & grow media.
Let’s break down some common sterile-friendly options:
* **Deep Water Culture (DWC):** The simplest! Roots dangle in oxygenated nutrient solution; good for beginners and scaling up is relatively easy with multiple buckets/reservoirs.. Sterilization? Easy – fully replace the reservoir weekly, or biweekly depending on plant size. Keep that air pump running strong though - dissolved O2 critical here
* **Recirculating Deep Water Culture (RDWC):** DWC but linked together! One central oxygenated nutrient res feeds multiple smaller containers with individual net pots.. More complex setup and requires a little more monitoring, especially for pH drift across the system. Sterilization is still largely reservoir based – though you’ll want to flush all connecting lines weekly when changing out solution
* **Drain-to-Waste (DTW):** My personal workhorse! Nutrient flows *through*, not recirculates.. Less risk of disease buildup because old nutrient gets flushed with each watering cycle. You need a bit more nute and water, but the consistency in uptake is worth it for consistent results . Sterilization focuses on initial reservoir prep & ensuring clean tubing/media
* **Rockwool Slab / Coco Coir Systems:** Technically *hydroponic*, even though they use an inert growing medium.. Excellent control over root zone environment. Requires careful pH and EC management, but are very forgiving – especially coco coirs which can handle minor fluctuations much better than rock wool . Sterilization of the slabs/bricks before planting is key!
**Troubleshooting 101:** Don’t panic when things go sideways (and they will!). Here's what I see most:
* **Root Rot**: Usually caused by Pythium or Fusarium. Often from low dissolved oxygen, high temperatures and not enough sterilization.. Check your water temp; aim for under 75F/24C , bump up aeration & replace that solution! Consider a hydrogen peroxide addition to the reservoir (3% H₂O₂, ~1 tsp per gallon)
* **pH Drift:** Common in recirculating systems. Calibrate you pH meter regularly, and check it at multiple points within your system.. Check for algae growth too – light hitting nutrient solutions encourages them & throws off readings!
***Nutrient Lockout**: Happens when salts build up or a specific element is unavailable to the plant . Flush with plain PH balanced water. If that doesn’t work, reassess you EC/PPM and make sure your ratios are correct for what growth stage they're in
Bottom line? Sterile hydroponics takes some attention but yields amazing results! Don’t be afraid experiment & document everything - it pays dividends later down the road when scaling up or optimizing a system.

30
content/blog/post-11.md Normal file
View File

@@ -0,0 +1,30 @@
---
title: "Alright, let’s talk roots – specifically how getting oxygen *to* those cannabis roots is the sing..."
date: 2025-12-26T16:48:36Z
image: "/images/IMG_8377.jpeg"
summary: "Everyone focuses so much on lights and nutrient formulas – crucial stuff sure - but if your root zone is suffocating in anaerobic conditions you're le..."
---
Everyone focuses so much on lights and nutrient formulas – crucial stuff sure - but if your root zone is suffocating in anaerobic conditions you're leaving serious potential untapped.. Cannabis *loves* oxygen, especially when it’s pulling nutrients directly from the water column instead of sifting through soil.
**So how do we maximize that vital O2? Here are a few techniques I swear by:**
1.**Consistent Water Temperature is Key**: Aim for between 68-74°F (around ~20 Celsius). Warmer temps hold less dissolved oxygen, colder slows nutrient uptake.. A simple aquarium heater with an inline thermostat will do the trick. Don’t cheap out here; stability matters more than fancy features
2.**Air Stone Power – But Size Matters**: Everyone uses air stones in DWC! Great start,. The mistake I see a lot is using too *few* or ones that are clogged.. Get enough to create consistent, but not violent turbulence. Think gentle bubbling throughout the entire reservoir - it should look like its lightly simmering
3.**Hydrogen Peroxide (H2O2) – A Sterile Boost**: I use H₂0₂, at a low dose of 1-2 tsp per gallon every few days,. It breaks down into water and oxygen, giving those roots an extra boost. Plus helps keeps the system cleaner reducing root rot.. Just don’t overdo it - too much can shock them!
4.**Water Change Frequency**: A lot depends on your plant size & nutrient uptake but I always aim for a complete reservoir change every 7-10 days, especially during peak bloom,. This replenishes oxygen and prevents salt buildup. Don't just top off – *replace* the water completely.. It’s more work upfront that yields big results
5.**Root Mass Inspection:** When you do your changes pull out some roots! Healthy cannabis root are bright white & fluffy - a sign of healthy O2 uptake . Brown, slimy mush is an indicator something'a amiss
These aren’t groundbreaking secrets,. but they *are* often overlooked. Sterile hydroponics isn't just about the nutrients; it requires active management to create and maintain that ideal oxygen-rich environment for your cannabis roots.. Nail this part down ,and you will see a significant difference in vigor, growth rate & ultimately – bud density!
I’ll dive deeper into sterile system sanitation next time. Until then happy growing folks!.

12
content/blog/post-2.md Normal file
View File

@@ -0,0 +1,12 @@
---
title: "The Truth About H2O2 (Hydrogen Peroxide)"
date: 2024-02-15T10:00:00Z
image: "/images/IMG_9769.jpeg"
summary: "It's a powerful tool, but it's not a long-term solution. Here is why."
---
Hydrogen Peroxide is the flamethrower of the hydro world. It kills everything—good and bad—on contact.
Its half-life in a nutrient solution is tiny. Within hours it's just water and oxygen. If you've got a dirty pump or line feeding pathogens, H2O2 will clear it for a moment and the colony will be back by morning.
I use H2O2 for *cleaning equipment between runs*. For daily maintenance, I prefer Hypochlorous Acid.

12
content/blog/post-3.md Normal file
View File

@@ -0,0 +1,12 @@
---
title: "My Battle with Pythium (and How I Won)"
date: 2024-03-01T10:00:00Z
image: "/images/IMG_6475.jpeg"
summary: "Root rot isn't a death sentence if you catch it early and treat it aggressively."
---
It started as slight discoloration on the root tips. I ignored it. Two days later the whole reservoir smelled like a swamp. Pythium moves fast.
My mistake was trusting "beneficial bacteria" in a system running too warm (75°F). Pythium loves warm, low-oxygen soup.
The fix? A water chiller. Dropping to 66°F was the turning point. Cold water holds more oxygen, and Pythium hates that.

13
content/blog/post-4.md Normal file
View File

@@ -0,0 +1,13 @@
---
title: "VPD Explained for Complete Beginners"
date: 2024-03-20T10:00:00Z
image: "/images/IMG_9449.jpeg"
summary: "Temperature and humidity are connected. Learn how to hit the sweet spot."
---
VPD stands for Vapor Pressure Deficit. Think of it as the air's drying power.
If the air is too dry (high VPD), plants sweat too much and slam their stomata to save water. Growth stops.
If the air is too humid (low VPD), they barely sweat. No water movement means no calcium uptake.
You want the Goldilocks zone. For early veg, aim around 0.8 kPa. For flower, push 1.2–1.4 kPa.

10
content/blog/post-5.md Normal file
View File

@@ -0,0 +1,10 @@
---
title: "Review: The Best pH Pen Under $100"
date: 2024-04-05T10:00:00Z
image: "/images/IMG_9413.jpeg"
summary: "Stop buying the yellow $10 pens. They are costing you your harvest."
---
I've used BlueLab, Hanna, and the cheap yellow generics. The value winner? The Apera PH60.
It has a replaceable probe, decent calibration stability, and it's waterproof. The most important feature of a pH pen is trust. If you can't trust the reading (and keep it calibrated), you're flying blind.

10
content/blog/post-6.md Normal file
View File

@@ -0,0 +1,10 @@
---
title: "Why Your Reservoir Smells Bad"
date: 2024-04-20T10:00:00Z
image: "/images/IMG_2870.jpeg"
summary: "Healthy roots smell like fresh rain or cut potatoes. Swamp smell means trouble."
---
If you open your res and it smells like sulfur or rotten eggs, you have anaerobic bacteria. These thrive in low-oxygen water.
The immediate fix? Add more air. Get a bigger air pump and more stones. You can't really have "too much" dissolved oxygen in a home setup. Then figure out why the system got that stale in the first place.

11
content/blog/post-7.md Normal file
View File

@@ -0,0 +1,11 @@
---
title: "Rockwool vs. Coco Coir: A Sterile Perspective"
date: 2024-05-10T10:00:00Z
image: "/images/IMG_9612 2.jpeg"
summary: "Why I prefer inert media for precise control."
---
Coco is great, but it's organic. It comes with its own cation exchange capacity (CEC), meaning it holds onto Calcium and Magnesium. That's an extra variable in your math.
Rockwool is basalt rock spun into fiber. It is chemically inert. It holds nothing. If I pour 100ppm of Nitrogen in, 100ppm is available to the plant. For scientific growing, Rockwool wins.
If you want sterile, rockwool plays nice; coco fights back.

11
content/blog/post-8.md Normal file
View File

@@ -0,0 +1,11 @@
---
title: "Automating pH: Is it Worth It?"
date: 2024-06-01T10:00:00Z
image: "/images/IMG_9768.jpeg"
summary: "Dosers are cool, but they can fail. Should you trust a robot with your crop?"
---
I love automation, but I always tell new growers: **Don't automate what you don't understand.**
If you don't know *why* your pH is drifting up (plants eating nitrates) or down (root rot or ammonia), a doser will just mask the problem until it's too late. Master manual control first. Then get the robot.
Once you can predict the drift before it happens, then let the robot babysit.

22
content/blog/post-9.md Normal file
View File

@@ -0,0 +1,22 @@
---
title: "Dialing EC and pH in Sterile Hydro"
date: 2025-12-26T09:22:47Z
image: "/images/IMG_1439.jpeg"
summary: "How I set EC and pH, what the drifts mean, and how to log it without losing your mind."
---
Sterile growing means zero buffer. EC and pH are your steering wheel.
**EC (electrical conductivity):** total salts/ppm. I keep veg around 1.6–2.0 EC and push 2.4–3.0 in flower depending on strain and light intensity. Don't chase numbers blind—watch the leaves. Light green with stalled growth? You probably underfed. Dark, clawed tips? Back off.
**pH:** Cannabis likes slightly acidic solution. My sweet spot is 5.8–6.2 in the reservoir. Calibrate your meter often; cheap meters can burn you faster than a bad dab.
**Common screw-up:** adjusting pH without checking EC first. If the soup is too hot or too weak, pH tweaks are lipstick on a pig.
**Practical tips:**
- Start low and creep up. Easier to add salts than undo burn.
- Check pH daily. Check EC every couple days or any time the plants look different.
- Track everything—reservoir swaps, nutrient adds, readings. Your logbook will solve next week's mystery smell.
Happy growing,
Dr Jones

11
content/chat.md Normal file
View File

@@ -0,0 +1,11 @@
---
title: "Chat with Dr. Jones AI"
date: 2024-12-26T12:00:00Z
draft: false
layout: "chat"
---
# Chat with Dr. Jones AI
Get instant answers about sterile hydroponics, cannabis cultivation, and growing techniques from an AI trained on 20 years of experience.

61
content/contact.md Normal file
View File

@@ -0,0 +1,61 @@
---
title: "Contact"
date: 2024-12-24T12:00:00Z
draft: false
---
No assistant. No gatekeeper. If you email me, you're talking to the same dude cleaning his reservoirs.
### How to Reach Me
<div class="membership-only" style="display: none;">
**Email:** [indianaholmes@thetempleofdoom.com](mailto:indianaholmes@thetempleofdoom.com)
</div>
<div class="guest-only" style="display: block;">
**Email:** <span class="text-gray-400">Available to members only</span>
</div>
If you're looking for a consultation, hit the form below or email me directly (members only). I usually reply within 24 hours on weekdays unless I'm mid-harvest.
**Quick questions:** Check the [Blog](/blog) first. I might have already ranted about it there.
---
<!-- Paywall Message -->
<div class="paywall-message mt-8 bg-gray-800 rounded-lg p-8 border border-gray-700 text-center" style="display: none;">
<h2 class="text-2xl font-bold text-gray-100 mb-4">Members Only</h2>
<p class="text-gray-300 mb-6">
The contact form is available exclusively to members. Sign up to get direct access to email me.
</p>
<a href="/membership/" class="inline-flex items-center px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors">
Become a Member
</a>
</div>
<!-- Contact Form (Members Only) -->
<form id="contact-form" class="paywall-content mt-8 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-8" style="display: none;">
<div class="sm:col-span-2">
<label for="name" class="block text-sm font-medium text-gray-300">Name</label>
<div class="mt-1">
<input type="text" name="name" id="name" autocomplete="name" required class="py-3 px-4 block w-full bg-gray-800 border border-gray-700 rounded-md text-gray-100 focus:ring-primary focus:border-primary">
</div>
</div>
<div class="sm:col-span-2">
<label for="email" class="block text-sm font-medium text-gray-300">Your Email</label>
<div class="mt-1">
<input type="email" name="email" id="email" autocomplete="email" required class="py-3 px-4 block w-full bg-gray-800 border border-gray-700 rounded-md text-gray-100 focus:ring-primary focus:border-primary">
</div>
</div>
<div class="sm:col-span-2">
<label for="message" class="block text-sm font-medium text-gray-300">Message</label>
<div class="mt-1">
<textarea id="message" name="message" rows="4" required class="py-3 px-4 block w-full bg-gray-800 border border-gray-700 rounded-md text-gray-100 focus:ring-primary focus:border-primary"></textarea>
</div>
</div>
<div class="sm:col-span-2">
<button type="submit" class="w-full inline-flex items-center justify-center px-6 py-3 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-primary hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
Send Message
</button>
</div>
</form>

10
content/earnings.md Normal file
View File

@@ -0,0 +1,10 @@
---
title: "Potential Earnings Calculator"
date: 2024-12-24T12:00:00Z
draft: false
layout: "earnings"
---
# Discover Your Potential Earnings
Play with this calculator to see what a sterile run can net you. My baseline: **1.5lb of cannabis from a 2x2.5x7 space**. It's a sanity check, not a lottery ticket.

View File

@@ -0,0 +1,4 @@
---
title: "Library"
layout: "library"
---

7
content/membership.md Normal file
View File

@@ -0,0 +1,7 @@
---
title: "Membership"
date: 2024-12-24T12:00:00Z
draft: false
layout: "membership"
---

36
content/offerings.md Normal file
View File

@@ -0,0 +1,36 @@
---
title: "What I Bring to the Table"
date: 2024-12-24T12:00:00Z
draft: false
---
# What I Bring to the Table
I'm not here to feed you Reddit folklore. I'm a mellow science nerd who's been running sterile hydro in a legal state for two decades and smoking the proof. I grow the cleanest, loudest plants I can and teach the same way.
## The Real Deal
I don't hand out generic templates. I give you the guts and know-how to trust your own room. DIY tutorials tuned to your exact space and gear so you can grow anything without guessing.
No more random advice that costs you harvests. You get a direct line to someone who's seen the weird problems, fixed them, and knows what your plants actually need.
## Sterile Hydroponics: The Methodology
The foundation is **sterile hydroponics**. It's a mindset for your grow room: clean inputs, clean plumbing, and no biological wildcards.
The secrets are in the details I've refined over twenty years—timing, dosing, cleaning cadence—the little moves that separate award-winning harvests from "good enough."
Those details are what you get when we work together: the protocols, the timing, the tricks that turn a decent grow into something special.
## What You Actually Get
- **Personalized guidance** built for your setup
- **DIY walkthroughs** that match your space and gear
- **Real fixes** to real problems, not theory for theory's sake
- **Hard-earned playbooks** from 20 years hands-on
- **Confidence** to run clean without second-guessing
- **A direct line** to someone who lives this every day
I'm not here to babysit every step. I'm here to hand you the tools and courage to run a sterile, high-output grow on your own.
The secrets are real. The results are real. Ready to stop guessing and start growing on purpose?

View File

@@ -0,0 +1,19 @@
---
title: "One-on-One Tutoring"
date: 2024-12-24T01:00:00Z
price: "$20"
price_period: "/ hour"
cta: "Book Session"
foxy_link: "https://example.foxycart.com/cart?name=Tutoring&price=20"
---
A 60-minute video call to untangle your exact situation and leave you with a plan.
**What's Included:**
- Quick diagnosis of deficiencies, pests, or pathogens.
- Sterile nutrient recipe tune-up for your reservoir.
- Protocol check on how you're cleaning and running the system.
- Session recording so you don't miss the details.
**Who it's for:**
Home growers stuck on a specific problem who want straight answers instead of forum guesses.

View File

@@ -0,0 +1,19 @@
---
title: "System Design Review"
date: 2024-12-24T02:00:00Z
price: "$200"
price_period: "/ project"
cta: "Submit Design"
foxy_link: "https://example.foxycart.com/cart?name=DesignReview&price=200"
---
Before you build, let me sanity-check your engineering so it stays sterile and easy to maintain.
**What's Included:**
- Full schematic audit (plumbing, lighting, HVAC).
- Parts and compatibility check.
- Sterility risk map.
- Written report with 3-5 actionable fixes.
**Who it's for:**
Growers planning a new room or major retrofit who want to avoid expensive rework and keep everything lab-clean from day one.

View File

@@ -0,0 +1,19 @@
---
title: "Ongoing Mentorship"
date: 2024-12-24T03:00:00Z
price: "$500"
price_period: "/ month"
cta: "Request Mentorship"
foxy_link: "https://example.foxycart.com/cart?name=Mentorship&price=500&sub_frequency=1m"
---
I become your fractional Master Grower for an entire crop cycle.
**What's Included:**
- Weekly 30-min check-in calls.
- Unlimited email/text support (within business hours).
- Real-time crop steering on EC, pH, and canopy calls.
- Harvest and curing guidance.
**Who it's for:**
Serious growers chasing competition-quality results who want a safety net and someone to geek out with along the way.

View File

@@ -0,0 +1,4 @@
---
title: "Services"
description: "Professional Hydroponic Consulting"
---

6
content/testimonials.md Normal file
View File

@@ -0,0 +1,6 @@
---
title: "Testimonials"
date: 2024-12-24T12:00:00Z
draft: false
layout: "testimonials"
---

514
data/library.yaml Normal file
View File

@@ -0,0 +1,514 @@
- title: 'Hydroponics Garden: The Complete DIY Guide For Beginners To Learn How To
Build A System To Grow Plants'
filename: Hydroponics Garden The Complete DIY Guide For Beginners To Learn How To
Build A System To Grow Plants.pdf
description: 'Educational resource: Hydroponics Garden: The Complete DIY Guide For
Beginners To Learn How To Build A System To Grow Plants'
category: Systems
date: '2024-03-25'
file: /library/Hydroponics Garden The Complete DIY Guide For Beginners To Learn
How To Build A System To Grow Plants.pdf
- title: Beginner's Guide to Growing Cannabis and Making Your Own Healing Remedies
filename: Beginner's Guide to Growing Cannabis and Making Your Own Healing Remedies.epub
description: 'Educational resource: Beginner''s Guide to Growing Cannabis and Making
Your Own Healing Remedies'
category: Cultivation
date: '2024-03-25'
file: /library/Beginner's Guide to Growing Cannabis and Making Your Own Healing
Remedies.epub
- title: Totally Organic Hydroponics
filename: Totally Organic Hydroponics.pdf
description: 'Educational resource: Totally Organic Hydroponics'
category: Systems
date: '2024-03-25'
file: /library/Totally Organic Hydroponics.pdf
- title: 'Clearing the Smoke: The Science of Cannabis'
filename: Clearing the Smoke The Science of Cannabis.mp4
description: 'Educational resource: Clearing the Smoke: The Science of Cannabis'
category: Cultivation
date: '2024-03-25'
file: /library/Clearing the Smoke The Science of Cannabis.mp4
- title: Gardening For Beginners 2024
filename: Gardening For Beginners 2024.pdf
description: 'Educational resource: Gardening For Beginners 2024'
category: General
date: '2024-03-25'
file: /library/Gardening For Beginners 2024.pdf
- title: Encyclopedia of Garden Plants for Every Location
filename: Encyclopedia of Garden Plants for Every Location.pdf
description: 'Educational resource: Encyclopedia of Garden Plants for Every Location'
category: General
date: '2024-03-25'
file: /library/Encyclopedia of Garden Plants for Every Location.pdf
- title: 'Cannabis: A Complete Guide'
filename: Cannabis A Complete Guide.pdf
description: 'Educational resource: Cannabis: A Complete Guide'
category: Cultivation
date: '2024-03-25'
file: /library/Cannabis A Complete Guide.pdf
- title: Guide to Hydroponics
filename: Guide to Hydroponics.epub
description: 'Educational resource: Guide to Hydroponics'
category: Systems
date: '2024-03-25'
file: /library/Guide to Hydroponics.epub
- title: 'Marijuana Harvest: How to Maximize Quality and Yield in Your Cannabis Garden'
filename: Marijuana Harvest How to Maximize Quality and Yield in Your Cannabis Garden.epub
description: 'Educational resource: Marijuana Harvest: How to Maximize Quality and
Yield in Your Cannabis Garden'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Harvest How to Maximize Quality and Yield in Your Cannabis
Garden.epub
- title: The Ecology of Plants
filename: The Ecology of Plants.pdf
description: 'Educational resource: The Ecology of Plants'
category: Science
date: '2024-03-25'
file: /library/The Ecology of Plants.pdf
- title: 'Hydroponic Food Production: A Definitive Guidebook for the Commercial Hydroponic
Grower'
filename: Hydroponic Food Production A Definitive Guidebook for the Commercial Hydroponic
Grower.pdf
description: 'Educational resource: Hydroponic Food Production: A Definitive Guidebook
for the Commercial Hydroponic Grower'
category: Systems
date: '2024-03-25'
file: /library/Hydroponic Food Production A Definitive Guidebook for the Commercial
Hydroponic Grower.pdf
- title: 'Grow Great Weed: Personal & Medical Marijuana Indoor Outdoor Grower Big
Bud Bible'
filename: Grow Great Weed Personal & Medical Marijuana Indoor Outdoor Grower Big
Bud Bible.pdf
description: 'Educational resource: Grow Great Weed: Personal & Medical Marijuana
Indoor Outdoor Grower Big Bud Bible'
category: Cultivation
date: '2024-03-25'
file: /library/Grow Great Weed Personal & Medical Marijuana Indoor Outdoor Grower
Big Bud Bible.pdf
- title: Scanned Image
filename: Scanned Image.pdf
description: 'Educational resource: Scanned Image'
category: General
date: '2024-03-25'
file: /library/Scanned Image.pdf
- title: 100 Best Cannabis Strains
filename: 100 Best Cannabis Strains.pdf
description: 'Educational resource: 100 Best Cannabis Strains'
category: Cultivation
date: '2024-03-25'
file: /library/100 Best Cannabis Strains.pdf
- title: Downmagaz.net
filename: Downmagaz.net.pdf
description: 'Educational resource: Downmagaz.net'
category: General
date: '2024-03-25'
file: /library/Downmagaz.net.pdf
- title: The Cannabis Grow Bible
filename: The Cannabis Grow Bible.pdf
description: 'Educational resource: The Cannabis Grow Bible'
category: Cultivation
date: '2024-03-25'
file: /library/The Cannabis Grow Bible.pdf
- title: Diagnosing Hemp and Cannabis Crop Diseases
filename: Diagnosing Hemp and Cannabis Crop Diseases.pdf
description: 'Educational resource: Diagnosing Hemp and Cannabis Crop Diseases'
category: Cultivation
date: '2024-03-25'
file: /library/Diagnosing Hemp and Cannabis Crop Diseases.pdf
- title: Guide to Hydroponics
filename: Guide to Hydroponics.pdf
description: 'Educational resource: Guide to Hydroponics'
category: Systems
date: '2024-03-25'
file: /library/Guide to Hydroponics.pdf
- title: 'Hydroponics Garden: The Complete DIY Guide For Beginners To Learn How To
Build A System To Grow Plants'
filename: Hydroponics Garden The Complete DIY Guide For Beginners To Learn How To
Build A System To Grow Plants.pdf
description: 'Educational resource: Hydroponics Garden: The Complete DIY Guide For
Beginners To Learn How To Build A System To Grow Plants'
category: Systems
date: '2024-03-25'
file: /library/Hydroponics Garden The Complete DIY Guide For Beginners To Learn
How To Build A System To Grow Plants.pdf
- title: 'How to Grow Marijuana: 3 Books in 1 - The Complete Guide to Growing Top-Quality
Weed Indoors and Outdoors'
filename: How to Grow Marijuana 3 Books in 1 - The Complete Guide to Growing Top-Quality
Weed Indoors and Outdoors.pdf
description: 'Educational resource: How to Grow Marijuana: 3 Books in 1 - The Complete
Guide to Growing Top-Quality Weed Indoors and Outdoors'
category: Cultivation
date: '2024-03-25'
file: /library/How to Grow Marijuana 3 Books in 1 - The Complete Guide to Growing
Top-Quality Weed Indoors and Outdoors.pdf
- title: Hydroponics Gardening Guide
filename: Hydroponics Gardening Guide.pdf
description: 'Educational resource: Hydroponics Gardening Guide'
category: Cultivation
date: '2024-03-25'
file: /library/Hydroponics Gardening Guide.pdf
- title: Jesse Ventura's Marijuana Manifesto
filename: Jesse Ventura's Marijuana Manifesto.pdf
description: 'Educational resource: Jesse Ventura''s Marijuana Manifesto'
category: Cultivation
date: '2024-03-25'
file: /library/Jesse Ventura's Marijuana Manifesto.pdf
- title: True Living Organics
filename: True Living Organics.pdf
description: 'Educational resource: True Living Organics'
category: General
date: '2024-03-25'
file: /library/True Living Organics.pdf
- title: 'DIY Home Hydroponics For Beginners: The Essential Guide To Turn Your Backyard
Into A Farm'
filename: DIY Home Hydroponics For Beginners The Essential Guide To Turn Your Backyard
Into A Farm.pdf
description: 'Educational resource: DIY Home Hydroponics For Beginners: The Essential
Guide To Turn Your Backyard Into A Farm'
category: Systems
date: '2024-03-25'
file: /library/DIY Home Hydroponics For Beginners The Essential Guide To Turn Your
Backyard Into A Farm.pdf
- title: 'DIY Home Hydroponics For Beginners: The Essential Guide To Turn Your Backyard
Into A Farm'
filename: DIY Home Hydroponics For Beginners The Essential Guide To Turn Your Backyard
Into A Farm.pdf
description: 'Educational resource: DIY Home Hydroponics For Beginners: The Essential
Guide To Turn Your Backyard Into A Farm'
category: Systems
date: '2024-03-25'
file: /library/DIY Home Hydroponics For Beginners The Essential Guide To Turn Your
Backyard Into A Farm.pdf
- title: What a Plant Knows
filename: What a Plant Knows.pdf
description: 'Educational resource: What a Plant Knows'
category: General
date: '2024-03-25'
file: /library/What a Plant Knows.pdf
- title: 'The Biology of Marijuana: From Gene to Behavior'
filename: The Biology of Marijuana From Gene to Behavior.pdf
description: 'Educational resource: The Biology of Marijuana: From Gene to Behavior'
category: Cultivation
date: '2024-03-25'
file: /library/The Biology of Marijuana From Gene to Behavior.pdf
- title: 'The Cannabis Breeder''s Bible: The Definitive Guide to Marijuana Genetics,
Cannabis Botany and Creating Strains for the Seed Market'
filename: The Cannabis Breeder's Bible The Definitive Guide to Marijuana Genetics,
Cannabis Botany and Creating Strains for the Seed Market.epub
description: 'Educational resource: The Cannabis Breeder''s Bible: The Definitive
Guide to Marijuana Genetics, Cannabis Botany and Creating Strains for the Seed
Market'
category: Cultivation
date: '2024-03-25'
file: /library/The Cannabis Breeder's Bible The Definitive Guide to Marijuana Genetics,
Cannabis Botany and Creating Strains for the Seed Market.epub
- title: 'Cannabis Alchemy: The Art of Modern Hashmaking'
filename: Cannabis Alchemy The Art of Modern Hashmaking.pdf
description: 'Educational resource: Cannabis Alchemy: The Art of Modern Hashmaking'
category: Cultivation
date: '2024-03-25'
file: /library/Cannabis Alchemy The Art of Modern Hashmaking.pdf
- title: 'Marijuana Botany: An Advanced Study - The Propagation and Breeding of Distinctive
Cannabis'
filename: Marijuana Botany An Advanced Study - The Propagation and Breeding of Distinctive
Cannabis.pdf
description: 'Educational resource: Marijuana Botany: An Advanced Study - The Propagation
and Breeding of Distinctive Cannabis'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Botany An Advanced Study - The Propagation and Breeding
of Distinctive Cannabis.pdf
- title: DIY Home Hydroponics For Beginners
filename: DIY Home Hydroponics For Beginners.epub
description: 'Educational resource: DIY Home Hydroponics For Beginners'
category: Systems
date: '2024-03-25'
file: /library/DIY Home Hydroponics For Beginners.epub
- title: Diagnosing Hemp and Cannabis Crop Diseases
filename: Diagnosing Hemp and Cannabis Crop Diseases.pdf
description: 'Educational resource: Diagnosing Hemp and Cannabis Crop Diseases'
category: Cultivation
date: '2024-03-25'
file: /library/Diagnosing Hemp and Cannabis Crop Diseases.pdf
- title: Handbook of Cannabis
filename: Handbook of Cannabis.pdf
description: 'Educational resource: Handbook of Cannabis'
category: Cultivation
date: '2024-03-25'
file: /library/Handbook of Cannabis.pdf
- title: 'Hydroponics: A Standard Methodology for Plant Biological Researches'
filename: Hydroponics A Standard Methodology for Plant Biological Researches.pdf
description: 'Educational resource: Hydroponics: A Standard Methodology for Plant
Biological Researches'
category: Systems
date: '2024-03-25'
file: /library/Hydroponics A Standard Methodology for Plant Biological Researches.pdf
- title: 'Cannabis Sativa Volume 3: The Essential Guide to the World''s Finest Marijuana
Strains'
filename: Cannabis Sativa Volume 3 The Essential Guide to the World's Finest Marijuana
Strains.epub
description: 'Educational resource: Cannabis Sativa Volume 3: The Essential Guide
to the World''s Finest Marijuana Strains'
category: Cultivation
date: '2024-03-25'
file: /library/Cannabis Sativa Volume 3 The Essential Guide to the World's Finest
Marijuana Strains.epub
- title: 'Growing Marijuana Mastery: The Easiest and Complete Handbook to Growing
Weed Indoor and Outdoor'
filename: Growing Marijuana Mastery The Easiest and Complete Handbook to Growing
Weed Indoor and Outdoor.pdf
description: 'Educational resource: Growing Marijuana Mastery: The Easiest and Complete
Handbook to Growing Weed Indoor and Outdoor'
category: Cultivation
date: '2024-03-25'
file: /library/Growing Marijuana Mastery The Easiest and Complete Handbook to Growing
Weed Indoor and Outdoor.pdf
- title: Gardening Indoors with Soil & Hydroponics, Fifth Edition
filename: Gardening Indoors with Soil & Hydroponics, Fifth Edition.pdf
description: 'Educational resource: Gardening Indoors with Soil & Hydroponics, Fifth
Edition'
category: Systems
date: '2024-03-25'
file: /library/Gardening Indoors with Soil & Hydroponics, Fifth Edition.pdf
- title: 'Cannabis Chemistry and Biology: Fundamentals'
filename: Cannabis Chemistry and Biology Fundamentals.pdf
description: 'Educational resource: Cannabis Chemistry and Biology: Fundamentals'
category: Cultivation
date: '2024-03-25'
file: /library/Cannabis Chemistry and Biology Fundamentals.pdf
- title: Nutrients for Hydroponics Tissue Culture
filename: Nutrients for Hydroponics Tissue Culture.pdf
description: 'Educational resource: Nutrients for Hydroponics Tissue Culture'
category: Systems
date: '2024-03-25'
file: /library/Nutrients for Hydroponics Tissue Culture.pdf
- title: 'Marijuana Grow Basics: The Easy Guide for Cannabis Aficionados'
filename: Marijuana Grow Basics The Easy Guide for Cannabis Aficionados.pdf
description: 'Educational resource: Marijuana Grow Basics: The Easy Guide for Cannabis
Aficionados'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Grow Basics The Easy Guide for Cannabis Aficionados.pdf
- title: 'DIY Home Hydroponics For Beginners: The Essential Guide To Turn Your Backyard
Into A Farm'
filename: DIY Home Hydroponics For Beginners The Essential Guide To Turn Your Backyard
Into A Farm.epub
description: 'Educational resource: DIY Home Hydroponics For Beginners: The Essential
Guide To Turn Your Backyard Into A Farm'
category: Systems
date: '2024-03-25'
file: /library/DIY Home Hydroponics For Beginners The Essential Guide To Turn Your
Backyard Into A Farm.epub
- title: '100 Best Cannabis Strains: A Pocket Guide for Medicinal and Recreational
Use'
filename: 100 Best Cannabis Strains A Pocket Guide for Medicinal and Recreational
Use.epub
description: 'Educational resource: 100 Best Cannabis Strains: A Pocket Guide for
Medicinal and Recreational Use'
category: Cultivation
date: '2024-03-25'
file: /library/100 Best Cannabis Strains A Pocket Guide for Medicinal and Recreational
Use.epub
- title: Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible
filename: '#1 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
description: 'Educational resource: Marijuana Horticulture - The Indoor-Outdoor Medical
Growers Bible'
category: Cultivation
date: '2024-03-25'
file: /library/#1 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
- title: Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible
filename: '#2 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
description: 'Educational resource: Marijuana Horticulture - The Indoor-Outdoor Medical
Growers Bible'
category: Cultivation
date: '2024-03-25'
file: /library/#2 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
- title: Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible
filename: '#3 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
description: 'Educational resource: Marijuana Horticulture - The Indoor-Outdoor Medical
Growers Bible'
category: Cultivation
date: '2024-03-25'
file: /library/#3 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
- title: Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible
filename: '#4 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
description: 'Educational resource: Marijuana Horticulture - The Indoor-Outdoor Medical
Growers Bible'
category: Cultivation
date: '2024-03-25'
file: /library/#4 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
- title: Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible
filename: '#5 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
description: 'Educational resource: Marijuana Horticulture - The Indoor-Outdoor Medical
Growers Bible'
category: Cultivation
date: '2024-03-25'
file: /library/#5 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
- title: Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible
filename: '#6 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
description: 'Educational resource: Marijuana Horticulture - The Indoor-Outdoor Medical
Growers Bible'
category: Cultivation
date: '2024-03-25'
file: /library/#6 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
- title: Jesse Ventura's Marijuana Manifesto
filename: Jesse Ventura's Marijuana Manifesto.epub
description: 'Educational resource: Jesse Ventura''s Marijuana Manifesto'
category: Cultivation
date: '2024-03-25'
file: /library/Jesse Ventura's Marijuana Manifesto.epub
- title: 'Grow Great Weed: Personal & Medical Marijuana Indoor Outdoor Grower Big
Bud Bible'
filename: Grow Great Weed Personal & Medical Marijuana Indoor Outdoor Grower Big
Bud Bible.pdf
description: 'Educational resource: Grow Great Weed: Personal & Medical Marijuana
Indoor Outdoor Grower Big Bud Bible'
category: Cultivation
date: '2024-03-25'
file: /library/Grow Great Weed Personal & Medical Marijuana Indoor Outdoor Grower
Big Bud Bible.pdf
- title: Hydroponics Gardening Guide
filename: Hydroponics Gardening Guide.epub
description: 'Educational resource: Hydroponics Gardening Guide'
category: Systems
date: '2024-03-25'
file: /library/Hydroponics Gardening Guide.epub
- title: 'How to Grow Marijuana: 3 Books in 1'
filename: How to Grow Marijuana 3 Books in 1.pdf
description: 'Educational resource: How to Grow Marijuana: 3 Books in 1'
category: Cultivation
date: '2024-03-25'
file: /library/How to Grow Marijuana 3 Books in 1.pdf
- title: 'Cannabis Grower''s Handbook: The Complete Guide to Marijuana and Hemp Cultivation'
filename: Cannabis Grower's Handbook The Complete Guide to Marijuana and Hemp Cultivation.pdf
description: 'Educational resource: Cannabis Grower''s Handbook: The Complete Guide
to Marijuana and Hemp Cultivation'
category: Cultivation
date: '2024-03-25'
file: /library/Cannabis Grower's Handbook The Complete Guide to Marijuana and Hemp
Cultivation.pdf
- title: 'The Big Book of Buds: More Marijuana Varieties from the World''s Greatest
Seed Breeders'
filename: The Big Book of Buds More Marijuana Varieties from the World's Greatest
Seed Breeders.epub
description: 'Educational resource: The Big Book of Buds: More Marijuana Varieties
from the World''s Greatest Seed Breeders'
category: Cultivation
date: '2024-03-25'
file: /library/The Big Book of Buds More Marijuana Varieties from the World's Greatest
Seed Breeders.epub
- title: 'Marijuana Hydroponics: High-Tech Water Culture'
filename: Marijuana Hydroponics High-Tech Water Culture.epub
description: 'Educational resource: Marijuana Hydroponics: High-Tech Water Culture'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Hydroponics High-Tech Water Culture.epub
- title: 'Closet Cultivator: Indoor Marijuana Cultivation Made Easy'
filename: Closet Cultivator Indoor Marijuana Cultivation Made Easy.pdf
description: 'Educational resource: Closet Cultivator: Indoor Marijuana Cultivation
Made Easy'
category: Cultivation
date: '2024-03-25'
file: /library/Closet Cultivator Indoor Marijuana Cultivation Made Easy.pdf
- title: 'What a Plant Knows: A Field Guide to the Senses'
filename: What a Plant Knows A Field Guide to the Senses.epub
description: 'Educational resource: What a Plant Knows: A Field Guide to the Senses'
category: General
date: '2024-03-25'
file: /library/What a Plant Knows A Field Guide to the Senses.epub
- title: Handbook of Cannabis Production in Controlled Environments
filename: Handbook of Cannabis Production in Controlled Environments.pdf
description: 'Educational resource: Handbook of Cannabis Production in Controlled
Environments'
category: Cultivation
date: '2024-03-25'
file: /library/Handbook of Cannabis Production in Controlled Environments.pdf
- title: The Ecology of Plants, 3rd Edition
filename: The Ecology of Plants, 3rd Edition.pdf
description: 'Educational resource: The Ecology of Plants, 3rd Edition'
category: Science
date: '2024-03-25'
file: /library/The Ecology of Plants, 3rd Edition.pdf
- title: 'Growing Marijuana Mastery: The Advanced Guide to Grow Cannabis'
filename: Growing Marijuana Mastery The Advanced Guide to Grow Cannabis.epub
description: 'Educational resource: Growing Marijuana Mastery: The Advanced Guide
to Grow Cannabis'
category: Cultivation
date: '2024-03-25'
file: /library/Growing Marijuana Mastery The Advanced Guide to Grow Cannabis.epub
- title: 'Marijuana Botany: An Advanced Study - The Propagation and Breeding of Distinctive
Cannabis'
filename: Marijuana Botany An Advanced Study - The Propagation and Breeding of Distinctive
Cannabis.pdf
description: 'Educational resource: Marijuana Botany: An Advanced Study - The Propagation
and Breeding of Distinctive Cannabis'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Botany An Advanced Study - The Propagation and Breeding
of Distinctive Cannabis.pdf
- title: 'Cannabis Grower''s Handbook: The Complete Guide to Marijuana and Hemp Cultivation'
filename: Cannabis Grower's Handbook The Complete Guide to Marijuana and Hemp Cultivation.epub
description: 'Educational resource: Cannabis Grower''s Handbook: The Complete Guide
to Marijuana and Hemp Cultivation'
category: Cultivation
date: '2024-03-25'
file: /library/Cannabis Grower's Handbook The Complete Guide to Marijuana and Hemp
Cultivation.epub
- title: 'True Living Organics: The Ultimate Guide to Growing All-Natural Marijuana
Indoors'
filename: True Living Organics The Ultimate Guide to Growing All-Natural Marijuana
Indoors.epub
description: 'Educational resource: True Living Organics: The Ultimate Guide to
Growing All-Natural Marijuana Indoors'
category: Cultivation
date: '2024-03-25'
file: /library/True Living Organics The Ultimate Guide to Growing All-Natural Marijuana
Indoors.epub
- title: Marijuana Chemistry
filename: Marijuana Chemistry.pdf
description: 'Educational resource: Marijuana Chemistry'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Chemistry.pdf
- title: 'Plant Science: An Introduction to Botany'
filename: Plant Science An Introduction to Botany.pdf
description: 'Educational resource: Plant Science: An Introduction to Botany'
category: Science
date: '2024-03-25'
file: /library/Plant Science An Introduction to Botany.pdf
- title: 'Hydroponic Food Production: A Definitive Guidebook, 7th Edition'
filename: Hydroponic Food Production A Definitive Guidebook, 7th Edition.pdf
description: 'Educational resource: Hydroponic Food Production: A Definitive Guidebook,
7th Edition'
category: Systems
date: '2024-03-25'
file: /library/Hydroponic Food Production A Definitive Guidebook, 7th Edition.pdf
- title: 'Higher: The Lore, Legends, and Legacy of Cannabis'
filename: Higher The Lore, Legends, and Legacy of Cannabis.epub
description: 'Educational resource: Higher: The Lore, Legends, and Legacy of Cannabis'
category: Cultivation
date: '2024-03-25'
file: /library/Higher The Lore, Legends, and Legacy of Cannabis.epub
- title: Marijuana Horticulture Complete
filename: Marijuana Horticulture Complete.pdf
description: 'Educational resource: Marijuana Horticulture Complete'
category: General
date: '2024-03-25'
file: /library/Marijuana Horticulture Complete.pdf
- title: Commercial Hydroponics, 3rd Edition
filename: Commercial Hydroponics, 3rd Edition.pdf
description: 'Educational resource: Commercial Hydroponics, 3rd Edition'
category: Systems
date: '2024-03-25'
file: /library/Commercial Hydroponics, 3rd Edition.pdf
- title: 'DIY Aquaponics Marijuana Growing at Home: Learn to Grow Most Potent THC
Cannabis Ever!'
filename: DIY Aquaponics Marijuana Growing at Home Learn to Grow Most Potent THC
Cannabis Ever!.epub
description: 'Educational resource: DIY Aquaponics Marijuana Growing at Home: Learn
to Grow Most Potent THC Cannabis Ever!'
category: Cultivation
date: '2024-03-25'
file: /library/DIY Aquaponics Marijuana Growing at Home Learn to Grow Most Potent
THC Cannabis Ever!.epub

BIN
db/members.db Normal file

Binary file not shown.

74
hugo.yaml Normal file
View File

@@ -0,0 +1,74 @@
baseURL: 'https://hydrolord.thetempleofdoom.com/'
languageCode: 'en-us'
title: 'Pure Hydroponics'
theme: []
params:
description: 'Sterile hydroponics expert with 20 years of experience helping growers achieve award-winning results. Learn sterile hydroponic protocols for cannabis and all plants.'
author: 'Dr. Jones'
build:
writeStats: true
module:
mounts:
- source: content
target: content
- source: layouts
target: layouts
- source: static
target: static
ignoreLogs: ['warning-goldmark-raw-html']
# Menu Configuration
menu:
main:
- identifier: home
name: Home
url: /
weight: 10
- identifier: about
name: About Me
url: /about/
weight: 20
- identifier: services
name: Services
url: /services/
weight: 30
- identifier: membership
name: Membership
url: /membership/
weight: 32
- identifier: offerings
name: Offerings
url: /offerings/
weight: 35
- identifier: earnings
name: Earnings Calculator
url: /earnings/
weight: 36
- identifier: chat
name: Chat
url: /chat/
weight: 37
- identifier: library
name: Library
url: /library/
weight: 40
- identifier: albums
name: Albums
url: /albums/
weight: 50
- identifier: blog
name: Journal
url: /blog/
weight: 60
- identifier: testimonials
name: Testimonials
url: /testimonials/
weight: 65
- identifier: contact
name: Contact
url: /contact/
weight: 70

View File

@@ -0,0 +1,207 @@
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; connect-src *; img-src * data:; frame-ancestors 'self';">
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<meta http-equiv="Permissions-Policy" content="camera=(), microphone=(), geolocation=()">
<!-- Primary Meta Tags -->
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
<meta name="title" content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}">
<meta name="description" content="{{ if .Description }}{{ .Description }}{{ else if .Summary }}{{ .Summary | truncate 160 }}{{ else }}{{ .Site.Params.description | default "Sterile hydroponics expert helping growers achieve award-winning results. 20 years of experience." }}{{ end }}">
<meta name="keywords" content="{{ if .Params.keywords }}{{ delimit .Params.keywords ", " }}{{ else }}sterile hydroponics, hydroponic growing, cannabis cultivation, hydroponic consulting, sterile growing, hydroponic systems{{ end }}">
<meta name="author" content="{{ .Site.Params.author | default "Dr. Jones" }}">
<link rel="canonical" href="{{ .Permalink }}">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:title" content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }}{{ end }}">
<meta property="og:description" content="{{ if .Description }}{{ .Description }}{{ else if .Summary }}{{ .Summary | truncate 160 }}{{ else }}{{ .Site.Params.description | default "Sterile hydroponics expert helping growers achieve award-winning results." }}{{ end }}">
{{ if .Params.image }}
<meta property="og:image" content="{{ .Params.image | absURL }}">
{{ else }}
<meta property="og:image" content="{{ "/images/IMG_1767.jpeg" | absURL }}">
{{ end }}
<meta property="og:site_name" content="{{ .Site.Title }}">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="{{ .Permalink }}">
<meta property="twitter:title" content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }}{{ end }}">
<meta property="twitter:description" content="{{ if .Description }}{{ .Description }}{{ else if .Summary }}{{ .Summary | truncate 160 }}{{ else }}{{ .Site.Params.description | default "Sterile hydroponics expert helping growers achieve award-winning results." }}{{ end }}">
{{ if .Params.image }}
<meta property="twitter:image" content="{{ .Params.image | absURL }}">
{{ else }}
<meta property="twitter:image" content="{{ "/images/IMG_1767.jpeg" | absURL }}">
{{ end }}
<!-- Structured Data (JSON-LD) -->
{{ if .IsHome }}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "{{ .Site.Title }}",
"url": "{{ .Site.BaseURL }}",
"description": "{{ .Site.Params.description | default "Sterile hydroponics expert helping growers achieve award-winning results." }}",
"sameAs": [
"https://www.instagram.com/drj0nes/"
]
}
</script>
{{ else if .IsPage }}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "{{ if eq .Section "blog" }}BlogPosting{{ else }}Article{{ end }}",
"headline": "{{ .Title }}",
"datePublished": "{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}",
"dateModified": "{{ if .Lastmod }}{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}{{ end }}",
"author": {
"@type": "Person",
"name": "{{ .Site.Params.author | default "Dr. Jones" }}"
},
"description": "{{ if .Description }}{{ .Description }}{{ else if .Summary }}{{ .Summary | truncate 160 }}{{ end }}"
}
</script>
{{ end }}
{{ $styles := resources.Get "css/main.css" | css.PostCSS }}
<link rel="stylesheet" href="{{ $styles.RelPermalink }}">
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- Membership System -->
<script src="/js/membership.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body class="flex flex-col min-h-screen font-sans" x-data="{ mobileMenuOpen: false }">
<!-- Navigation -->
<nav class="sticky top-0 z-50 bg-gray-900/90 backdrop-blur-md border-b border-gray-800 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<a href="/" class="flex-shrink-0 flex items-center gap-2">
<div class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center text-white font-bold text-xl">P</div>
<span class="font-bold text-xl tracking-tight text-gray-100">Pure<span class="text-primary">Hydro</span></span>
</a>
</div>
<!-- Desktop Menu -->
<div class="hidden md:flex md:items-center md:space-x-8">
{{ range .Site.Menus.main }}
<a href="{{ .URL }}" class="text-gray-300 hover:text-primary px-3 py-2 rounded-md text-sm font-medium transition-colors {{ if or (eq $.RelPermalink .URL) (eq $.RelPermalink (printf "%s/" .URL)) }}text-primary font-semibold{{ end }}">
{{ .Name }}
</a>
{{ end }}
<!-- Membership Login/Logout -->
<div class="membership-login" style="display: none;">
<a href="/membership/" class="text-gray-300 hover:text-primary px-3 py-2 rounded-md text-sm font-medium transition-colors">
Login
</a>
</div>
<div class="membership-logout" style="display: none;">
<span class="text-gray-300 px-3 py-2 text-sm">
<span class="user-name"></span>
</span>
<button onclick="window.MembershipAPI.logout()" class="text-gray-300 hover:text-primary px-3 py-2 rounded-md text-sm font-medium transition-colors">
Logout
</button>
</div>
<a href="/services/" class="bg-primary text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-emerald-700 transition-colors shadow-sm hover:shadow-md">
Work With Me
</a>
</div>
<!-- Mobile Menu Button -->
<div class="flex items-center md:hidden">
<button @click="mobileMenuOpen = !mobileMenuOpen" type="button" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-300 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary">
<span class="sr-only">Open main menu</span>
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile Menu -->
<div x-show="mobileMenuOpen"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-2"
class="md:hidden bg-gray-900 border-b border-gray-800 shadow-lg absolute w-full"
style="display: none;">
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
{{ range .Site.Menus.main }}
<a href="{{ .URL }}" class="text-gray-300 hover:text-primary hover:bg-gray-800 block px-3 py-2 rounded-md text-base font-medium">
{{ .Name }}
</a>
{{ end }}
<!-- Mobile Membership Login/Logout -->
<div class="membership-login" style="display: none;">
<a href="/membership/" class="text-gray-300 hover:text-primary hover:bg-gray-800 block px-3 py-2 rounded-md text-base font-medium">
Login
</a>
</div>
<div class="membership-logout" style="display: none;">
<span class="text-gray-300 block px-3 py-2 text-base">
<span class="user-name"></span>
</span>
<button onclick="window.MembershipAPI.logout()" class="text-gray-300 hover:text-primary hover:bg-gray-800 block px-3 py-2 rounded-md text-base font-medium w-full text-left">
Logout
</button>
</div>
<a href="/services/" class="text-white bg-primary block px-3 py-2 rounded-md text-base font-medium mt-4 text-center">
Work With Me
</a>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="flex-grow">
{{ block "main" . }}{{ end }}
</main>
<!-- Footer -->
<footer class="bg-gray-900 border-t border-gray-800 mt-12">
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center flex-col md:flex-row gap-6">
<div class="text-center md:text-left">
<span class="font-bold text-xl tracking-tight text-gray-100">Pure<span class="text-primary">Hydro</span></span>
<p class="mt-2 text-sm text-gray-400">Cultivating clean, sterile, and abundant gardens.</p>
</div>
<div class="flex items-center flex-wrap justify-center gap-4">
<a href="https://www.buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener noreferrer" class="inline-flex items-center px-4 py-2 bg-[#FFDD00] hover:bg-[#FFCC00] text-gray-900 rounded-lg font-semibold transition-colors shadow-sm hover:shadow-md">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M18.5 3H6c-1.1 0-2 .9-2 2v5.71c0 3.83 2.95 7.18 6.78 7.29 3.96.12 7.22-3.06 7.22-7v-1h.5c1.38 0 2.5-1.12 2.5-2.5S19.88 3 18.5 3zM16 5v3H6V5h10zm2.5 5H18V5h.5c.28 0 .5.22.5.5s-.22.5-.5.5zM4 19h16v2H4v-2z"/>
</svg>
Buy Me a Coffee
</a>
<a href="https://www.instagram.com/drj0nes/" target="_blank" rel="noopener noreferrer" class="text-gray-400 hover:text-gray-300">
Instagram
</a>
</div>
</div>
<div class="mt-8 border-t border-gray-800 pt-8 text-center text-sm text-gray-500">
&copy; {{ now.Year }} Pure Hydroponics. All rights reserved.
</div>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,42 @@
{{ define "main" }}
<div class="bg-gray-900 py-16 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto">
<div class="text-center">
<h2 class="text-base text-primary font-semibold tracking-wide uppercase">{{ .Section }}</h2>
<p class="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-100 sm:text-4xl">{{ .Title }}</p>
<p class="mt-4 max-w-2xl text-xl text-gray-300 lg:mx-auto">{{ .Description }}</p>
</div>
<div class="mt-12 grid gap-16 lg:grid-cols-3 lg:gap-x-5 lg:gap-y-12">
{{ range .Paginator.Pages }}
<div class="flex flex-col rounded-lg shadow-lg overflow-hidden">
<div class="flex-shrink-0">
<img class="h-48 w-full object-cover" src="{{ with .Params.image }}{{ . }}{{ else }}/images/IMG_0599.jpeg{{ end }}" alt="{{ .Title }}">
</div>
<div class="flex-1 bg-gray-800 p-6 flex flex-col justify-between">
<div class="flex-1">
<p class="text-sm font-medium text-primary">
Article
</p>
<a href="{{ .RelPermalink }}" class="block mt-2">
<p class="text-xl font-semibold text-gray-100">{{ .Title }}</p>
<p class="mt-3 text-base text-gray-300">{{ .Summary | truncate 150 }}</p>
</a>
</div>
<div class="mt-6 flex items-center">
<div class="text-sm text-gray-400">
<time datetime="{{ .Date }}">{{ .Date.Format "Jan 2, 2006" }}</time>
</div>
</div>
</div>
</div>
{{ end }}
</div>
<!-- Pagination -->
<div class="mt-10 flex justify-center">
{{ template "_internal/pagination.html" . }}
</div>
</div>
</div>
{{ end }}

View File

@@ -0,0 +1,13 @@
{{ define "main" }}
<div class="bg-gray-900 py-16 px-4 sm:px-6 lg:px-8">
<article class="max-w-3xl mx-auto prose prose-emerald prose-lg prose-invert">
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-8">{{ .Title }}</h1>
<div class="text-gray-400 mb-10">
{{ if .Date }}Published on {{ .Date.Format "January 2, 2006" }}{{ end }}
</div>
<div class="mt-6">
{{ .Content }}
</div>
</article>
</div>
{{ end }}

360
layouts/admin/single.html Normal file
View File

@@ -0,0 +1,360 @@
{{ define "main" }}
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8" x-data="adminPanel()" x-init="init()">
<div class="max-w-6xl mx-auto">
<!-- Login Screen -->
<div x-show="!authenticated" class="max-w-md mx-auto">
<div class="bg-gray-800 rounded-lg p-8 border border-gray-700">
<h1 class="text-3xl font-bold text-gray-100 mb-6 text-center">Admin Login</h1>
<form @submit.prevent="adminLogin()" class="space-y-6">
<div>
<label for="admin-password" class="block text-sm font-medium text-gray-300 mb-2">Password</label>
<input type="password" id="admin-password" x-model="adminPassword" required
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div x-show="loginError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200">
<p x-text="loginError"></p>
</div>
<button type="submit"
:disabled="loginLoading"
class="w-full px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors disabled:opacity-50">
<span x-show="!loginLoading">Login</span>
<span x-show="loginLoading">Logging in...</span>
</button>
</form>
</div>
</div>
<!-- Admin Dashboard -->
<div x-show="authenticated" x-transition>
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-gray-100">Admin Dashboard</h1>
<button @click="logout()" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
Logout
</button>
</div>
<!-- Tabs -->
<div class="border-b border-gray-700 mb-6 flex">
<button @click="activeTab = 'members'; loadPendingMembers()"
:class="activeTab === 'members' ? 'border-b-2 border-primary text-primary' : 'text-gray-400 hover:text-gray-300'"
class="px-6 py-3 font-medium transition-colors">
Pending Members
</button>
<button @click="activeTab = 'testimonials'; loadTestimonials()"
:class="activeTab === 'testimonials' ? 'border-b-2 border-primary text-primary' : 'text-gray-400 hover:text-gray-300'"
class="px-6 py-3 font-medium transition-colors">
Testimonials
</button>
</div>
<!-- Pending Members Tab -->
<div x-show="activeTab === 'members'" x-transition>
<div x-show="loadingMembers" class="text-center text-gray-400 py-12">
Loading...
</div>
<div x-show="membersError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200 mb-6">
<p x-text="membersError"></p>
</div>
<div class="space-y-4">
<template x-for="member in pendingMembers" :key="member.id">
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-semibold text-gray-100" x-text="member.name"></h3>
<p class="text-gray-400" x-text="member.email"></p>
<p class="text-sm text-gray-500 mt-2" x-text="'Signed up: ' + new Date(member.created_at).toLocaleString()"></p>
</div>
<div class="flex gap-3">
<button @click="approveMember(member.id)"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
Approve
</button>
<button @click="denyMember(member.id)"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
Deny
</button>
</div>
</div>
</div>
</template>
<div x-show="!loadingMembers && !membersError && pendingMembers.length === 0" class="text-center text-gray-400 py-12">
<p>No pending memberships</p>
</div>
</div>
</div>
<!-- Testimonials Tab -->
<div x-show="activeTab === 'testimonials'" x-transition>
<div x-show="loadingTestimonials" class="text-center text-gray-400 py-12">
Loading...
</div>
<div x-show="testimonialsError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200 mb-6">
<p x-text="testimonialsError"></p>
</div>
<div class="space-y-4">
<template x-for="testimonial in allTestimonials" :key="testimonial.id">
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-lg font-semibold text-gray-100" x-text="testimonial.author_name"></h3>
<p class="text-sm text-gray-400" x-text="new Date(testimonial.created_at).toLocaleString()"></p>
<span x-show="testimonial.approved" class="inline-block mt-2 px-2 py-1 bg-green-600/20 text-green-300 text-xs rounded">
Approved
</span>
<span x-show="!testimonial.approved" class="inline-block mt-2 px-2 py-1 bg-yellow-600/20 text-yellow-300 text-xs rounded">
Pending
</span>
</div>
<div class="flex gap-3">
<button x-show="!testimonial.approved"
@click="approveTestimonial(testimonial.id)"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
Approve
</button>
<button @click="deleteTestimonial(testimonial.id)"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
Delete
</button>
</div>
</div>
<p class="text-gray-300 leading-relaxed" x-text="testimonial.content"></p>
</div>
</template>
<div x-show="!loadingTestimonials && !testimonialsError && allTestimonials.length === 0" class="text-center text-gray-400 py-12">
<p>No testimonials</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const getAdminApiBase = () => {
const host = window.location.hostname;
const isLocal = host === 'localhost' || host === '127.0.0.1' || host.startsWith('10.');
return isLocal ? 'http://' + host + ':5001' : window.location.origin.replace(/\/$/, '');
};
const ADMIN_API_BASE = getAdminApiBase() + '/api';
function adminPanel() {
return {
authenticated: false,
adminToken: null,
adminPassword: '',
loginError: '',
loginLoading: false,
activeTab: 'members',
pendingMembers: [],
loadingMembers: false,
membersError: '',
allTestimonials: [],
loadingTestimonials: false,
testimonialsError: '',
init() {
const token = localStorage.getItem('admin_token');
if (token) {
this.adminToken = token;
this.authenticated = true;
this.loadPendingMembers();
}
},
async adminLogin() {
this.loginError = '';
this.loginLoading = true;
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: this.adminPassword })
});
const data = await response.json();
this.loginLoading = false;
if (response.ok) {
this.adminToken = data.token;
localStorage.setItem('admin_token', data.token);
this.authenticated = true;
this.adminPassword = '';
this.loadPendingMembers();
} else {
this.loginError = data.error || 'Login failed';
}
} catch (error) {
this.loginLoading = false;
this.loginError = 'Login failed. Please try again.';
}
},
logout() {
localStorage.removeItem('admin_token');
this.adminToken = null;
this.authenticated = false;
},
async loadPendingMembers() {
this.loadingMembers = true;
this.membersError = '';
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/pending`, {
headers: { 'Authorization': `Bearer ${this.adminToken}` }
});
const data = await response.json();
this.loadingMembers = false;
if (response.ok) {
this.pendingMembers = data.members || [];
} else {
if (response.status === 401) {
this.logout();
this.loginError = 'Session expired. Please login again.';
} else {
this.membersError = data.error || 'Failed to load members';
}
}
} catch (error) {
this.loadingMembers = false;
this.membersError = 'Failed to load members';
}
},
async approveMember(userId) {
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.adminToken}`
},
body: JSON.stringify({ user_id: userId })
});
const data = await response.json();
if (response.ok) {
this.loadPendingMembers();
alert('Member approved successfully');
} else {
if (response.status === 401) {
this.logout();
} else {
alert(data.error || 'Failed to approve member');
}
}
} catch (error) {
alert('Failed to approve member');
}
},
async denyMember(userId) {
if (!confirm('Are you sure you want to deny this membership?')) return;
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/deny`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.adminToken}`
},
body: JSON.stringify({ user_id: userId })
});
const data = await response.json();
if (response.ok) {
this.loadPendingMembers();
alert('Member denied');
} else {
if (response.status === 401) {
this.logout();
} else {
alert(data.error || 'Failed to deny member');
}
}
} catch (error) {
alert('Failed to deny member');
}
},
async loadTestimonials() {
this.loadingTestimonials = true;
this.testimonialsError = '';
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/testimonials`, {
headers: { 'Authorization': `Bearer ${this.adminToken}` }
});
const data = await response.json();
this.loadingTestimonials = false;
if (response.ok) {
this.allTestimonials = data.testimonials || [];
} else {
if (response.status === 401) {
this.logout();
} else {
this.testimonialsError = data.error || 'Failed to load testimonials';
}
}
} catch (error) {
this.loadingTestimonials = false;
this.testimonialsError = 'Failed to load testimonials';
}
},
async approveTestimonial(testimonialId) {
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/testimonials/${testimonialId}/approve`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${this.adminToken}` }
});
const data = await response.json();
if (response.ok) {
this.loadTestimonials();
} else {
if (response.status === 401) {
this.logout();
} else {
alert(data.error || 'Failed to approve testimonial');
}
}
} catch (error) {
alert('Failed to approve testimonial');
}
},
async deleteTestimonial(testimonialId) {
if (!confirm('Are you sure you want to delete this testimonial?')) return;
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/testimonials/${testimonialId}/delete`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${this.adminToken}` }
});
const data = await response.json();
if (response.ok) {
this.loadTestimonials();
} else {
if (response.status === 401) {
this.logout();
} else {
alert(data.error || 'Failed to delete testimonial');
}
}
} catch (error) {
alert('Failed to delete testimonial');
}
}
}
}
</script>
{{ end }}

35
layouts/albums/list.html Normal file
View File

@@ -0,0 +1,35 @@
{{ define "main" }}
<div class="bg-gray-900 py-16 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto text-center">
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl">Photo Albums</h1>
<p class="mt-4 max-w-2xl text-xl text-gray-300 mx-auto">
Visual documentation of successful grows and sterile setups.
</p>
</div>
<div class="mt-12 max-w-7xl mx-auto grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{{ range .Pages }}
<a href="{{ .RelPermalink }}" class="group relative block rounded-xl overflow-hidden shadow-lg hover:shadow-2xl transition-all transform hover:-translate-y-1">
<div class="aspect-[4/3] bg-gray-800">
<!-- Cover Image (First image from params or fallback) -->
{{ $cover := "/images/IMG_0599.jpeg" }}
{{ if .Params.cover }}
{{ $cover = .Params.cover }}
{{ else if .Params.images }}
{{ range first 1 .Params.images }}
{{ $cover = .url }}
{{ end }}
{{ end }}
<img src="{{ $cover }}" alt="{{ .Title }}" class="object-cover w-full h-full group-hover:scale-105 transition-transform duration-500">
</div>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent flex items-end p-6">
<div>
<h3 class="text-xl font-bold text-white">{{ .Title }}</h3>
<p class="text-gray-200 text-sm mt-1">{{ .Params.description }}</p>
</div>
</div>
</a>
{{ end }}
</div>
</div>
{{ end }}

View File

@@ -0,0 +1,98 @@
{{ define "main" }}
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8"
x-data="{
lightboxOpen: false,
activeImage: '',
activeCaption: '',
images: [
{{ range .Params.images }}
{ url: '{{ .url }}', caption: '{{ .caption }}' },
{{ end }}
],
currentIndex: 0,
openLightbox(index) {
this.currentIndex = index;
this.activeImage = this.images[index].url;
this.activeCaption = this.images[index].caption;
this.lightboxOpen = true;
},
next() {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
this.activeImage = this.images[this.currentIndex].url;
this.activeCaption = this.images[this.currentIndex].caption;
},
prev() {
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
this.activeImage = this.images[this.currentIndex].url;
this.activeCaption = this.images[this.currentIndex].caption;
}
}"
@keydown.escape.window="lightboxOpen = false"
@keydown.arrow-right.window="next()"
@keydown.arrow-left.window="prev()">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-12">
<a href="/albums/" class="text-primary hover:text-emerald-700 font-medium mb-4 inline-block">&larr; Back to Albums</a>
<h1 class="text-4xl font-extrabold text-gray-100">{{ .Title }}</h1>
<p class="mt-4 text-gray-300">{{ .Description }}</p>
</div>
<!-- Masonry Grid -->
<div class="columns-1 md:columns-2 lg:columns-3 gap-8 space-y-8">
<template x-for="(img, index) in images" :key="index">
<div class="break-inside-avoid rounded-xl overflow-hidden shadow-md hover:shadow-xl transition-all cursor-pointer bg-gray-800"
@click="openLightbox(index)">
<template x-if="img.url.toLowerCase().endsWith('.mov') || img.url.toLowerCase().endsWith('.mp4')">
<video class="w-full h-auto object-cover" controls preload="metadata">
<source :src="img.url">
Your browser does not support the video tag.
</video>
</template>
<template x-if="!(img.url.toLowerCase().endsWith('.mov') || img.url.toLowerCase().endsWith('.mp4'))">
<img :src="img.url" :alt="img.caption" class="w-full h-auto">
</template>
<div class="p-4" x-show="img.caption">
<p class="text-sm text-gray-600" x-text="img.caption"></p>
</div>
</div>
</template>
</div>
</div>
<!-- Lightbox Modal -->
<div x-show="lightboxOpen"
style="display: none;"
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 backdrop-blur-sm"
x-transition.opacity>
<!-- Close Button -->
<button @click="lightboxOpen = false" class="absolute top-4 right-4 text-white text-4xl font-light hover:text-gray-300 focus:outline-none">&times;</button>
<!-- Nav Buttons -->
<button @click="prev()" class="absolute left-4 text-white text-4xl font-light hover:text-gray-300 focus:outline-none p-4">&lsaquo;</button>
<button @click="next()" class="absolute right-4 text-white text-4xl font-light hover:text-gray-300 focus:outline-none p-4">&rsaquo;</button>
<!-- Image/Video Display -->
<div class="max-w-5xl max-h-screen p-4 flex flex-col items-center justify-center w-full">
<template x-if="activeImage.toLowerCase().endsWith('.mov') || activeImage.toLowerCase().endsWith('.mp4')">
<video controls autoplay class="max-h-[85vh] max-w-full rounded-lg shadow-2xl">
<source :src="activeImage">
Your browser does not support the video tag.
</video>
</template>
<template x-if="!(activeImage.toLowerCase().endsWith('.mov') || activeImage.toLowerCase().endsWith('.mp4'))">
<img :src="activeImage" class="max-h-[85vh] max-w-full rounded-lg shadow-2xl object-contain">
</template>
<p x-text="activeCaption" class="text-white mt-4 text-lg font-medium tracking-wide"></p>
<p class="text-gray-400 text-sm mt-1" x-text="(currentIndex + 1) + ' / ' + images.length"></p>
</div>
</div>
</div>
{{ end }}

498
layouts/chat/single.html Normal file
View File

@@ -0,0 +1,498 @@
{{ define "main" }}
<div class="bg-gray-900 min-h-screen py-8 px-4 sm:px-6 lg:px-8" x-data="chatInterface()" x-init="init()">
<div class="max-w-7xl mx-auto">
<!-- Header -->
<div class="text-center mb-8">
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-4">
Chat with Dr. Jones AI
</h1>
<p class="text-xl text-gray-300 max-w-2xl mx-auto">
Get instant answers about sterile hydroponics, cannabis cultivation, and growing techniques. Drag library files into the chat to ask questions about specific documents.
</p>
</div>
<!-- Main Layout: Sidebar + Chat -->
<div class="flex flex-col lg:flex-row gap-6 lg:h-[700px]">
<!-- Left Sidebar: Library Files -->
<div class="w-full lg:w-80 flex-shrink-0 h-64 lg:h-auto bg-gray-800 rounded-lg shadow-xl border border-gray-700 flex flex-col">
<div class="p-4 border-b border-gray-700">
<h2 class="text-lg font-bold text-gray-100 flex items-center gap-2">
<svg class="w-5 h-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Library Files
</h2>
<p class="text-xs text-gray-400 mt-1">Drag files into chat to reference them</p>
</div>
<!-- Search/Filter -->
<div class="p-4 border-b border-gray-700">
<input
type="text"
x-model="fileSearch"
placeholder="Search files..."
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm"
>
</div>
<!-- Files List -->
<div class="flex-1 overflow-y-auto p-2">
<template x-if="loadingFiles">
<div class="text-center py-8 text-gray-400">
<svg class="animate-spin h-6 w-6 mx-auto mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading files...
</div>
</template>
<template x-if="!loadingFiles && filteredFiles.length === 0">
<div class="text-center py-8 text-gray-400 text-sm">
No files found
</div>
</template>
<div class="space-y-2" x-show="filteredFiles.length > 0">
<template x-for="file in filteredFiles" :key="file.file">
<div
draggable="true"
@dragstart="dragStart($event, file)"
@dragend="dragEnd($event)"
class="p-3 bg-gray-700 hover:bg-gray-600 rounded-lg cursor-move transition-colors border border-transparent hover:border-primary/30"
:class="{ 'border-primary': isDragging && draggedFile?.file === file.file }"
>
<div class="flex items-start gap-2">
<svg class="w-4 h-4 text-primary mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<div class="flex-1 min-w-0">
<div class="font-medium text-gray-100 text-sm leading-tight" x-text="file.title"></div>
<div class="text-xs text-gray-400 mt-1" x-text="file.category"></div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- Right Side: Chat Container -->
<div class="flex-1 bg-gray-800 rounded-lg shadow-xl border border-gray-700 flex flex-col">
<!-- Messages Area -->
<div
class="flex-1 overflow-y-auto p-6 space-y-4"
x-ref="messagesContainer"
@dragover.prevent="dragOver($event)"
@drop.prevent="dropFile($event)"
:class="{ 'bg-primary/5 border-2 border-primary border-dashed': isDragOver }"
>
<!-- Welcome Message -->
<template x-if="messages.length === 0">
<div class="text-center py-12">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/20 mb-4">
<svg class="w-8 h-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
</div>
<p class="text-gray-400 text-lg mb-2">Ask me anything about sterile hydroponics!</p>
<p class="text-gray-500 text-sm">Drag library files here to ask questions about them</p>
</div>
</template>
<!-- Messages -->
<template x-for="(message, index) in messages" :key="index">
<div class="flex" :class="message.role === 'user' ? 'justify-end' : 'justify-start'">
<div class="max-w-[80%] rounded-lg px-4 py-3"
:class="message.role === 'user'
? 'bg-primary text-white'
: 'bg-gray-700 text-gray-100'">
<div class="flex items-start gap-3">
<template x-if="message.role === 'assistant'">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center">
<svg class="w-5 h-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
</div>
</template>
<div class="flex-1">
<p class="text-sm font-medium mb-1" x-show="message.role === 'assistant'">Dr. Jones AI</p>
<div class="prose prose-sm prose-invert max-w-none" x-html="formatMessage(message.content)"></div>
<!-- Show referenced files -->
<template x-if="message.files && message.files.length > 0">
<div class="mt-2 pt-2 border-t border-gray-600/50">
<div class="text-xs text-gray-400 mb-1">Referenced files:</div>
<div class="flex flex-wrap gap-1">
<template x-for="file in message.files" :key="file.file">
<span class="text-xs px-2 py-0.5 bg-gray-600/50 rounded" x-text="file.title"></span>
</template>
</div>
</div>
</template>
</div>
<template x-if="message.role === 'user'">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary/30 flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
</template>
</div>
</div>
</div>
</template>
<!-- Loading Indicator -->
<template x-if="loading">
<div class="flex justify-start">
<div class="max-w-[80%] rounded-lg px-4 py-3 bg-gray-700 text-gray-100">
<div class="flex items-center gap-2">
<div class="flex space-x-1">
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 0ms"></div>
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 150ms"></div>
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 300ms"></div>
</div>
<span class="text-sm text-gray-400 ml-2">Dr. Jones AI is thinking...</span>
</div>
</div>
</div>
</template>
</div>
<!-- Input Area -->
<div class="border-t border-gray-700 p-4">
<!-- Selected Files Display -->
<div x-show="selectedFiles.length > 0" class="mb-3 pb-3 border-b border-gray-700">
<div class="text-xs text-gray-400 mb-2">Files to include in this message:</div>
<div class="flex flex-wrap gap-2">
<template x-for="file in selectedFiles" :key="file.file">
<div class="inline-flex items-center gap-2 px-3 py-1.5 bg-primary/20 text-primary rounded-lg text-xs">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<span x-text="file.title"></span>
<button @click="removeSelectedFile(file)" class="hover:text-primary/70">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</template>
</div>
</div>
<form @submit.prevent="sendMessage()" class="flex gap-3">
<div class="flex-1 relative">
<input
type="text"
x-model="inputMessage"
@keydown.enter.prevent="sendMessage()"
:disabled="loading || rateLimited"
placeholder="Ask about sterile hydroponics, or drag files here to ask questions about them..."
class="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
>
<template x-if="rateLimited">
<p class="absolute -top-6 left-0 text-xs text-yellow-400">
Rate limit: Please wait a moment before sending another message
</p>
</template>
</div>
<button
type="submit"
:disabled="!inputMessage.trim() || loading || rateLimited"
class="px-6 py-3 bg-primary text-white rounded-lg font-semibold hover:bg-emerald-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 shadow-lg hover:shadow-xl"
>
<template x-if="!loading">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
<span>Send</span>
</template>
<template x-if="loading">
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</template>
</button>
</form>
<p class="text-xs text-gray-500 mt-2 text-center">
Powered by Ollama • Responses are AI-generated and should be verified
</p>
</div>
</div>
</div>
<!-- Info Section -->
<div class="mt-8 bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 class="text-xl font-bold text-gray-100 mb-4">About This Chat</h2>
<div class="grid md:grid-cols-2 gap-4 text-sm text-gray-300">
<div>
<h3 class="font-semibold text-gray-100 mb-2">What I Can Help With</h3>
<ul class="space-y-1 list-disc list-inside text-gray-400">
<li>Sterile hydroponic protocols</li>
<li>Nutrient management (EC, pH)</li>
<li>System design and setup</li>
<li>Problem diagnosis</li>
<li>Questions about library documents</li>
</ul>
</div>
<div>
<h3 class="font-semibold text-gray-100 mb-2">How to Use</h3>
<ul class="space-y-1 list-disc list-inside text-gray-400">
<li>Drag files from the sidebar into chat</li>
<li>Ask questions about specific documents</li>
<li>Up to 3 files can be referenced per message</li>
<li>Files are processed securely on the server</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script>
function chatInterface() {
return {
messages: [],
inputMessage: '',
loading: false,
rateLimited: false,
lastMessageTime: 0,
messageCount: 0,
rateLimitWindow: 60000,
maxMessagesPerWindow: 10,
// File selector state
libraryFiles: [],
loadingFiles: false,
fileSearch: '',
selectedFiles: [],
// Drag and drop state
isDragging: false,
draggedFile: null,
isDragOver: false,
get filteredFiles() {
if (!this.fileSearch.trim()) {
return this.libraryFiles;
}
const search = this.fileSearch.toLowerCase();
return this.libraryFiles.filter(file =>
file.title.toLowerCase().includes(search) ||
file.category.toLowerCase().includes(search)
);
},
init() {
this.apiBase = this.getApiBase();
// Load conversation history from sessionStorage
const saved = sessionStorage.getItem('chatHistory');
if (saved) {
try {
this.messages = JSON.parse(saved);
this.$nextTick(() => this.scrollToBottom());
} catch (e) {
console.error('Failed to load chat history', e);
}
}
// Load library files
this.loadLibraryFiles();
},
getApiBase() {
const host = window.location.hostname;
const isLocal = host === 'localhost' || host === '127.0.0.1' || host.startsWith('10.');
return isLocal ? 'http://localhost:5000' : window.location.origin.replace(/\/$/, '');
},
async loadLibraryFiles() {
this.loadingFiles = true;
try {
const apiUrl = `${this.apiBase}/api/library/files`;
const response = await fetch(apiUrl);
if (response.ok) {
const data = await response.json();
this.libraryFiles = data.files || [];
}
} catch (error) {
console.error('Failed to load library files:', error);
} finally {
this.loadingFiles = false;
}
},
dragStart(event, file) {
this.isDragging = true;
this.draggedFile = file;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', JSON.stringify(file));
},
dragEnd(event) {
this.isDragging = false;
this.draggedFile = null;
},
dragOver(event) {
this.isDragOver = true;
event.dataTransfer.dropEffect = 'move';
},
dropFile(event) {
this.isDragOver = false;
try {
const fileData = event.dataTransfer.getData('text/plain');
if (fileData) {
const file = JSON.parse(fileData);
this.addSelectedFile(file);
}
} catch (e) {
console.error('Error handling drop:', e);
}
},
addSelectedFile(file) {
// Limit to 3 files max
if (this.selectedFiles.length >= 3) {
alert('You can reference up to 3 files at a time.');
return;
}
// Don't add duplicates
if (!this.selectedFiles.find(f => f.file === file.file)) {
this.selectedFiles.push(file);
}
},
removeSelectedFile(file) {
this.selectedFiles = this.selectedFiles.filter(f => f.file !== file.file);
},
formatMessage(text) {
const safe = this.escapeHTML(text || '');
return safe
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/\n/g, '<br>')
.replace(/`(.*?)`/g, '<code class="bg-gray-900 px-1 py-0.5 rounded text-primary">$1</code>');
},
escapeHTML(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
},
scrollToBottom() {
this.$nextTick(() => {
const container = this.$refs.messagesContainer;
if (container) {
container.scrollTop = container.scrollHeight;
}
});
},
checkRateLimit() {
const now = Date.now();
if (now - this.lastMessageTime > this.rateLimitWindow) {
this.messageCount = 0;
this.lastMessageTime = now;
}
if (this.messageCount >= this.maxMessagesPerWindow) {
this.rateLimited = true;
setTimeout(() => {
this.rateLimited = false;
this.messageCount = 0;
}, this.rateLimitWindow - (now - this.lastMessageTime));
return false;
}
this.messageCount++;
this.lastMessageTime = now;
return true;
},
async sendMessage() {
const message = this.inputMessage.trim();
if (!message || this.loading || this.rateLimited) return;
if (!this.checkRateLimit()) return;
const safeForDisplay = this.escapeHTML(message);
// Store selected files for this message
const filesForMessage = [...this.selectedFiles];
// Add user message
this.messages.push({
role: 'user',
content: safeForDisplay,
files: filesForMessage.length > 0 ? filesForMessage : undefined
});
this.inputMessage = '';
this.selectedFiles = []; // Clear selected files after sending
this.loading = true;
this.saveHistory();
this.scrollToBottom();
try {
const sanitizedMessage = this.sanitizeInput(message);
const apiUrl = `${this.apiBase}/api/chat`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: sanitizedMessage,
files: filesForMessage.map(f => ({ file: f.file, title: f.title })), // Only send file references
history: this.messages.slice(-5)
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
this.messages.push({
role: 'assistant',
content: data.response || 'Sorry, I encountered an error. Please try again.'
});
} catch (error) {
console.error('Chat error:', error);
this.messages.push({
role: 'assistant',
content: 'Sorry, I\'m having trouble connecting right now. Please try again in a moment.'
});
} finally {
this.loading = false;
this.saveHistory();
this.scrollToBottom();
}
},
sanitizeInput(input) {
return input
.replace(/[<>]/g, '')
.substring(0, 1000)
.trim();
},
saveHistory() {
try {
sessionStorage.setItem('chatHistory', JSON.stringify(this.messages));
} catch (e) {
console.error('Failed to save chat history', e);
}
}
}
}
</script>
{{ end }}

View File

@@ -0,0 +1,40 @@
{{ define "main" }}
<div class="bg-gray-900 py-16 px-4 sm:px-6 lg:px-8" x-data="contactPage()" x-init="initContactPage()">
<article class="max-w-3xl mx-auto">
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-8">{{ .Title }}</h1>
<div class="mt-6 prose prose-emerald prose-lg prose-invert max-w-none">
{{ .Content }}
</div>
</article>
</div>
<script>
function contactPage() {
return {
initContactPage() {
// Check auth status and show/hide form
setTimeout(() => {
const authState = window.MembershipAPI.getAuthState();
const paywallMessage = document.querySelector('.paywall-message');
const paywallContent = document.querySelector('.paywall-content');
const membershipOnly = document.querySelectorAll('.membership-only');
const guestOnly = document.querySelectorAll('.guest-only');
if (authState.authenticated) {
if (paywallMessage) paywallMessage.style.display = 'none';
if (paywallContent) paywallContent.style.display = 'block';
membershipOnly.forEach(el => el.style.display = 'block');
guestOnly.forEach(el => el.style.display = 'none');
} else {
if (paywallMessage) paywallMessage.style.display = 'block';
if (paywallContent) paywallContent.style.display = 'none';
membershipOnly.forEach(el => el.style.display = 'none');
guestOnly.forEach(el => el.style.display = 'block');
}
}, 100);
}
}
}
</script>
{{ end }}

View File

@@ -0,0 +1,308 @@
{{ define "main" }}
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8" x-data="earningsCalculator()">
<div class="max-w-6xl mx-auto">
<!-- Header -->
<div class="text-center mb-12">
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-4">
Discover Your Potential Earnings
</h1>
<p class="text-xl text-gray-300 max-w-3xl mx-auto">
See what you could earn with my sterile hydroponics methodology. Based on proven results: <span class="text-primary font-semibold">1.5lb of cannabis from a 2x2.5x7 space</span>.
</p>
</div>
<!-- Calculator Form -->
<div class="bg-gray-800 rounded-lg shadow-xl p-8 mb-8 border border-gray-700">
<h2 class="text-2xl font-bold text-gray-100 mb-6">Grow Room Specifications</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- CFM Airflow -->
<div>
<label for="cfm" class="block text-sm font-medium text-gray-300 mb-2">
Room CFM (Cubic Feet per Minute) Airflow
</label>
<input
type="number"
id="cfm"
x-model="cfm"
@input="calculate()"
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="Enter CFM"
min="0"
>
</div>
<!-- Number of Plants -->
<div>
<label for="plants" class="block text-sm font-medium text-gray-300 mb-2">
Number of Plants
</label>
<input
type="number"
id="plants"
x-model="plants"
@input="calculate()"
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="Enter number"
min="1"
>
</div>
<!-- Number of Lights -->
<div>
<label for="lights" class="block text-sm font-medium text-gray-300 mb-2">
Number of Lights
</label>
<input
type="number"
id="lights"
x-model="lights"
@input="calculate()"
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="Enter number"
min="1"
>
</div>
<!-- Total Wattage -->
<div>
<label for="wattage" class="block text-sm font-medium text-gray-300 mb-2">
Total Wattage
</label>
<input
type="number"
id="wattage"
x-model="wattage"
@input="calculate()"
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="Enter watts"
min="0"
>
</div>
<!-- Width -->
<div>
<label for="width" class="block text-sm font-medium text-gray-300 mb-2">
Width (feet)
</label>
<input
type="number"
id="width"
x-model="width"
@input="calculate()"
step="0.1"
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="Enter width"
min="0"
>
</div>
<!-- Height -->
<div>
<label for="height" class="block text-sm font-medium text-gray-300 mb-2">
Height (feet)
</label>
<input
type="number"
id="height"
x-model="height"
@input="calculate()"
step="0.1"
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="Enter height"
min="0"
>
</div>
<!-- Length -->
<div>
<label for="length" class="block text-sm font-medium text-gray-300 mb-2">
Length (feet)
</label>
<input
type="number"
id="length"
x-model="length"
@input="calculate()"
step="0.1"
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="Enter length"
min="0"
>
</div>
<!-- Method (Fixed to Hydro) -->
<div>
<label for="method" class="block text-sm font-medium text-gray-300 mb-2">
Growing Method
</label>
<input
type="text"
id="method"
value="Hydroponics"
disabled
class="w-full px-4 py-2 bg-gray-600 border border-gray-500 rounded-md text-gray-400 cursor-not-allowed"
>
<p class="mt-1 text-xs text-gray-400">Always set to Hydroponics</p>
</div>
</div>
</div>
<!-- Results Section -->
<div x-show="hasResults"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform translate-y-4"
x-transition:enter-end="opacity-100 transform translate-y-0"
class="space-y-6">
<!-- Breakdown Table -->
<div class="bg-gray-800 rounded-lg shadow-xl p-8 border border-gray-700">
<h2 class="text-2xl font-bold text-gray-100 mb-6">Your Potential Results</h2>
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead>
<tr class="border-b border-gray-700">
<th class="pb-4 text-sm font-semibold text-gray-300 uppercase">Metric</th>
<th class="pb-4 text-sm font-semibold text-gray-300 uppercase text-right">Value</th>
</tr>
</thead>
<tbody class="text-gray-200">
<tr class="border-b border-gray-700">
<td class="py-4 font-medium">Reservoir Size Needed</td>
<td class="py-4 text-right">
<span class="text-primary font-bold text-lg" x-text="formatNumber(results.waterGallons)"></span>
<span class="text-gray-400 ml-2">gallons</span>
</td>
</tr>
<tr class="border-b border-gray-700">
<td class="py-4 font-medium">Total Yield (Grams)</td>
<td class="py-4 text-right">
<span class="text-primary font-bold text-lg" x-text="formatNumber(results.totalGrams)"></span>
<span class="text-gray-400 ml-2">grams</span>
</td>
</tr>
<tr class="border-b border-gray-700">
<td class="py-4 font-medium">Price per Gram</td>
<td class="py-4 text-right">
<span class="text-primary font-bold text-lg">$10.00</span>
</td>
</tr>
<tr class="border-b border-gray-700">
<td class="py-4 font-medium">Grow Cycle Duration</td>
<td class="py-4 text-right">
<span class="text-primary font-bold text-lg">2.5</span>
<span class="text-gray-400 ml-2">months</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Grand Totals - Exciting Display -->
<div class="bg-gradient-to-br from-primary/20 to-emerald-600/20 rounded-lg shadow-xl p-8 border-2 border-primary">
<div class="text-center">
<h2 class="text-3xl font-extrabold text-gray-100 mb-4">
🎉 Potential Earnings 🎉
</h2>
<div class="mb-6">
<p class="text-lg text-gray-300 mb-2">Total Revenue</p>
<p class="text-6xl font-bold text-primary mb-2" x-text="'$' + formatNumber(results.totalRevenue)"></p>
<p class="text-xl text-gray-400" x-text="'From ' + formatNumber(results.totalGrams) + ' grams in just 2.5 months!'"></p>
</div>
<div class="mt-8 p-6 bg-gray-800/50 rounded-lg border border-primary/30">
<p class="text-2xl font-bold text-gray-100 mb-2">
<span class="text-primary">WOAH!</span> You could make that much!
</p>
<p class="text-gray-300 mt-4">
This is what's possible with my sterile hydroponics methodology.
<span class="text-primary font-semibold">Ready to make it happen?</span>
</p>
<a href="/services/" class="inline-block mt-6 px-8 py-3 bg-primary text-white rounded-lg font-semibold hover:bg-emerald-700 transition-colors shadow-lg hover:shadow-xl">
Work With Me
</a>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div x-show="!hasResults"
class="text-center py-12 bg-gray-800 rounded-lg border border-gray-700">
<p class="text-gray-400 text-lg">Enter your grow room specifications above to see your potential earnings!</p>
</div>
</div>
</div>
<script>
function earningsCalculator() {
return {
// Input values
cfm: '',
plants: '',
lights: '',
wattage: '',
width: '',
height: '',
length: '',
// Results
results: {
waterGallons: 0,
totalGrams: 0,
totalRevenue: 0
},
// Calculation constants
// Base ratio: 2x2.5x7 space = 1.5lb
// 2 * 2.5 * 7 = 35 cubic feet = 1.5lb = 680.39 grams
BASE_CUBIC_FEET: 35, // 2 * 2.5 * 7
BASE_GRAMS: 680.39, // 1.5lb in grams
GALLONS_PER_PLANT: 0.7,
PRICE_PER_GRAM: 10,
get hasResults() {
return this.width > 0 && this.height > 0 && this.length > 0 && this.plants > 0;
},
calculate() {
if (!this.hasResults) {
return;
}
// Calculate cubic feet of grow space
const cubicFeet = parseFloat(this.width) * parseFloat(this.height) * parseFloat(this.length);
// Calculate yield based on ratio
// If user's space is 35 cubic feet, they get 680.39 grams
// Scale proportionally
const scaleFactor = cubicFeet / this.BASE_CUBIC_FEET;
const totalGrams = this.BASE_GRAMS * scaleFactor;
// Calculate water needed: 0.7 gallons per plant
const waterGallons = parseFloat(this.plants) * this.GALLONS_PER_PLANT;
// Calculate total revenue
const totalRevenue = totalGrams * this.PRICE_PER_GRAM;
// Update results
this.results = {
waterGallons: Math.round(waterGallons * 10) / 10, // Round to 1 decimal
totalGrams: Math.round(totalGrams * 10) / 10, // Round to 1 decimal
totalRevenue: Math.round(totalRevenue * 100) / 100 // Round to 2 decimals
};
},
formatNumber(num) {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 2
}).format(num);
}
}
}
</script>
{{ end }}

121
layouts/index.html Normal file
View File

@@ -0,0 +1,121 @@
{{ define "main" }}
<!-- Hero Section -->
<div class="relative bg-background overflow-hidden">
<div class="max-w-7xl mx-auto">
<div class="relative z-10 pb-8 bg-background sm:pb-16 md:pb-20 lg:max-w-2xl lg:w-full lg:pb-28 xl:pb-32">
<main class="mt-10 mx-auto max-w-7xl px-4 sm:mt-12 sm:px-6 md:mt-16 lg:mt-20 lg:px-8 xl:mt-28">
<div class="sm:text-center lg:text-left">
<h1 class="text-4xl tracking-tight font-extrabold text-gray-100 sm:text-5xl md:text-6xl">
<span class="block xl:inline">Unlock the Power of</span>
<span class="block text-primary">Sterile Hydroponics</span>
</h1>
<p class="mt-3 text-base text-gray-300 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0">
Disease-free roots, accelerated growth, and maximum yields. Learn how a clinical approach to gardening transforms your harvest.
</p>
<div class="mt-5 sm:mt-8 sm:flex sm:justify-center lg:justify-start">
<div class="rounded-md shadow">
<a href="/services/" class="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-white bg-primary hover:bg-emerald-700 md:py-4 md:text-lg md:px-10">
Start Mentorship
</a>
</div>
<div class="mt-3 sm:mt-0 sm:ml-3">
<a href="/library/" class="w-full flex items-center justify-center px-8 py-3 border border-primary text-base font-medium rounded-md text-primary bg-gray-800 hover:bg-gray-700 md:py-4 md:text-lg md:px-10">
Free Guide
</a>
</div>
</div>
</div>
</main>
</div>
</div>
<div class="lg:absolute lg:inset-y-0 lg:right-0 lg:w-1/2">
<img class="h-56 w-full object-cover sm:h-72 md:h-96 lg:w-full lg:h-full" src="/images/IMG_1767.jpeg" alt="Sterile Hydroponics Setup">
</div>
</div>
<!-- Benefits Section -->
<div class="py-12 bg-gray-900">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="lg:text-center">
<h2 class="text-base text-primary font-semibold tracking-wide uppercase">Why Sterile?</h2>
<p class="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-100 sm:text-4xl">
A Better Way to Grow
</p>
<p class="mt-4 max-w-2xl text-xl text-gray-300 lg:mx-auto">
Eliminate pathogens before they start. Our sterile methodology ensures your plants spend energy on growth, not fighting disease.
</p>
</div>
<div class="mt-10">
<dl class="space-y-10 md:space-y-0 md:grid md:grid-cols-3 md:gap-x-8 md:gap-y-10">
<div class="relative">
<dt>
<div class="absolute flex items-center justify-center h-12 w-12 rounded-md bg-primary text-white">
<!-- Icon -->
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p class="ml-16 text-lg leading-6 font-medium text-gray-100">Zero Pathogens</p>
</dt>
<dd class="mt-2 ml-16 text-base text-gray-300">
By maintaining a sterile reservoir, you prevent Pythium (root rot) and other fungal issues completely.
</dd>
</div>
<div class="relative">
<dt>
<div class="absolute flex items-center justify-center h-12 w-12 rounded-md bg-primary text-white">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<p class="ml-16 text-lg leading-6 font-medium text-gray-100">Maximum Uptake</p>
</dt>
<dd class="mt-2 ml-16 text-base text-gray-300">
Clean water allows for optimal nutrient absorption without biofilm interference.
</dd>
</div>
<div class="relative">
<dt>
<div class="absolute flex items-center justify-center h-12 w-12 rounded-md bg-primary text-white">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
</div>
<p class="ml-16 text-lg leading-6 font-medium text-gray-100">Clinical Precision</p>
</dt>
<dd class="mt-2 ml-16 text-base text-gray-300">
Data-driven gardening using precise measurements and sterile tools for predictable success.
</dd>
</div>
</dl>
</div>
</div>
</div>
<!-- Recent Posts (Placeholder) -->
<div class="py-12 bg-gray-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-3xl font-extrabold text-gray-100">Recent from the Journal</h2>
<div class="mt-6 grid gap-16 lg:grid-cols-3 lg:gap-x-5 lg:gap-y-12">
{{ range first 3 (where .Site.RegularPages "Section" "blog") }}
<div class="flex flex-col rounded-lg shadow-lg overflow-hidden">
<div class="flex-shrink-0">
<img class="h-48 w-full object-cover" src="{{ with .Params.image }}{{ . }}{{ else }}/images/IMG_0599.jpeg{{ end }}" alt="{{ .Title }}">
</div>
<div class="flex-1 bg-gray-900 p-6 flex flex-col justify-between">
<div class="flex-1">
<a href="{{ .RelPermalink }}" class="block mt-2">
<p class="text-xl font-semibold text-gray-100">{{ .Title }}</p>
<p class="mt-3 text-base text-gray-300">{{ .Summary | truncate 100 }}</p>
</a>
</div>
</div>
</div>
{{ end }}
</div>
</div>
</div>
{{ end }}

86
layouts/library/list.html Normal file
View File

@@ -0,0 +1,86 @@
{{ define "main" }}
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8" x-data="{ category: 'All' }" x-init="window.MembershipAPI.checkDownloadLimit()">
<div class="max-w-7xl mx-auto">
<div class="text-center">
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl">Resource Library</h1>
<p class="mt-4 max-w-2xl text-xl text-gray-300 mx-auto">
Download guides, checklists, and protocols to upgrade your hydroponic game.
<br>
<span class="text-sm text-gray-400 italic">For educational purposes only.</span>
</p>
</div>
<!-- Paywall Message -->
<div class="paywall-message mt-8 bg-gray-800 rounded-lg p-8 border border-gray-700 text-center" style="display: none;">
<h2 class="text-2xl font-bold text-gray-100 mb-4">Download Limit Reached</h2>
<p class="text-gray-300 mb-6">
You've used your free download. Become a member for unlimited access to all resources.
</p>
<a href="/membership/" class="inline-flex items-center px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors">
Become a Member
</a>
</div>
<!-- Download Limit Info -->
<div class="guest-only mt-6 bg-blue-900/30 border border-blue-700 rounded-lg p-4 text-center" style="display: block;">
<p class="text-blue-200">
<span class="font-semibold">Free users:</span> 1 download allowed.
<a href="/membership/" class="underline hover:text-blue-100">Sign up for membership</a> for unlimited downloads.
</p>
</div>
<div class="membership-only mt-6 bg-green-900/30 border border-green-700 rounded-lg p-4 text-center" style="display: none;">
<p class="text-green-200">
<span class="font-semibold">Member:</span> Unlimited downloads available.
</p>
</div>
<!-- Filters -->
<div class="mt-10 flex justify-center flex-wrap gap-2">
{{ $categories := slice "All" }}
{{ range .Site.Data.library }}
{{ $categories = $categories | append .category }}
{{ end }}
{{ range ($categories | uniq) }}
<button
@click="category = '{{ . }}'"
:class="{ 'bg-primary text-white': category === '{{ . }}', 'bg-gray-800 text-gray-300 hover:bg-gray-700': category !== '{{ . }}' }"
class="px-4 py-2 rounded-full text-sm font-medium shadow-sm transition-colors border border-gray-700">
{{ . }}
</button>
{{ end }}
</div>
<!-- Grid -->
<div class="paywall-content mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{{ range .Site.Data.library }}
<div x-show="category === 'All' || category === '{{ .category }}'"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100"
class="bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-shadow overflow-hidden flex flex-col border border-gray-700">
<div class="p-6 flex-1">
<div class="flex justify-between items-start">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary/20 text-primary">
{{ .category }}
</span>
<span class="text-xs text-gray-500">{{ .date }}</span>
</div>
<div class="mt-4 flex items-center gap-3">
<svg class="h-8 w-8 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd" />
</svg>
<h3 class="text-lg font-medium text-gray-100">{{ .title }}</h3>
</div>
<p class="mt-3 text-sm text-gray-300">{{ .description }}</p>
</div>
<div class="bg-gray-900 px-6 py-4 border-t border-gray-700">
<a href="{{ .file }}" download data-file-path="{{ .file }}" class="download-link flex items-center justify-center w-full px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-primary bg-white hover:bg-gray-50 border-primary transition-colors">
Download {{ .filename }}
</a>
</div>
</div>
{{ end }}
</div>
</div>
</div>
{{ end }}

View File

@@ -0,0 +1,221 @@
{{ define "main" }}
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8" x-data="membershipPage()">
<div class="max-w-4xl mx-auto">
<div class="text-center mb-12">
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-4">Become a Member</h1>
<p class="text-xl text-gray-300 max-w-2xl mx-auto">
Unlock exclusive access to premium content, unlimited downloads, and direct communication with Dr. Jones.
</p>
</div>
<!-- Benefits Section -->
<div class="bg-gray-800 rounded-lg p-8 mb-8 border border-gray-700">
<h2 class="text-2xl font-bold text-gray-100 mb-6">Member Benefits</h2>
<div class="grid md:grid-cols-2 gap-6">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-8 h-8 bg-primary rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-100 mb-2">Unlimited Downloads</h3>
<p class="text-gray-300">Access and download all resources from the library without limits.</p>
</div>
</div>
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-8 h-8 bg-primary rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-100 mb-2">Direct Email Access</h3>
<p class="text-gray-300">Get my direct email address and submit contact forms for personalized help.</p>
</div>
</div>
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-8 h-8 bg-primary rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-100 mb-2">Member Testimonials</h3>
<p class="text-gray-300">Read and share success stories from other growers using the sterile method.</p>
</div>
</div>
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-8 h-8 bg-primary rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-100 mb-2">Priority Support</h3>
<p class="text-gray-300">Members get priority responses and access to exclusive content updates.</p>
</div>
</div>
</div>
</div>
<!-- Buy Me a Coffee Section -->
<div class="bg-gradient-to-r from-[#FFDD00] to-[#FFCC00] rounded-lg p-8 mb-8 text-center">
<h2 class="text-3xl font-bold text-gray-900 mb-4">Support the Method</h2>
<p class="text-lg text-gray-800 mb-6">
Membership requires a $5 minimum donation via Buy Me a Coffee. This helps support continued development of sterile hydroponic protocols and resources.
</p>
<a href="https://www.buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center px-8 py-4 bg-gray-900 hover:bg-gray-800 text-white rounded-lg font-bold text-lg transition-colors shadow-lg">
<svg class="w-6 h-6 mr-3" fill="currentColor" viewBox="0 0 24 24">
<path d="M18.5 3H6c-1.1 0-2 .9-2 2v5.71c0 3.83 2.95 7.18 6.78 7.29 3.96.12 7.22-3.06 7.22-7v-1h.5c1.38 0 2.5-1.12 2.5-2.5S19.88 3 18.5 3zM16 5v3H6V5h10zm2.5 5H18V5h.5c.28 0 .5.22.5.5s-.22.5-.5.5zM4 19h16v2H4v-2z"/>
</svg>
Donate $5 Minimum
</a>
<p class="text-sm text-gray-700 mt-4 italic">After completing your donation, return here to sign up for membership.</p>
</div>
<!-- Login/Register Tabs -->
<div class="bg-gray-800 rounded-lg border border-gray-700 overflow-hidden">
<div class="border-b border-gray-700 flex">
<button @click="activeTab = 'login'"
:class="activeTab === 'login' ? 'bg-primary text-white' : 'text-gray-300 hover:text-gray-100'"
class="flex-1 px-6 py-4 font-medium transition-colors">
Login
</button>
<button @click="activeTab = 'register'"
:class="activeTab === 'register' ? 'bg-primary text-white' : 'text-gray-300 hover:text-gray-100'"
class="flex-1 px-6 py-4 font-medium transition-colors">
Sign Up
</button>
</div>
<div class="p-8">
<!-- Login Form -->
<div x-show="activeTab === 'login'" x-transition>
<form @submit.prevent="handleLogin()" class="space-y-6">
<div>
<label for="login-email" class="block text-sm font-medium text-gray-300 mb-2">Email</label>
<input type="email" id="login-email" x-model="loginEmail" required
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div>
<label for="login-password" class="block text-sm font-medium text-gray-300 mb-2">Password</label>
<input type="password" id="login-password" x-model="loginPassword" required
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div x-show="loginError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200">
<p x-text="loginError"></p>
</div>
<button type="submit"
:disabled="loginLoading"
class="w-full px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors disabled:opacity-50">
<span x-show="!loginLoading">Login</span>
<span x-show="loginLoading">Logging in...</span>
</button>
</form>
</div>
<!-- Register Form -->
<div x-show="activeTab === 'register'" x-transition>
<form @submit.prevent="handleRegister()" class="space-y-6">
<div>
<label for="register-name" class="block text-sm font-medium text-gray-300 mb-2">Full Name</label>
<input type="text" id="register-name" x-model="registerName" required
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div>
<label for="register-email" class="block text-sm font-medium text-gray-300 mb-2">Email</label>
<input type="email" id="register-email" x-model="registerEmail" required
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div>
<label for="register-password" class="block text-sm font-medium text-gray-300 mb-2">Password</label>
<input type="password" id="register-password" x-model="registerPassword" required minlength="8"
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
<p class="mt-1 text-sm text-gray-400">Must be at least 8 characters</p>
</div>
<div x-show="registerError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200">
<p x-text="registerError"></p>
</div>
<div x-show="registerSuccess" class="bg-green-900/50 border border-green-700 rounded-lg p-4 text-green-200">
<p x-text="registerSuccess"></p>
</div>
<button type="submit"
:disabled="registerLoading"
class="w-full px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors disabled:opacity-50">
<span x-show="!registerLoading">Sign Up</span>
<span x-show="registerLoading">Creating account...</span>
</button>
</form>
</div>
</div>
</div>
<!-- Status Message -->
<div x-show="statusMessage" class="mt-6 p-4 rounded-lg"
:class="statusMessageType === 'success' ? 'bg-green-900/50 border border-green-700 text-green-200' : 'bg-blue-900/50 border border-blue-700 text-blue-200'">
<p x-text="statusMessage"></p>
</div>
</div>
</div>
<script>
function membershipPage() {
return {
activeTab: 'login',
loginEmail: '',
loginPassword: '',
loginError: '',
loginLoading: false,
registerName: '',
registerEmail: '',
registerPassword: '',
registerError: '',
registerSuccess: '',
registerLoading: false,
statusMessage: '',
statusMessageType: 'info',
async handleLogin() {
this.loginError = '';
this.loginLoading = true;
const result = await window.MembershipAPI.login(this.loginEmail, this.loginPassword);
this.loginLoading = false;
if (result.success) {
this.statusMessage = 'Login successful! Redirecting...';
this.statusMessageType = 'success';
setTimeout(() => {
window.location.href = '/';
}, 1500);
} else {
this.loginError = result.error || 'Login failed';
}
},
async handleRegister() {
this.registerError = '';
this.registerSuccess = '';
this.registerLoading = true;
const result = await window.MembershipAPI.register(this.registerEmail, this.registerPassword, this.registerName);
this.registerLoading = false;
if (result.success) {
this.registerSuccess = result.message || 'Registration successful! Your membership is pending approval. You will receive an email when approved.';
this.registerEmail = '';
this.registerPassword = '';
this.registerName = '';
} else {
this.registerError = result.error || 'Registration failed';
}
}
}
}
</script>
{{ end }}

View File

@@ -0,0 +1,34 @@
{{ define "main" }}
<div class="bg-gray-900 py-16 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto text-center">
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl">Work With Me</h1>
<p class="mt-4 max-w-2xl text-xl text-gray-300 mx-auto">
Professional guidance to fast-track your success.
</p>
</div>
<div class="mt-16 grid gap-8 lg:grid-cols-3">
{{ range .Pages }}
<div class="relative bg-gray-800 border border-gray-700 rounded-2xl shadow-sm flex flex-col p-8 hover:shadow-lg transition-shadow">
<div class="flex-1">
<h3 class="text-xl font-semibold text-gray-100">{{ .Title }}</h3>
<p class="mt-4 flex items-baseline text-gray-100">
<span class="text-4xl font-extrabold tracking-tight">{{ .Params.price }}</span>
{{ if .Params.price_period }}<span class="ml-1 text-xl font-semibold text-gray-400">{{ .Params.price_period }}</span>{{ end }}
</p>
<div class="mt-6 text-gray-300 space-y-4 prose prose-emerald">
{{ .Content }}
</div>
</div>
<div class="mt-8">
<a href="{{ .Params.foxy_link | default "#" }}"
class="w-full bg-primary border border-transparent rounded-md py-3 px-8 flex items-center justify-center text-base font-medium text-white hover:bg-emerald-700 md:text-lg transition-colors">
{{ .Params.cta | default "Book Session" }}
</a>
<p class="mt-4 text-xs text-gray-400 text-center">Secured by Stripe</p>
</div>
</div>
{{ end }}
</div>
</div>
{{ end }}

View File

@@ -0,0 +1,159 @@
{{ define "main" }}
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8" x-data="testimonialsPage()" x-init="initTestimonials()">
<div class="max-w-4xl mx-auto">
<div class="text-center mb-12">
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-4">Member Testimonials</h1>
<p class="text-xl text-gray-300 max-w-2xl mx-auto">
Real stories from growers who have transformed their gardens using the sterile hydroponic method.
</p>
</div>
<!-- Paywall Message -->
<div class="paywall-message bg-gray-800 rounded-lg p-8 border border-gray-700 text-center mb-8" style="display: none;">
<h2 class="text-2xl font-bold text-gray-100 mb-4">Members Only Content</h2>
<p class="text-gray-300 mb-6">
Testimonials are available exclusively to members. Sign up to read success stories and share your own experience.
</p>
<a href="/membership/" class="inline-flex items-center px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors">
Become a Member
</a>
</div>
<!-- Testimonials Content -->
<div class="paywall-content">
<!-- Submit Testimonial Form (Members Only) -->
<div class="membership-only bg-gray-800 rounded-lg p-8 mb-8 border border-gray-700" style="display: none;">
<h2 class="text-2xl font-bold text-gray-100 mb-6">Share Your Story</h2>
<form @submit.prevent="submitTestimonial()" class="space-y-6">
<div>
<label for="author-name" class="block text-sm font-medium text-gray-300 mb-2">Your Name</label>
<input type="text" id="author-name" x-model="authorName" required
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div>
<label for="testimonial-content" class="block text-sm font-medium text-gray-300 mb-2">Your Testimonial</label>
<textarea id="testimonial-content" x-model="testimonialContent" required rows="6"
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent"></textarea>
</div>
<div x-show="submitError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200">
<p x-text="submitError"></p>
</div>
<div x-show="submitSuccess" class="bg-green-900/50 border border-green-700 rounded-lg p-4 text-green-200">
<p x-text="submitSuccess"></p>
</div>
<button type="submit"
:disabled="submitLoading"
class="px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors disabled:opacity-50">
<span x-show="!submitLoading">Submit Testimonial</span>
<span x-show="submitLoading">Submitting...</span>
</button>
</form>
</div>
<!-- Testimonials List -->
<div class="space-y-6">
<div x-show="loading" class="text-center text-gray-400 py-12">
Loading testimonials...
</div>
<div x-show="error" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200">
<p x-text="error"></p>
</div>
<template x-for="testimonial in testimonials" :key="testimonial.id">
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div class="flex items-start justify-between mb-4">
<div>
<h3 class="text-lg font-semibold text-gray-100" x-text="testimonial.author_name"></h3>
<p class="text-sm text-gray-400" x-text="new Date(testimonial.created_at).toLocaleDateString()"></p>
</div>
</div>
<p class="text-gray-300 leading-relaxed" x-text="testimonial.content"></p>
</div>
</template>
<div x-show="!loading && !error && testimonials.length === 0" class="text-center text-gray-400 py-12">
<p>No testimonials yet. Be the first to share your success story!</p>
</div>
</div>
</div>
</div>
</div>
<script>
function testimonialsPage() {
return {
testimonials: [],
loading: true,
error: '',
authorName: '',
testimonialContent: '',
submitError: '',
submitSuccess: '',
submitLoading: false,
initTestimonials() {
// Check auth and show/hide paywall
const authState = window.MembershipAPI.getAuthState();
const paywallMessage = document.querySelector('.paywall-message');
const paywallContent = document.querySelector('.paywall-content');
const membershipOnly = document.querySelectorAll('.membership-only');
if (!authState.authenticated) {
if (paywallMessage) paywallMessage.style.display = 'block';
if (paywallContent) paywallContent.style.display = 'none';
membershipOnly.forEach(el => el.style.display = 'none');
this.loading = false;
} else {
if (paywallMessage) paywallMessage.style.display = 'none';
if (paywallContent) paywallContent.style.display = 'block';
membershipOnly.forEach(el => el.style.display = 'block');
this.loadTestimonials();
}
},
async loadTestimonials() {
this.loading = true;
this.error = '';
const authState = window.MembershipAPI.getAuthState();
if (!authState.authenticated) {
this.loading = false;
return;
}
const result = await window.MembershipAPI.loadTestimonials();
this.loading = false;
if (result.success) {
this.testimonials = result.testimonials || [];
} else {
this.error = result.error || 'Failed to load testimonials';
}
},
async submitTestimonial() {
if (!this.authorName || !this.testimonialContent) {
this.submitError = 'Please fill in all fields';
return;
}
this.submitError = '';
this.submitSuccess = '';
this.submitLoading = true;
const result = await window.MembershipAPI.submitTestimonial(this.testimonialContent, this.authorName);
this.submitLoading = false;
if (result.success) {
this.submitSuccess = result.message || 'Testimonial submitted successfully!';
this.authorName = '';
this.testimonialContent = '';
// Reload testimonials after a delay
setTimeout(() => this.loadTestimonials(), 2000);
} else {
this.submitError = result.error || 'Failed to submit testimonial';
}
}
}
}
</script>
{{ end }}

628
library.yaml.backup Normal file
View File

@@ -0,0 +1,628 @@
- title: HYDROPONICS GARDEN - The Complete Diy Guide For Beginners To Learn How To
Build A System To Grow Plants
filename: HYDROPONICS GARDEN - The Complete Diy Guide For Beginners To Learn How
To Build A System To Grow Plants.pdf
description: 'Educational resource: HYDROPONICS GARDEN - The Complete Diy Guide
For Beginners To Learn How To Build A System To Grow Plants'
category: Systems
date: '2024-03-25'
file: /library/HYDROPONICS GARDEN - The Complete Diy Guide For Beginners To Learn
How To Build A System To Grow Plants.pdf
- title: Tammi Sweet - Beginner's Guide to Growing Cannabis and Making Your Own Healing
Remedies Learn about the Plant's Medicinal Properties Grow O
filename: Tammi Sweet - Beginner's Guide to Growing Cannabis and Making Your Own
Healing Remedies_ Learn about the Plant's Medicinal Properties_ Grow O - libgen.li.epub
description: 'Educational resource: Tammi Sweet - Beginner''s Guide to Growing Cannabis
and Making Your Own Healing Remedies Learn about the Plant''s Medicinal Properties Grow
O'
category: Cultivation
date: '2024-03-25'
file: /library/Tammi Sweet - Beginner's Guide to Growing Cannabis and Making Your
Own Healing Remedies_ Learn about the Plant's Medicinal Properties_ Grow O - libgen.li.epub
- title: '[Using the Sea of Green Process] Paul Wright - Totally organic hydroponics
(2004, Tin Alley Productions)'
filename: '[Using the Sea of Green Process] Paul Wright - Totally organic hydroponics
(2004, Tin Alley Productions) - libgen.li.pdf'
description: 'Educational resource: [Using the Sea of Green Process] Paul Wright
- Totally organic hydroponics (2004, Tin Alley Productions)'
category: Systems
date: '2024-03-25'
file: /library/[Using the Sea of Green Process] Paul Wright - Totally organic hydroponics
(2004, Tin Alley Productions) - libgen.li.pdf
- title: Cannabis Collection - PBS - Clearing the Smoke- The Science of Cannabis
filename: Cannabis Collection - PBS - Clearing the Smoke- The Science of Cannabis.mp4
description: 'Educational resource: Cannabis Collection - PBS - Clearing the Smoke-
The Science of Cannabis'
category: Cultivation
date: '2024-03-25'
file: /library/Cannabis Collection - PBS - Clearing the Smoke- The Science of Cannabis.mp4
- title: Amateur Gardening Easy Guide To - Gardening For Beginners 2024
filename: Amateur Gardening Easy Guide To - Gardening For Beginners 2024.pdf
description: 'Educational resource: Amateur Gardening Easy Guide To - Gardening
For Beginners 2024'
category: General
date: '2024-03-25'
file: /library/Amateur Gardening Easy Guide To - Gardening For Beginners 2024.pdf
- title: Encyclopedia of Garden Plants for Every Location - Featuring More Than 3,000
Plants By DK
filename: Encyclopedia of Garden Plants for Every Location - Featuring More Than
3,000 Plants By DK.pdf
description: 'Educational resource: Encyclopedia of Garden Plants for Every Location
- Featuring More Than 3,000 Plants By DK'
category: General
date: '2024-03-25'
file: /library/Encyclopedia of Garden Plants for Every Location - Featuring More
Than 3,000 Plants By DK.pdf
- title: Cannabis - A Complete Guide (2016)
filename: Cannabis - A Complete Guide (2016).pdf
description: 'Educational resource: Cannabis - A Complete Guide (2016)'
category: Cultivation
date: '2024-03-25'
file: /library/Cannabis - A Complete Guide (2016).pdf
- title: GUIDE TO HYDROPONICS By James Shelby PH.D
filename: GUIDE TO HYDROPONICS By James Shelby PH.D.epub
description: 'Educational resource: GUIDE TO HYDROPONICS By James Shelby PH.D'
category: Systems
date: '2024-03-25'
file: /library/GUIDE TO HYDROPONICS By James Shelby PH.D.epub
- title: Ed Rosenthal David Downs - Marijuana Harvest How to Maximize Quality and
Yield in Your Cannabis Garden (2017, Quick American Archives)
filename: Ed Rosenthal_ David Downs - Marijuana Harvest_ How to Maximize Quality
and Yield in Your Cannabis Garden (2017, Quick American Archives) - libgen.li.epub
description: 'Educational resource: Ed Rosenthal David Downs - Marijuana Harvest How
to Maximize Quality and Yield in Your Cannabis Garden (2017, Quick American Archives)'
category: Cultivation
date: '2024-03-25'
file: /library/Ed Rosenthal_ David Downs - Marijuana Harvest_ How to Maximize Quality
and Yield in Your Cannabis Garden (2017, Quick American Archives) - libgen.li.epub
- title: Ecology of Plants, The - Jessica Gurevitch, Samuel M. Scheiner, Gordon A.
Fox
filename: Ecology of Plants, The - Jessica Gurevitch, Samuel M. Scheiner, Gordon
A. Fox.pdf
description: 'Educational resource: Ecology of Plants, The - Jessica Gurevitch,
Samuel M. Scheiner, Gordon A. Fox'
category: Science
date: '2024-03-25'
file: /library/pdfs/Jessica Gurevitch, Samuel M. Scheiner, Gordon A. Fox/Ecology
of Plants, The/Ecology of Plants, The - Jessica Gurevitch, Samuel M. Scheiner,
Gordon A. Fox.pdf
- title: Hydroponic Food Production - A Definitive Guid Commercial Hydroponic Grower
- Howard M. Resh
filename: Hydroponic Food Production - A Definitive Guid Commercial Hydroponic Grower
- Howard M. Resh.pdf
description: 'Educational resource: Hydroponic Food Production - A Definitive Guid
Commercial Hydroponic Grower - Howard M. Resh'
category: Systems
date: '2024-03-25'
file: /library/pdfs/Resh,rd M_/Hydroponic Food Production - A Definitive Grdener
and the Commercial Hydroponic Grower/Hydroponic Food Production - A Definitive
Guid Commercial Hydroponic Grower - Howard M. Resh.pdf
- title: GROW GREAT WEED Personal & Medical Marijuana Indoor Outdoor Grower Big Bud
Bible - Dale Denton
filename: GROW GREAT WEED_ Personal & Medical Marijuana Indoor_Outdoor Grower Big
Bud Bible - Dale Denton.pdf
description: 'Educational resource: GROW GREAT WEED Personal & Medical Marijuana
Indoor Outdoor Grower Big Bud Bible - Dale Denton'
category: Cultivation
date: '2024-03-25'
file: /library/pdfs/Denton, Dale/GROW GREAT WEED_ Personal & Medical Marijuana Indoor_Outdoor
Grower Big Bud Bible/GROW GREAT WEED_ Personal & Medical Marijuana Indoor_Outdoor
Grower Big Bud Bible - Dale Denton.pdf
- title: Scanned Image - drjones
filename: Scanned Image - drjones.pdf
description: 'Educational resource: Scanned Image - drjones'
category: General
date: '2024-03-25'
file: /library/pdfs/drjones/Scanned Image/Scanned Image - drjones.pdf
- title: 100 Best Cannabis Strains - Michael Blood
filename: 100 Best Cannabis Strains - Michael Blood.pdf
description: 'Educational resource: 100 Best Cannabis Strains - Michael Blood'
category: Cultivation
date: '2024-03-25'
file: /library/pdfs/Blood, Michael/100 Best Cannabis Strains/100 Best Cannabis Strains
- Michael Blood.pdf
- title: downmagaz.net - downmagaz.net
filename: downmagaz.net - downmagaz.net.pdf
description: 'Educational resource: downmagaz.net - downmagaz.net'
category: General
date: '2024-03-25'
file: /library/pdfs/downmagaz.net/downmagaz.net/downmagaz.net - downmagaz.net.pdf
- title: Cannabis Grow Bible, The - Greg Green
filename: Cannabis Grow Bible, The - Greg Green.pdf
description: 'Educational resource: Cannabis Grow Bible, The - Greg Green'
category: Cultivation
date: '2024-03-25'
file: /library/pdfs/Green, Greg/Cannabis Grow Bible, The/Cannabis Grow Bible, The
- Greg Green.pdf
- title: Diagnosing Hemp and Cannabis Crop Diseases by Shouhua Wang - Unknown
filename: Diagnosing Hemp and Cannabis Crop Diseases by Shouhua Wang - Unknown.pdf
description: 'Educational resource: Diagnosing Hemp and Cannabis Crop Diseases by
Shouhua Wang - Unknown'
category: Cultivation
date: '2024-03-25'
file: /library/pdfs/Unknown/Diagnosing Hemp and Cannabis Crop Diseases by Shouhua
Wang/Diagnosing Hemp and Cannabis Crop Diseases by Shouhua Wang - Unknown.pdf
- title: GUIDE TO HYDROPONICS - James Shelby PH.D
filename: GUIDE TO HYDROPONICS - James Shelby PH.D.pdf
description: 'Educational resource: GUIDE TO HYDROPONICS - James Shelby PH.D'
category: Systems
date: '2024-03-25'
file: /library/pdfs/Shelby PH.D, James/GUIDE TO HYDROPONICS/GUIDE TO HYDROPONICS
- James Shelby PH.D.pdf
- title: HYDROPONICS GARDEN - Water, Tyler
filename: HYDROPONICS GARDEN - Water, Tyler.pdf
description: 'Educational resource: HYDROPONICS GARDEN - Water, Tyler'
category: Systems
date: '2024-03-25'
file: /library/pdfs/Water, Tyler/HYDROPONICS GARDEN/HYDROPONICS GARDEN - Water,
Tyler.pdf
- title: How to Grow Marijuana 3 Books in 1 - The Complality Weed Indoors and Outdoors
- Whistler, Tom
filename: How to Grow Marijuana_ 3 Books in 1 - The Complality Weed Indoors and
Outdoors - Whistler, Tom.pdf
description: 'Educational resource: How to Grow Marijuana 3 Books in 1 - The Complality
Weed Indoors and Outdoors - Whistler, Tom'
category: Cultivation
date: '2024-03-25'
file: /library/pdfs/Whis Tom/How to Grow Marijuana_ 3 Books in 1 - The Cowing Top-Quality
Weed Indoors and Outdoors/How to Grow Marijuana_ 3 Books in 1 - The Complality
Weed Indoors and Outdoors - Whistler, Tom.pdf
- title: Hydroponics Hydroponics Gardening Guide - frg, horticulture, Cannabis) -
Thatcher, Thomas
filename: Hydroponics_ Hydroponics Gardening Guide - frg, horticulture, Cannabis)
- Thatcher, Thomas.pdf
description: 'Educational resource: Hydroponics Hydroponics Gardening Guide - frg,
horticulture, Cannabis) - Thatcher, Thomas'
category: Cultivation
date: '2024-03-25'
file: /library/pdfs/Thatchomas/Hydroponics_ Hydroponics Gardening Guide - fresteading,
Gardening, horticulture, Cannabis)/Hydroponics_ Hydroponics Gardening Guide -
frg, horticulture, Cannabis) - Thatcher, Thomas.pdf
- title: Jesse Ventura's Marijuana Manifesto - Jesse Ventura
filename: Jesse Ventura's Marijuana Manifesto - Jesse Ventura.pdf
description: 'Educational resource: Jesse Ventura''s Marijuana Manifesto - Jesse
Ventura'
category: Cultivation
date: '2024-03-25'
file: /library/pdfs/Ventura, Jesse/Jesse Ventura's Marijuana Manifesto/Jesse Ventura's
Marijuana Manifesto - Jesse Ventura.pdf
- title: True Living Organics - The Rev
filename: True Living Organics - The Rev.pdf
description: 'Educational resource: True Living Organics - The Rev'
category: General
date: '2024-03-25'
file: /library/pdfs/Rev, The/True Living Organics/True Living Organics - The Rev.pdf
- title: DIY Home Hydroponics For Beginners The Essentiao Turn Your Backyard Into
A Farm - Sally R. Ball (1)
filename: DIY Home Hydroponics For Beginners_ The Essentiao Turn Your Backyard Into
A Farm - Sally R. Ball (1).pdf
description: 'Educational resource: DIY Home Hydroponics For Beginners The Essentiao
Turn Your Backyard Into A Farm - Sally R. Ball (1)'
category: Systems
date: '2024-03-25'
file: /library/pdfs/Ball, lly R_/DIY Home Hydroponics For Beginners_ The Guide
To Turn Your Backyard Into A Farm/DIY Home Hydroponics For Beginners_ The Essentiao
Turn Your Backyard Into A Farm - Sally R. Ball (1).pdf
- title: DIY Home Hydroponics For Beginners The Essentiao Turn Your Backyard Into
A Farm - Sally R. Ball
filename: DIY Home Hydroponics For Beginners_ The Essentiao Turn Your Backyard Into
A Farm - Sally R. Ball.pdf
description: 'Educational resource: DIY Home Hydroponics For Beginners The Essentiao
Turn Your Backyard Into A Farm - Sally R. Ball'
category: Systems
date: '2024-03-25'
file: /library/pdfs/Ball, lly R_/DIY Home Hydroponics For Beginners_ The Guide
To Turn Your Backyard Into A Farm/DIY Home Hydroponics For Beginners_ The Essentiao
Turn Your Backyard Into A Farm - Sally R. Ball.pdf
- title: What a Plant Knows - Daniel Chamovitz
filename: What a Plant Knows - Daniel Chamovitz.pdf
description: 'Educational resource: What a Plant Knows - Daniel Chamovitz'
category: General
date: '2024-03-25'
file: /library/pdfs/Chamovitz, Daniel/What a Plant Knows/What a Plant Knows - Daniel
Chamovitz.pdf
- title: Emmanuel S. Onaivi - The Biology of Marijuana From Gene to Behavior (2002,
CRC Press)
filename: Emmanuel S. Onaivi - The Biology of Marijuana_ From Gene to Behavior (2002,
CRC Press) - libgen.li.pdf
description: 'Educational resource: Emmanuel S. Onaivi - The Biology of Marijuana From
Gene to Behavior (2002, CRC Press)'
category: Cultivation
date: '2024-03-25'
file: /library/Emmanuel S. Onaivi - The Biology of Marijuana_ From Gene to Behavior
(2002, CRC Press) - libgen.li.pdf
- title: Greg Green - The Cannabis Breeder's Bible The Definitive Guide to Marijuana
Genetics, Cannabis Botany and Creating Strains for the Seed Mark (2005, Green
Candy Press)
filename: Greg Green - The Cannabis Breeder's Bible_ The Definitive Guide to Marijuana
Genetics, Cannabis Botany and Creating Strains for the Seed Mark (2005, Green
Candy Press) - libgen.li.epub
description: 'Educational resource: Greg Green - The Cannabis Breeder''s Bible The
Definitive Guide to Marijuana Genetics, Cannabis Botany and Creating Strains for
the Seed Mark (2005, Green Candy Press)'
category: Cultivation
date: '2024-03-25'
file: /library/Greg Green - The Cannabis Breeder's Bible_ The Definitive Guide to
Marijuana Genetics, Cannabis Botany and Creating Strains for the Seed Mark (2005,
Green Candy Press) - libgen.li.epub
- title: '[20th Century Alchemist] D. Gold (David Hoye) - Cannabis Alchemy The Art
of Modern Hashmaking (1993, Ronin Publishing)'
filename: '[20th Century Alchemist] D. Gold (David Hoye) - Cannabis Alchemy_ The
Art of Modern Hashmaking (1993, Ronin Publishing) - libgen.li.pdf'
description: 'Educational resource: [20th Century Alchemist] D. Gold (David Hoye)
- Cannabis Alchemy The Art of Modern Hashmaking (1993, Ronin Publishing)'
category: Cultivation
date: '2024-03-25'
file: /library/[20th Century Alchemist] D. Gold (David Hoye) - Cannabis Alchemy_
The Art of Modern Hashmaking (1993, Ronin Publishing) - libgen.li.pdf
- title: Marijuana Botany - An Advanced Study - The Propagation and Breeding of Distinctive
Cannabis
filename: Marijuana Botany - An Advanced Study - The Propagation and Breeding of
Distinctive Cannabis.pdf
description: 'Educational resource: Marijuana Botany - An Advanced Study - The Propagation
and Breeding of Distinctive Cannabis'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Botany - An Advanced Study - The Propagation and Breeding
of Distinctive Cannabis.pdf
- title: 'DIY Home Hydroponics For Beginners '
filename: DIY Home Hydroponics For Beginners .epub
description: 'Educational resource: DIY Home Hydroponics For Beginners '
category: Systems
date: '2024-03-25'
file: /library/DIY Home Hydroponics For Beginners .epub
- title: Diagnosing Hemp and Cannabis Crop Diseases by Shouhua Wang
filename: Diagnosing Hemp and Cannabis Crop Diseases by Shouhua Wang.pdf
description: 'Educational resource: Diagnosing Hemp and Cannabis Crop Diseases by
Shouhua Wang'
category: Cultivation
date: '2024-03-25'
file: /library/Diagnosing Hemp and Cannabis Crop Diseases by Shouhua Wang.pdf
- title: '[Handbooks in psychopharmacology] Pertwee, Roger G - Handbook of cannabis
(2016, Oxford University Press)'
filename: '[Handbooks in psychopharmacology] Pertwee, Roger G - Handbook of cannabis
(2016, Oxford University Press) - libgen.li.pdf'
description: 'Educational resource: [Handbooks in psychopharmacology] Pertwee, Roger
G - Handbook of cannabis (2016, Oxford University Press)'
category: Cultivation
date: '2024-03-25'
file: /library/[Handbooks in psychopharmacology] Pertwee, Roger G - Handbook of
cannabis (2016, Oxford University Press) - libgen.li.pdf
- title: T. Asao - Hydroponics -A Standard Methodology for Plant Biological Researches-Intech
(2012)
filename: T. Asao - Hydroponics -A Standard Methodology for Plant Biological Researches-Intech
(2012).pdf
description: 'Educational resource: T. Asao - Hydroponics -A Standard Methodology
for Plant Biological Researches-Intech (2012)'
category: Systems
date: '2024-03-25'
file: /library/T. Asao - Hydroponics -A Standard Methodology for Plant Biological
Researches-Intech (2012).pdf
- title: Oner, S. T - Cannabis Sativa. Volume 3, The essential guide to the world's
finest marijuana strains (2014, Green Candy Press,Perseus Books, LLC)
filename: Oner, S. T - Cannabis Sativa. Volume 3, The essential guide to the world's
finest marijuana strains (2014, Green Candy Press,Perseus Books, LLC) - libgen.li.epub
description: 'Educational resource: Oner, S. T - Cannabis Sativa. Volume 3, The
essential guide to the world''s finest marijuana strains (2014, Green Candy Press,Perseus
Books, LLC)'
category: Cultivation
date: '2024-03-25'
file: /library/Oner, S. T - Cannabis Sativa. Volume 3, The essential guide to the
world's finest marijuana strains (2014, Green Candy Press,Perseus Books, LLC)
- libgen.li.epub
- title: Growing Marijuana Mastery The Easiest and Complete Handbook to Growing Weed
Indoor and Outdoor – Your Weed Growers Guide With Secrets for Big Bug Harvest
filename: Growing Marijuana Mastery_ The Easiest and Complete Handbook to Growing
Weed Indoor and Outdoor – Your Weed Growers Guide With Secrets for Big Bug Harvest.pdf
description: 'Educational resource: Growing Marijuana Mastery The Easiest and Complete
Handbook to Growing Weed Indoor and Outdoor – Your Weed Growers Guide With Secrets
for Big Bug Harvest'
category: Cultivation
date: '2024-03-25'
file: /library/Growing Marijuana Mastery_ The Easiest and Complete Handbook to Growing
Weed Indoor and Outdoor – Your Weed Growers Guide With Secrets for Big Bug Harvest.pdf
- title: George Van Patten - Gardening Indoors with Soil & Hydroponics , Fifth Edition
(2007, Van Patten Publishing)
filename: George Van Patten - Gardening Indoors with Soil & Hydroponics , Fifth
Edition (2007, Van Patten Publishing) - libgen.li.pdf
description: 'Educational resource: George Van Patten - Gardening Indoors with Soil
& Hydroponics , Fifth Edition (2007, Van Patten Publishing)'
category: Systems
date: '2024-03-25'
file: /library/George Van Patten - Gardening Indoors with Soil & Hydroponics , Fifth
Edition (2007, Van Patten Publishing) - libgen.li.pdf
- title: Mahmoud A. ElSohly (editor) - Cannabis Chemistry and Biology Fundamentals
(2023, De Gruyter) [10.1515 9783110718362]
filename: Mahmoud A. ElSohly (editor) - Cannabis Chemistry and Biology_ Fundamentals
(2023, De Gruyter) [10.1515_9783110718362] - libgen.li.pdf
description: 'Educational resource: Mahmoud A. ElSohly (editor) - Cannabis Chemistry
and Biology Fundamentals (2023, De Gruyter) [10.1515 9783110718362]'
category: Cultivation
date: '2024-03-25'
file: /library/Mahmoud A. ElSohly (editor) - Cannabis Chemistry and Biology_ Fundamentals
(2023, De Gruyter) [10.1515_9783110718362] - libgen.li.pdf
- title: Basil John Mason, Lyn Morgan, Peta Jinnath Abdul, Marie Beerman - Nutrients
for Hydroponics Tissue Culture (2018)
filename: Basil John Mason, Lyn Morgan, Peta Jinnath Abdul, Marie Beerman - Nutrients
for Hydroponics Tissue Culture (2018) - libgen.li.pdf
description: 'Educational resource: Basil John Mason, Lyn Morgan, Peta Jinnath Abdul,
Marie Beerman - Nutrients for Hydroponics Tissue Culture (2018)'
category: Systems
date: '2024-03-25'
file: /library/Basil John Mason, Lyn Morgan, Peta Jinnath Abdul, Marie Beerman -
Nutrients for Hydroponics Tissue Culture (2018) - libgen.li.pdf
- title: Cervantes, Jorge - Marijuana Grow Basics The Easy Guide for Cannabis Aficionados
(2009, Van Patten Publishing)
filename: Cervantes, Jorge - Marijuana Grow Basics_ The Easy Guide for Cannabis
Aficionados (2009, Van Patten Publishing) - libgen.li.pdf
description: 'Educational resource: Cervantes, Jorge - Marijuana Grow Basics The
Easy Guide for Cannabis Aficionados (2009, Van Patten Publishing)'
category: Cultivation
date: '2024-03-25'
file: /library/Cervantes, Jorge - Marijuana Grow Basics_ The Easy Guide for Cannabis
Aficionados (2009, Van Patten Publishing) - libgen.li.pdf
- title: DIY Home Hydroponics For Beginners - The Essential Guide To Turn Your Backyard
Into A Farm
filename: DIY Home Hydroponics For Beginners - The Essential Guide To Turn Your
Backyard Into A Farm.epub
description: 'Educational resource: DIY Home Hydroponics For Beginners - The Essential
Guide To Turn Your Backyard Into A Farm'
category: Systems
date: '2024-03-25'
file: /library/DIY Home Hydroponics For Beginners - The Essential Guide To Turn
Your Backyard Into A Farm.epub
- title: 100 Best Cannabis Strains - A Pocket Guide for Medicinal and Recreational
Use
filename: 100 Best Cannabis Strains - A Pocket Guide for Medicinal and Recreational
Use.epub
description: 'Educational resource: 100 Best Cannabis Strains - A Pocket Guide for
Medicinal and Recreational Use'
category: Cultivation
date: '2024-03-25'
file: /library/100 Best Cannabis Strains - A Pocket Guide for Medicinal and Recreational
Use.epub
- title: '#4 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible'
filename: '#4 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
description: 'Educational resource: #4 Marijuana Horticulture - The Indoor-Outdoor
Medical Growers Bible'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Horticulture - The Indoor & Outdoor Medical Growers Bible/#4
Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
- title: '#5 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible'
filename: '#5 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
description: 'Educational resource: #5 Marijuana Horticulture - The Indoor-Outdoor
Medical Growers Bible'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Horticulture - The Indoor & Outdoor Medical Growers Bible/#5
Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
- title: '#6 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible'
filename: '#6 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
description: 'Educational resource: #6 Marijuana Horticulture - The Indoor-Outdoor
Medical Growers Bible'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Horticulture - The Indoor & Outdoor Medical Growers Bible/#6
Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
- title: '#1 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible'
filename: '#1 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
description: 'Educational resource: #1 Marijuana Horticulture - The Indoor-Outdoor
Medical Growers Bible'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Horticulture - The Indoor & Outdoor Medical Growers Bible/#1
Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
- title: '#2 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible'
filename: '#2 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
description: 'Educational resource: #2 Marijuana Horticulture - The Indoor-Outdoor
Medical Growers Bible'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Horticulture - The Indoor & Outdoor Medical Growers Bible/#2
Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
- title: '#3 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible'
filename: '#3 Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf'
description: 'Educational resource: #3 Marijuana Horticulture - The Indoor-Outdoor
Medical Growers Bible'
category: Cultivation
date: '2024-03-25'
file: /library/Marijuana Horticulture - The Indoor & Outdoor Medical Growers Bible/#3
Marijuana Horticulture - The Indoor-Outdoor Medical Growers Bible.pdf
- title: Jesse Ventura's Marijuana Manifesto
filename: Jesse Ventura's Marijuana Manifesto.epub
description: 'Educational resource: Jesse Ventura''s Marijuana Manifesto'
category: Cultivation
date: '2024-03-25'
file: /library/Jesse Ventura's Marijuana Manifesto.epub
- title: GROW GREAT WEED Personal & Med - Dale Denton
filename: GROW GREAT WEED_ Personal & Med - Dale Denton.pdf
description: 'Educational resource: GROW GREAT WEED Personal & Med - Dale Denton'
category: Cultivation
date: '2024-03-25'
file: /library/GROW GREAT WEED_ Personal & Med - Dale Denton.pdf
- title: Hydroponics Gardening Guide
filename: Hydroponics Gardening Guide.epub
description: 'Educational resource: Hydroponics Gardening Guide'
category: Systems
date: '2024-03-25'
file: /library/Hydroponics Gardening Guide.epub
- title: How to Grow Marijuana - 3 Boo...
filename: How to Grow Marijuana - 3 Boo....pdf
description: 'Educational resource: How to Grow Marijuana - 3 Boo...'
category: Cultivation
date: '2024-03-25'
file: /library/How to Grow Marijuana - 3 Boo....pdf
- title: Ed Rosenthal Robert Flannery Angela Bacca Tommy Chong - Cannabis Grower's
Handbook The Complete Guide to Marijuana and Hemp Cultivation (2021, Quick American
Archives)
filename: Ed Rosenthal_ Robert Flannery_ Angela Bacca_ Tommy Chong - Cannabis Grower's
Handbook_ The Complete Guide to Marijuana and Hemp Cultivation (2021, Quick American
Archives) - libgen.li_ocr.pdf
description: 'Educational resource: Ed Rosenthal Robert Flannery Angela Bacca Tommy
Chong - Cannabis Grower''s Handbook The Complete Guide to Marijuana and Hemp
Cultivation (2021, Quick American Archives)'
category: Cultivation
date: '2024-03-25'
file: /library/Ed Rosenthal_ Robert Flannery_ Angela Bacca_ Tommy Chong - Cannabis
Grower's Handbook_ The Complete Guide to Marijuana and Hemp Cultivation (2021,
Quick American Archives) - libgen.li_ocr.pdf
- title: '[Big of Buds] Ed Rosenthal - The Big Book of Buds More Marijuana Varieties
from the World''s Greatest Seed Breeders (2007, Quick Trading Company Ed Rosenthal)'
filename: '[Big of Buds] Ed Rosenthal - The Big Book of Buds_ More Marijuana Varieties
from the World''s Greatest Seed Breeders (2007, Quick Trading Company_Ed Rosenthal)
- libgen.li.epub'
description: 'Educational resource: [Big of Buds] Ed Rosenthal - The Big Book of
Buds More Marijuana Varieties from the World''s Greatest Seed Breeders (2007,
Quick Trading Company Ed Rosenthal)'
category: Cultivation
date: '2024-03-25'
file: /library/[Big of Buds] Ed Rosenthal - The Big Book of Buds_ More Marijuana
Varieties from the World's Greatest Seed Breeders (2007, Quick Trading Company_Ed
Rosenthal) - libgen.li.epub
- title: Storm - Marijuana Hydroponics High-Tech Water Culture (2016, Ronin Publishing)
filename: Storm - Marijuana Hydroponics_ High-Tech Water Culture (2016, Ronin Publishing)
- libgen.li.epub
description: 'Educational resource: Storm - Marijuana Hydroponics High-Tech Water
Culture (2016, Ronin Publishing)'
category: Cultivation
date: '2024-03-25'
file: /library/Storm - Marijuana Hydroponics_ High-Tech Water Culture (2016, Ronin
Publishing) - libgen.li.epub
- title: Ed Rosenthal - Closet Cultivator Indoor Marijuana Cultivation Made Easy
(1999, Quick American Archives)
filename: Ed Rosenthal - Closet Cultivator_ Indoor Marijuana Cultivation Made Easy
(1999, Quick American Archives) - libgen.li.pdf
description: 'Educational resource: Ed Rosenthal - Closet Cultivator Indoor Marijuana
Cultivation Made Easy (1999, Quick American Archives)'
category: Cultivation
date: '2024-03-25'
file: /library/Ed Rosenthal - Closet Cultivator_ Indoor Marijuana Cultivation Made
Easy (1999, Quick American Archives) - libgen.li.pdf
- title: What a Plant Knows - A Field Guide to the Senses By Daniel Chamovitz
filename: What a Plant Knows - A Field Guide to the Senses By Daniel Chamovitz.epub
description: 'Educational resource: What a Plant Knows - A Field Guide to the Senses
By Daniel Chamovitz'
category: General
date: '2024-03-25'
file: /library/What a Plant Knows - A Field Guide to the Senses By Daniel Chamovitz.epub
- title: Youbin Zheng - Handbook of Cannabis Production in Controlled Environments
(2022, CRC Press)
filename: Youbin Zheng - Handbook of Cannabis Production in Controlled Environments
(2022, CRC Press) - libgen.li.pdf
description: 'Educational resource: Youbin Zheng - Handbook of Cannabis Production
in Controlled Environments (2022, CRC Press)'
category: Cultivation
date: '2024-03-25'
file: /library/Youbin Zheng - Handbook of Cannabis Production in Controlled Environments
(2022, CRC Press) - libgen.li.pdf
- title: Gurevitch J. The Ecology of Plants 3ed 2020
filename: Gurevitch J. The Ecology of Plants 3ed 2020.pdf
description: 'Educational resource: Gurevitch J. The Ecology of Plants 3ed 2020'
category: Science
date: '2024-03-25'
file: /library/Gurevitch J. The Ecology of Plants 3ed 2020.pdf
- title: alfonso Garcia - Growing Marijuana Mastery The Advanced Guide to grow Cannabis Tips
and Techniques for Advanced Cultivation of Medical Marijua (2020, Alfonso Garcia)
filename: alfonso Garcia - Growing Marijuana Mastery_ The Advanced Guide to grow
Cannabis_ Tips and Techniques for Advanced Cultivation of Medical Marijua (2020,
Alfonso Garcia) - libgen.li.epub
description: 'Educational resource: alfonso Garcia - Growing Marijuana Mastery The
Advanced Guide to grow Cannabis Tips and Techniques for Advanced Cultivation
of Medical Marijua (2020, Alfonso Garcia)'
category: Cultivation
date: '2024-03-25'
file: /library/alfonso Garcia - Growing Marijuana Mastery_ The Advanced Guide to
grow Cannabis_ Tips and Techniques for Advanced Cultivation of Medical Marijua
(2020, Alfonso Garcia) - libgen.li.epub
- title: Robert Connell Clarke - Marijuana Botany - An Advanced Study The Propagation
and Breeding of Distinctive Cannabis (0)
filename: Robert Connell Clarke - Marijuana Botany - An Advanced Study_ The Propagation
and Breeding of Distinctive Cannabis (0) - libgen.li.pdf
description: 'Educational resource: Robert Connell Clarke - Marijuana Botany - An
Advanced Study The Propagation and Breeding of Distinctive Cannabis (0)'
category: Cultivation
date: '2024-03-25'
file: /library/Robert Connell Clarke - Marijuana Botany - An Advanced Study_ The
Propagation and Breeding of Distinctive Cannabis (0) - libgen.li.pdf
- title: Ed Rosenthal Robert Flannery Angela Bacca Tommy Chong - Cannabis Grower's
Handbook The Complete Guide to Marijuana and Hemp Cultivation (2021, Quick American
Archives)
filename: Ed Rosenthal_ Robert Flannery_ Angela Bacca_ Tommy Chong - Cannabis Grower's
Handbook_ The Complete Guide to Marijuana and Hemp Cultivation (2021, Quick American
Archives) - libgen.li.epub
description: 'Educational resource: Ed Rosenthal Robert Flannery Angela Bacca Tommy
Chong - Cannabis Grower''s Handbook The Complete Guide to Marijuana and Hemp
Cultivation (2021, Quick American Archives)'
category: Cultivation
date: '2024-03-25'
file: /library/Ed Rosenthal_ Robert Flannery_ Angela Bacca_ Tommy Chong - Cannabis
Grower's Handbook_ The Complete Guide to Marijuana and Hemp Cultivation (2021,
Quick American Archives) - libgen.li.epub
- title: True Living Organics - The Ultimate Guide to Growing All-Natural Marijuana
Indoors (2012)
filename: True Living Organics - The Ultimate Guide to Growing All-Natural Marijuana
Indoors (2012).epub
description: 'Educational resource: True Living Organics - The Ultimate Guide to
Growing All-Natural Marijuana Indoors (2012)'
category: Cultivation
date: '2024-03-25'
file: /library/True Living Organics - The Ultimate Guide to Growing All-Natural
Marijuana Indoors (2012).epub
- title: Michael Starks - Marijuana Chemistry-Ronin Publishing (1993)
filename: Michael Starks - Marijuana Chemistry-Ronin Publishing (1993).pdf
description: 'Educational resource: Michael Starks - Marijuana Chemistry-Ronin Publishing
(1993)'
category: Cultivation
date: '2024-03-25'
file: /library/Michael Starks - Marijuana Chemistry-Ronin Publishing (1993).pdf
- title: Kleier C. Plant Science. An Introduction to Botany (Course Guidebook) 2017
filename: Kleier C. Plant Science. An Introduction to Botany (Course Guidebook)
2017.pdf
description: 'Educational resource: Kleier C. Plant Science. An Introduction to
Botany (Course Guidebook) 2017'
category: Science
date: '2024-03-25'
file: /library/Kleier C. Plant Science. An Introduction to Botany (Course Guidebook)
2017.pdf
- title: Hydroponic Food Production - A Definitive Guidebook (7th Ed)(gnv64)
filename: Hydroponic Food Production - A Definitive Guidebook (7th Ed)(gnv64).pdf
description: 'Educational resource: Hydroponic Food Production - A Definitive Guidebook
(7th Ed)(gnv64)'
category: Systems
date: '2024-03-25'
file: /library/Hydroponic Food Production - A Definitive Guidebook (7th Ed)(gnv64).pdf
- title: Dan Michaels - Higher The Lore, Legends, and Legacy of Cannabis (2023, Ten
Speed Press)
filename: Dan Michaels - Higher_ The Lore, Legends, and Legacy of Cannabis (2023,
Ten Speed Press) - libgen.li.epub
description: 'Educational resource: Dan Michaels - Higher The Lore, Legends, and
Legacy of Cannabis (2023, Ten Speed Press)'
category: Cultivation
date: '2024-03-25'
file: /library/Dan Michaels - Higher_ The Lore, Legends, and Legacy of Cannabis
(2023, Ten Speed Press) - libgen.li.epub
- title: MM Horticulture complete
filename: MM Horticulture complete.pdf
description: 'Educational resource: MM Horticulture complete'
category: General
date: '2024-03-25'
file: /library/MM Horticulture complete.pdf
- title: John Mason - Commercial Hydroponics 3rd Edition
filename: John Mason - Commercial Hydroponics 3rd Edition.pdf
description: 'Educational resource: John Mason - Commercial Hydroponics 3rd Edition'
category: Systems
date: '2024-03-25'
file: /library/John Mason - Commercial Hydroponics 3rd Edition.pdf
- title: Ross Tillman - DIY Aquaponics Marijuana Growing at Home Learn to Grow Most
Potent THC Cannabis Ever! (2019, Grelot Books Publishing Company)
filename: Ross Tillman - DIY Aquaponics Marijuana Growing at Home Learn to Grow
Most Potent THC Cannabis Ever! (2019, Grelot Books Publishing Company) - libgen.li.epub
description: 'Educational resource: Ross Tillman - DIY Aquaponics Marijuana Growing
at Home Learn to Grow Most Potent THC Cannabis Ever! (2019, Grelot Books Publishing
Company)'
category: Cultivation
date: '2024-03-25'
file: /library/Ross Tillman - DIY Aquaponics Marijuana Growing at Home Learn to
Grow Most Potent THC Cannabis Ever! (2019, Grelot Books Publishing Company) -
libgen.li.epub

2916
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "hydro-sterile",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"autoprefixer": "^10.4.23",
"hugo-bin": "^0.149.2",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"tailwindcss": "^4.1.18"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

1
public Symbolic link
View File

@@ -0,0 +1 @@
/storage/hydro-sterile/public

8
requirements.txt Normal file
View File

@@ -0,0 +1,8 @@
flask>=2.3.0
flask-cors>=4.0.0
bcrypt>=4.0.0
PyJWT>=2.8.0
ollama>=0.1.0
pdfplumber>=0.10.0
pyyaml>=6.0

112
scripts/CHAT_API_README.md Normal file
View File

@@ -0,0 +1,112 @@
# Chat API Server
This Flask-based API server provides a secure interface for the chat feature, connecting users to the Ollama model.
## Features
- **Secure**: Input sanitization, rate limiting, CORS protection
- **Rate Limited**: 10 requests per minute per IP address
- **Context Aware**: Maintains conversation history (last 5 messages)
- **Auto-restart**: Runs as systemd service with automatic restart
## Configuration
**Environment Variables:**
- `OLLAMA_HOST`: Ollama endpoint (default: `http://10.30.20.110:11434`)
- `OLLAMA_MODEL`: Model name (default: `drjones-posts-to-much`)
- `PORT`: API server port (default: `5000`)
- `CHAT_API_KEY`: Optional API key for additional security (not set by default)
## Service Management
```bash
# Start service
sudo systemctl start hugo-chat-api.service
# Stop service
sudo systemctl stop hugo-chat-api.service
# Enable at boot
sudo systemctl enable hugo-chat-api.service
# Check status
sudo systemctl status hugo-chat-api.service
# View logs
sudo journalctl -u hugo-chat-api.service -f
```
## API Endpoints
### POST `/api/chat`
Send a chat message and get AI response.
**Request:**
```json
{
"message": "How do I maintain sterile conditions?",
"history": [
{"role": "user", "content": "Previous message"},
{"role": "assistant", "content": "Previous response"}
]
}
```
**Response:**
```json
{
"response": "AI response text...",
"model": "drjones-posts-to-much"
}
```
### GET `/api/health`
Health check endpoint.
**Response:**
```json
{
"status": "healthy",
"model": "drjones-posts-to-much",
"ollama_host": "http://10.30.20.110:11434"
}
```
## Security Features
1. **Rate Limiting**: 10 requests per minute per IP address
2. **Input Sanitization**: Removes dangerous characters, limits length to 1000 chars
3. **CORS Protection**: Only allows requests from configured origins
4. **Error Handling**: Graceful error handling with user-friendly messages
## Installation
1. Install dependencies:
```bash
pip3 install flask flask-cors ollama
```
2. Enable and start service:
```bash
sudo systemctl daemon-reload
sudo systemctl enable hugo-chat-api.service
sudo systemctl start hugo-chat-api.service
```
## Troubleshooting
**Service won't start:**
- Check logs: `sudo journalctl -u hugo-chat-api.service -n 50`
- Verify Ollama is accessible: `curl http://10.30.20.110:11434/api/tags`
- Check port 5000 is available: `sudo lsof -i :5000`
**Rate limit errors:**
- Normal behavior - users are limited to 10 requests per minute
- Rate limit resets after 1 minute
**Connection errors:**
- Verify Ollama endpoint is correct and accessible
- Check firewall rules allow connections to port 5000

171
scripts/README.md Normal file
View File

@@ -0,0 +1,171 @@
# HydroLord Website Automation Scripts
This directory contains automation scripts for the HydroLord Hugo website.
## Scripts
### `generate_blog_post.py`
Generates a new blog post using Ollama's AI model and adds it to the Hugo blog.
**Configuration:**
- **Ollama Endpoint:** `http://10.30.20.110:11434`
- **Model:** `drjones-posts-to-much`
- **Output Directory:** `/root/hydro-sterile/content/blog/`
- **Schedule:** Runs automatically every 3 days via systemd timer
- **Randomization:** Each run uses a randomly varied prompt to ensure unique content
**Features:**
- Automatically finds the next available post number
- Generates title and summary from content
- Creates proper Hugo frontmatter
- Fails quietly if Ollama endpoint is unavailable (will retry in 3 days)
**Usage:**
```bash
python3 generate_blog_post.py
```
**Error Handling:**
If the Ollama endpoint is down, the script exits gracefully with code 0 (no error) to prevent systemd from logging failures. It will automatically retry in 3 days when the timer runs again.
### `start_hugo_server.sh`
Starts the Hugo development server for the website.
**Configuration:**
- **Port:** 1313 (configurable via `HUGO_PORT` env var)
- **Bind Address:** 0.0.0.0 (all interfaces, configurable via `HUGO_BIND` env var)
- **Base URL:** Read from `hugo.yaml`
**Usage:**
```bash
./start_hugo_server.sh
```
## Systemd Services
### Hugo Website Service
**Service:** `hugo-hydrolord.service`
Starts the Hugo server at boot and keeps it running.
**Commands:**
```bash
# Enable service to start at boot
sudo systemctl enable hugo-hydrolord.service
# Start service now
sudo systemctl start hugo-hydrolord.service
# Check status
sudo systemctl status hugo-hydrolord.service
# View logs
sudo journalctl -u hugo-hydrolord.service -f
```
### Blog Post Generator
**Service:** `hugo-blog-generator.service`
**Timer:** `hugo-blog-generator.timer`
Automatically generates blog posts every 3 days.
**Commands:**
```bash
# Enable timer
sudo systemctl enable hugo-blog-generator.timer
# Start timer
sudo systemctl start hugo-blog-generator.timer
# Check timer status
sudo systemctl status hugo-blog-generator.timer
# List all timers
systemctl list-timers hugo-blog-generator.timer
# Run generator manually (for testing)
sudo systemctl start hugo-blog-generator.service
# View logs
sudo journalctl -u hugo-blog-generator.service -f
```
## Installation
1. **Install Python dependencies:**
```bash
pip3 install ollama
```
2. **Make scripts executable:**
```bash
chmod +x /root/hydro-sterile/scripts/*.py
chmod +x /root/hydro-sterile/scripts/*.sh
```
3. **Enable and start Hugo service:**
```bash
sudo systemctl daemon-reload
sudo systemctl enable hugo-hydrolord.service
sudo systemctl start hugo-hydrolord.service
```
4. **Enable and start blog generator timer:**
```bash
sudo systemctl enable hugo-blog-generator.timer
sudo systemctl start hugo-blog-generator.timer
```
5. **Verify everything is running:**
```bash
sudo systemctl status hugo-hydrolord.service
sudo systemctl status hugo-blog-generator.timer
```
## Troubleshooting
### Hugo Server Not Starting
- Check if port 1313 is already in use: `sudo lsof -i :1313`
- Check logs: `sudo journalctl -u hugo-hydrolord.service -n 50`
- Verify Hugo is installed: `which hugo` or check `node_modules/.bin/hugo`
### Blog Generator Not Running
- Check if Ollama endpoint is accessible: `curl http://10.30.20.110:11434/api/tags`
- Check timer status: `systemctl list-timers hugo-blog-generator.timer`
- View logs: `sudo journalctl -u hugo-blog-generator.service -n 50`
- Test script manually: `python3 /root/hydro-sterile/scripts/generate_blog_post.py`
### Ollama Endpoint Down
The script is designed to fail quietly if Ollama is unavailable. It will exit with code 0 (success) and retry automatically in 3 days. No action needed.
## Configuration Changes
To change the Ollama endpoint or model, edit `/root/hydro-sterile/scripts/generate_blog_post.py` and modify the constants at the top:
```python
OLLAMA_HOST = "http://10.30.20.110:11434"
MODEL = "drjones-posts-to-much"
```
**Note:** The script now includes random prompt variations to ensure each blog post is unique. The model generates titles with credentials, which the script automatically extracts and separates from the article content.
After making changes, restart the timer:
```bash
sudo systemctl restart hugo-blog-generator.timer
```
## Files Location
- **Scripts:** `/root/hydro-sterile/scripts/`
- **Systemd Services:** `/etc/systemd/system/hugo-*.service`
- **Systemd Timers:** `/etc/systemd/system/hugo-*.timer`
- **Blog Posts:** `/root/hydro-sterile/content/blog/`
- **Logs:** `journalctl -u hugo-hydrolord.service` or `journalctl -u hugo-blog-generator.service`

Binary file not shown.

Binary file not shown.

Binary file not shown.

348
scripts/chat_api.py Executable file
View File

@@ -0,0 +1,348 @@
#!/usr/bin/env python3
"""
Chat API Server for Ollama Integration
This Flask server provides a secure API endpoint for the chat interface.
It handles rate limiting, input validation, and communicates with Ollama.
Usage:
python3 chat_api.py
Configuration:
- Ollama endpoint: 10.30.20.110:11434
- Model: drjones-posts-to-much
- Port: 5000 (configurable via PORT env var)
"""
import os
import sys
import time
import re
import yaml
from pathlib import Path
from datetime import datetime, timedelta
from flask import Flask, request, jsonify
from flask_cors import CORS
from functools import wraps
import hashlib
try:
from ollama import Client
except ImportError:
print("ERROR: ollama Python library not installed. Run: pip3 install ollama", file=sys.stderr)
sys.exit(1)
try:
import pdfplumber
PDF_SUPPORT = True
except ImportError:
PDF_SUPPORT = False
print("WARNING: pdfplumber not installed. PDF text extraction disabled.", file=sys.stderr)
# Configuration
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://10.30.20.110:11434")
MODEL = os.getenv("OLLAMA_MODEL", "drjones-posts-to-much")
PORT = int(os.getenv("PORT", 5000))
API_KEY = os.getenv("CHAT_API_KEY", "") # Optional API key for additional security
LIBRARY_DIR = Path("/root/hydro-sterile/static/library")
LIBRARY_YAML = Path("/root/hydro-sterile/data/library.yaml")
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB max file size
MAX_TEXT_LENGTH = 5000 # Max characters to extract from PDF
# Rate limiting storage (in-memory, simple implementation)
rate_limits = {}
# System prompt for the chat
SYSTEM_PROMPT = """You are Dr. Jones, a sterile hydroponics expert with 20 years of experience growing cannabis in a legal state.
You provide practical, direct, and knowledgeable advice about sterile hydroponics, cannabis cultivation, and growing techniques.
Keep responses concise (2-4 paragraphs), practical, and actionable. Write in first person (I, me, my) and be conversational."""
app = Flask(__name__)
# Allow CORS from the website domain and local development
CORS(app, origins=[
"https://hydrolord.thetempleofdoom.com",
"http://hydrolord.thetempleofdoom.com",
"http://localhost:1313",
"http://127.0.0.1:1313",
"http://10.30.20.243",
"http://10.30.20.243:1313"
])
def get_client_ip():
"""Get client IP address for rate limiting."""
if request.headers.get('X-Forwarded-For'):
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
return request.remote_addr
def rate_limit(max_per_minute=10, window_minutes=1):
"""Simple rate limiting decorator."""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
client_ip = get_client_ip()
now = time.time()
window_seconds = window_minutes * 60
# Clean old entries
rate_limits[client_ip] = [
timestamp for timestamp in rate_limits.get(client_ip, [])
if now - timestamp < window_seconds
]
# Check rate limit
if len(rate_limits.get(client_ip, [])) >= max_per_minute:
return jsonify({
"error": "Rate limit exceeded. Please wait a moment before sending another message."
}), 429
# Add current request
if client_ip not in rate_limits:
rate_limits[client_ip] = []
rate_limits[client_ip].append(now)
return f(*args, **kwargs)
return decorated_function
return decorator
def sanitize_input(text):
"""Sanitize user input to prevent injection attacks."""
if not text or not isinstance(text, str):
return ""
# Remove potentially dangerous characters
text = re.sub(r'[<>]', '', text)
# Limit length
text = text[:1000].strip()
return text
def load_library_files():
"""Load library file list from YAML."""
try:
with open(LIBRARY_YAML, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
files = []
for entry in data or []:
if isinstance(entry, dict) and 'title' in entry and 'file' in entry:
file_path = entry.get('file', '')
# Convert /library/ path to actual file path
if file_path.startswith('/library/'):
filename = file_path.replace('/library/', '')
full_path = LIBRARY_DIR / filename
if full_path.exists():
files.append({
'title': entry.get('title', ''),
'filename': entry.get('filename', ''),
'file': file_path,
'category': entry.get('category', ''),
'size': full_path.stat().st_size if full_path.exists() else 0
})
return files
except Exception as e:
print(f"Error loading library: {e}", file=sys.stderr)
return []
def extract_pdf_text(file_path, max_length=MAX_TEXT_LENGTH):
"""Extract text from PDF file."""
if not PDF_SUPPORT:
return None
try:
full_path = LIBRARY_DIR / file_path.replace('/library/', '')
if not full_path.exists():
return None
if full_path.stat().st_size > MAX_FILE_SIZE:
return f"[File too large: {full_path.stat().st_size / 1024 / 1024:.1f}MB. Max size: {MAX_FILE_SIZE / 1024 / 1024}MB]"
text_parts = []
with pdfplumber.open(str(full_path)) as pdf:
for page in pdf.pages[:10]: # Limit to first 10 pages
page_text = page.extract_text()
if page_text:
text_parts.append(page_text)
if len(' '.join(text_parts)) > max_length:
break
full_text = ' '.join(text_parts)
if len(full_text) > max_length:
full_text = full_text[:max_length] + "... [truncated]"
return full_text.strip()
except Exception as e:
print(f"Error extracting PDF text: {e}", file=sys.stderr)
return None
@app.route('/api/chat', methods=['POST'])
@rate_limit(max_per_minute=10, window_minutes=1)
def chat():
"""Handle chat requests."""
try:
data = request.get_json()
if not data or 'message' not in data:
return jsonify({"error": "Message is required"}), 400
message = sanitize_input(data['message'])
if not message:
return jsonify({"error": "Message cannot be empty"}), 400
# Optional API key check
if API_KEY and data.get('api_key') != API_KEY:
return jsonify({"error": "Invalid API key"}), 401
# Get selected library files
selected_files = data.get('files', [])
file_context = ""
if selected_files:
file_context_parts = []
for file_ref in selected_files[:3]: # Limit to 3 files max
if isinstance(file_ref, str):
file_path = file_ref
elif isinstance(file_ref, dict) and 'file' in file_ref:
file_path = file_ref['file']
else:
continue
# Extract text from PDF
pdf_text = extract_pdf_text(file_path)
if pdf_text:
# Get file title
file_title = file_ref.get('title', '') if isinstance(file_ref, dict) else file_path
file_context_parts.append(f"\n\n--- Content from: {file_title} ---\n{pdf_text}\n--- End of {file_title} ---")
if file_context_parts:
file_context = "\n\n[The user has referenced the following library documents. Use this information to provide accurate, specific answers based on these sources:]\n" + "\n".join(file_context_parts)
# Get conversation history (last 5 messages for context)
history = data.get('history', [])
history_messages = []
# Build conversation history
for msg in history[-5:]: # Last 5 messages
if isinstance(msg, dict) and 'role' in msg and 'content' in msg:
history_messages.append({
"role": msg['role'],
"content": sanitize_input(msg['content'])
})
# Add current user message with file context
user_message = message
if file_context:
user_message = message + file_context
history_messages.append({
"role": "user",
"content": user_message
})
# Call Ollama
try:
client = Client(host=OLLAMA_HOST)
# Build messages for Ollama
ollama_messages = [
{"role": "system", "content": SYSTEM_PROMPT}
]
# Add conversation history
for msg in history_messages:
ollama_messages.append({
"role": msg['role'],
"content": msg['content']
})
# Generate response
response = client.chat(
model=MODEL,
messages=ollama_messages,
options={
"temperature": 0.7,
"top_p": 0.9,
}
)
assistant_response = response.message.content.strip()
return jsonify({
"response": assistant_response,
"model": MODEL
})
except Exception as e:
print(f"Ollama error: {e}", file=sys.stderr)
return jsonify({
"error": "Unable to connect to AI service. Please try again later."
}), 503
except Exception as e:
print(f"API error: {e}", file=sys.stderr)
return jsonify({
"error": "An error occurred processing your request."
}), 500
@app.route('/api/library/files', methods=['GET'])
def list_library_files():
"""List available library files."""
try:
files = load_library_files()
# Filter to only PDF files for now
pdf_files = [f for f in files if f['filename'].lower().endswith('.pdf')]
return jsonify({
"files": pdf_files,
"count": len(pdf_files)
})
except Exception as e:
print(f"Error listing files: {e}", file=sys.stderr)
return jsonify({
"error": "Failed to load library files"
}), 500
@app.route('/api/library/preview', methods=['POST'])
def preview_file():
"""Preview/extract text from a library file."""
try:
data = request.get_json()
file_path = data.get('file')
if not file_path:
return jsonify({"error": "File path required"}), 400
text = extract_pdf_text(file_path)
if text is None:
return jsonify({
"error": "Could not extract text from file. File may not be a PDF or may be corrupted."
}), 400
return jsonify({
"text": text,
"length": len(text)
})
except Exception as e:
print(f"Error previewing file: {e}", file=sys.stderr)
return jsonify({
"error": "Failed to preview file"
}), 500
@app.route('/api/health', methods=['GET'])
def health():
"""Health check endpoint."""
return jsonify({
"status": "healthy",
"model": MODEL,
"ollama_host": OLLAMA_HOST,
"pdf_support": PDF_SUPPORT
})
if __name__ == '__main__':
print(f"Starting Chat API server on port {PORT}")
print(f"Ollama endpoint: {OLLAMA_HOST}")
print(f"Model: {MODEL}")
app.run(host='0.0.0.0', port=PORT, debug=False)

320
scripts/generate_blog_post.py Executable file
View File

@@ -0,0 +1,320 @@
#!/usr/bin/env python3
"""
Ollama Blog Post Generator
This script generates a new blog post using Ollama's drjones-posts-to-much model
and adds it to the Hugo blog. It runs automatically every 3 days via systemd timer.
The script uses random prompt variations to ensure each blog post is unique.
The model generates titles with credentials, which are automatically extracted
and separated from the article content.
Usage:
python3 generate_blog_post.py
Configuration:
- Ollama endpoint: 10.30.20.110:11434
- Model: drjones-posts-to-much
- Blog directory: /root/hydro-sterile/content/blog/
Error Handling:
If the Ollama endpoint is down, the script fails quietly (exits with code 0)
to prevent systemd from logging errors. It will retry in 3 days.
Author: Dr. Jones
"""
import os
import sys
import json
import random
from datetime import datetime, timedelta
from pathlib import Path
try:
from ollama import Client
except ImportError:
print("ERROR: ollama Python library not installed. Run: pip3 install ollama", file=sys.stderr)
sys.exit(1)
# Configuration
OLLAMA_HOST = "http://10.30.20.110:11434"
MODEL = "drjones-posts-to-much"
BLOG_DIR = Path("/root/hydro-sterile/content/blog")
SITE_DIR = Path("/root/hydro-sterile")
# System prompt for generating blog posts
SYSTEM_PROMPT = """You are Dr. Jones, a sterile hydroponics expert with 20 years of experience growing cannabis in a legal state.
Your writing style is authentic, direct, and knowledgeable. You focus on sterile hydroponics methodology,
growing techniques, and practical advice. Write blog posts that are:
- 300-500 words
- Focused on sterile hydroponics, cannabis cultivation, or related topics
- Written in first person (I, me, my)
- Practical and actionable
- Authentic and conversational
The model will generate a title with credentials, then the article content."""
def get_next_post_number():
"""Find the next available post number by checking existing posts."""
existing_posts = list(BLOG_DIR.glob("post-*.md"))
if not existing_posts:
return 1
numbers = []
for post in existing_posts:
try:
# Extract number from filename like "post-1.md"
num = int(post.stem.split("-")[1])
numbers.append(num)
except (ValueError, IndexError):
continue
return max(numbers) + 1 if numbers else 1
def generate_filename(post_number):
"""Generate the filename for a new post."""
return BLOG_DIR / f"post-{post_number}.md"
def create_frontmatter(title, summary, image_path=None):
"""Create YAML frontmatter for Hugo blog post."""
date_str = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
frontmatter = f"""---
title: "{title}"
date: {date_str}
image: "{image_path or '/images/IMG_1767.jpeg'}"
summary: "{summary}"
---
"""
return frontmatter
def generate_random_prompt():
"""Generate a random prompt variation to ensure different content each time."""
# Different ways to ask for hydroponics knowledge
prompt_variations = [
"Share your wisdom about sterile hydroponics. What insights can you offer growers today?",
"I'm seeking your expertise on hydroponic cultivation. What knowledge can you impart?",
"Please share your knowledge about sterile hydroponics systems. What should growers know?",
"I need your guidance on hydroponic growing. What can you teach me today?",
"What hydroponic wisdom can you share? I'm eager to learn from your experience.",
"Please enlighten me about sterile hydroponics. What key insights do you have?",
"I'm asking for your hydroponic expertise. What valuable knowledge can you provide?",
"Share your hydroponic knowledge with me. What should I know about sterile systems?",
"I'm seeking your hydroponic wisdom. What can you teach growers today?",
"Please provide your insights on sterile hydroponics. What's important to know?",
"What can you tell me about hydroponic cultivation? I'm looking for your expertise.",
"I need your hydroponic knowledge. What guidance can you offer?",
"Share your experience with sterile hydroponics. What should growers understand?",
"I'm asking for your hydroponic insights. What knowledge can you share?",
"Please enlighten me about hydroponic systems. What wisdom do you have?",
]
# Additional random elements to add variety
topic_hints = [
"",
" Focus on practical techniques.",
" Emphasize sterile methodology.",
" Include real-world examples.",
" Cover common mistakes to avoid.",
" Discuss nutrient management.",
" Talk about root health.",
" Share system design tips.",
" Address problem-solving.",
" Include cultivation tips.",
]
# Random opening phrases
openings = [
"",
"Today, ",
"In this post, ",
"Let me share ",
"Here's what I know: ",
]
base_prompt = random.choice(prompt_variations)
topic_hint = random.choice(topic_hints)
opening = random.choice(openings)
# Combine with some randomness
full_prompt = opening + base_prompt + topic_hint
return full_prompt
def call_ollama():
"""Call Ollama API to generate blog post content."""
client = Client(host=OLLAMA_HOST)
try:
# Generate random prompt for variety
user_prompt = generate_random_prompt()
# Generate the blog post content
response = client.chat(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt}
],
options={
"temperature": 0.8 + random.uniform(0, 0.2), # Random temperature between 0.8-1.0 for more variety
"top_p": 0.9,
"repeat_penalty": 1.5, # High repeat penalty to avoid repetitive content
}
)
content = response.message.content
return content
except Exception as e:
# Fail quietly - don't raise exception, just return None
# This allows the script to exit gracefully when Ollama is down
print(f"Warning: Could not connect to Ollama at {OLLAMA_HOST}: {e}", file=sys.stderr)
return None
def extract_title_and_summary(content):
"""Extract title and summary from generated content.
The model generates title with credentials, then the article.
We need to separate these and extract just the title and article content.
"""
lines = content.strip().split('\n')
# The model typically generates: "Title - Dr. Jones, Credentials\n\nArticle content"
# Or: "Title\nDr. Jones, Credentials\n\nArticle content"
# Or: "# Title\n\nDr. Jones, Credentials\n\nArticle content"
title = ""
article_start_idx = 0
credentials_found = False
# Look for title (usually first line or first few lines)
# Title might be followed by credentials line, then blank line, then article
for i, line in enumerate(lines):
line_stripped = line.strip()
# Skip empty lines at the start
if not line_stripped:
continue
# If we haven't found title yet, this might be it
if not title:
# Remove markdown headers
potential_title = line_stripped.replace('#', '').strip()
# Check if this line contains credentials (common patterns)
if any(cred in potential_title.lower() for cred in ['dr.', 'dr ', 'phd', 'ph.d', 'credentials', 'expert', 'years']):
# This might be credentials, skip it and look for title before it
if i > 0:
# Previous line might be title
prev_line = lines[i-1].strip().replace('#', '').strip()
if prev_line:
title = prev_line
article_start_idx = i + 1
credentials_found = True
break
continue
# If line ends with common credential patterns, split it
if ' - ' in potential_title or ' | ' in potential_title:
parts = potential_title.split(' - ') if ' - ' in potential_title else potential_title.split(' | ')
title = parts[0].strip()
article_start_idx = i + 1
credentials_found = True
break
else:
# This might be the title, but check next line for credentials
title = potential_title
article_start_idx = i + 1
# Check if this is a credentials line (after title)
elif not credentials_found and any(cred in line_stripped.lower() for cred in ['dr.', 'dr ', 'phd', 'ph.d', 'expert', 'years of experience']):
credentials_found = True
article_start_idx = i + 1
break
# If we've passed potential title/credentials, this is article content
elif title and not credentials_found and line_stripped:
# No credentials found, article starts here
article_start_idx = i
break
# Clean up title
if not title:
# Fallback: use first non-empty line
for line in lines:
line_stripped = line.strip().replace('#', '').strip()
if line_stripped:
title = line_stripped
break
if len(title) > 100:
title = title[:97] + "..."
# Extract article content (skip title and credentials)
article_lines = lines[article_start_idx:]
article_content = '\n'.join(article_lines).strip()
# Generate summary from first paragraph of article
summary = ""
for line in article_lines:
line = line.strip()
if line and not line.startswith('#') and len(line) > 20:
summary = line[:150]
if len(line) > 150:
summary += "..."
break
if not summary:
summary = "A new insight on sterile hydroponics and cultivation techniques."
return title, summary, article_content
def main():
"""Main function to generate and save blog post."""
# Ensure blog directory exists
BLOG_DIR.mkdir(parents=True, exist_ok=True)
# Check if Ollama is available
content = call_ollama()
if content is None:
# Fail quietly - exit with code 0 so systemd doesn't log errors
print("Ollama endpoint unavailable. Will retry in 3 days.", file=sys.stderr)
sys.exit(0)
# Get next post number
post_number = get_next_post_number()
filename = generate_filename(post_number)
# Extract title, summary, and article content (separate from credentials)
title, summary, article_content = extract_title_and_summary(content)
# Create frontmatter
frontmatter = create_frontmatter(title, summary)
# Combine frontmatter and article content (not the full content with credentials)
full_content = frontmatter + "\n" + article_content + "\n"
# Write to file
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(full_content)
print(f"Successfully created blog post: {filename}")
print(f"Title: {title}")
return 0
except Exception as e:
print(f"ERROR: Failed to write blog post: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
sys.exit(main())

478
scripts/membership_api.py Normal file
View File

@@ -0,0 +1,478 @@
#!/usr/bin/env python3
"""
Membership API Server
Handles user authentication, membership management, download tracking,
testimonials, and secure contact form submissions.
"""
import os
import sys
import sqlite3
import hashlib
import secrets
import re
import smtplib
import time
from pathlib import Path
from datetime import datetime, timedelta
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from functools import wraps
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
import bcrypt
import jwt
# Configuration
PORT = int(os.getenv("MEMBERSHIP_PORT", 80))
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "")
ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "indiana.holmes8@gmail.com")
JWT_SECRET = os.getenv("JWT_SECRET", secrets.token_urlsafe(32))
SMTP_HOST = os.getenv("SMTP_HOST", "")
SMTP_PORT = int(os.getenv("SMTP_PORT", 587))
SMTP_USER = os.getenv("SMTP_USER", "")
SMTP_PASS = os.getenv("SMTP_PASS", "")
# Static folder for Hugo build
STATIC_FOLDER = "/root/hydro-sterile/public"
# Database path
DB_PATH = Path("/root/hydro-sterile/db/members.db")
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "*"}})
# Static folder for Hugo build
STATIC_FOLDER = "/root/hydro-sterile/public"
# Rate limiting storage
rate_limits = {}
def get_client_ip():
"""Get client IP address."""
if request.headers.get('X-Forwarded-For'):
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
return request.remote_addr or 'unknown'
def init_db():
"""Initialize database with required tables."""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# Users table
c.execute('''CREATE TABLE IF NOT EXISTS users
(id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
name TEXT NOT NULL,
status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
approved_at TIMESTAMP,
approved_by TEXT)''')
# Downloads table
c.execute('''CREATE TABLE IF NOT EXISTS downloads
(id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
user_id INTEGER,
file_path TEXT NOT NULL,
downloaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id))''')
# Testimonials table
c.execute('''CREATE TABLE IF NOT EXISTS testimonials
(id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
content TEXT NOT NULL,
author_name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
approved BOOLEAN DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id))''')
# Sessions table
c.execute('''CREATE TABLE IF NOT EXISTS sessions
(id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
session_token TEXT UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id))''')
# Admin sessions table
c.execute('''CREATE TABLE IF NOT EXISTS admin_sessions
(id INTEGER PRIMARY KEY AUTOINCREMENT,
session_token TEXT UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL)''')
# Contact submissions table
c.execute('''CREATE TABLE IF NOT EXISTS contact_submissions
(id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
message TEXT NOT NULL,
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id))''')
conn.commit()
conn.close()
def rate_limit(max_per_minute=10, window_minutes=1):
"""Rate limiting decorator."""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
client_ip = get_client_ip()
now = time.time()
window_seconds = window_minutes * 60
if client_ip not in rate_limits:
rate_limits[client_ip] = []
rate_limits[client_ip] = [
timestamp for timestamp in rate_limits[client_ip]
if now - timestamp < window_seconds
]
if len(rate_limits[client_ip]) >= max_per_minute:
return jsonify({"error": "Rate limit exceeded"}), 429
rate_limits[client_ip].append(now)
return f(*args, **kwargs)
return decorated_function
return decorator
def sanitize_input(text, max_length=1000):
"""Sanitize user input."""
if not text or not isinstance(text, str):
return ""
text = re.sub(r'[<>]', '', text)
return text[:max_length].strip()
def validate_email(email):
"""Validate email format."""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
def send_email(to_email, subject, body_html, body_text=None):
"""Send email via SMTP."""
if not SMTP_HOST or not SMTP_USER or not SMTP_PASS:
print(f"EMAIL NOT CONFIGURED: Would send to {to_email}: {subject}", file=sys.stderr)
return False
try:
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = SMTP_USER
msg['To'] = to_email
if body_text:
msg.attach(MIMEText(body_text, 'plain'))
msg.attach(MIMEText(body_html, 'html'))
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
server.starttls()
server.login(SMTP_USER, SMTP_PASS)
server.send_message(msg)
return True
except Exception as e:
print(f"Email send error: {e}", file=sys.stderr)
return False
def require_auth(f):
"""Require user authentication."""
@wraps(f)
def decorated_function(*args, **kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({"error": "Authentication required"}), 401
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
user_id = payload.get('user_id')
# Verify session exists
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('SELECT user_id FROM sessions WHERE session_token = ? AND expires_at > ?',
(token, datetime.utcnow()))
session = c.fetchone()
conn.close()
if not session:
return jsonify({"error": "Invalid session"}), 401
request.user_id = user_id
return f(*args, **kwargs)
except jwt.ExpiredSignatureError:
return jsonify({"error": "Session expired"}), 401
except Exception as e:
return jsonify({"error": "Invalid token"}), 401
return decorated_function
def require_admin(f):
"""Require admin authentication."""
@wraps(f)
def decorated_function(*args, **kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({"error": "Admin authentication required"}), 401
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('SELECT id FROM admin_sessions WHERE session_token = ? AND expires_at > ?',
(token, datetime.utcnow()))
session = c.fetchone()
conn.close()
if not session:
return jsonify({"error": "Invalid admin session"}), 401
return f(*args, **kwargs)
except Exception as e:
return jsonify({"error": "Invalid admin token"}), 401
return decorated_function
# Initialize database on startup
init_db()
# ============================================================================
# API Endpoints
# ============================================================================
@app.route('/api/auth/register', methods=['POST'])
@rate_limit(max_per_minute=5, window_minutes=1)
def register():
"""Register a new user."""
try:
data = request.get_json()
email = sanitize_input(data.get('email', ''), max_length=255)
password = data.get('password', '')
name = sanitize_input(data.get('name', ''), max_length=255)
if not email or not password or not name:
return jsonify({"error": "Email, password, and name are required"}), 400
if not validate_email(email):
return jsonify({"error": "Invalid email format"}), 400
if len(password) < 8:
return jsonify({"error": "Password must be at least 8 characters"}), 400
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
try:
c.execute('INSERT INTO users (email, password_hash, name, status) VALUES (?, ?, ?, ?)',
(email, password_hash, name, 'pending'))
user_id = c.lastrowid
conn.commit()
except sqlite3.IntegrityError:
conn.close()
return jsonify({"error": "Email already registered"}), 400
conn.close()
email_body = f"<h2>New Membership Signup</h2><p>Name: {name}</p><p>Email: {email}</p>"
send_email(ADMIN_EMAIL, f"New Membership Signup: {name}", email_body)
return jsonify({"message": "Registration successful. Pending approval.", "user_id": user_id}), 201
except Exception as e:
return jsonify({"error": "Registration failed"}), 500
@app.route('/api/auth/login', methods=['POST'])
@rate_limit(max_per_minute=5, window_minutes=1)
def login():
"""User login."""
try:
data = request.get_json()
email = sanitize_input(data.get('email', ''), max_length=255)
password = data.get('password', '')
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('SELECT id, password_hash, status FROM users WHERE email = ?', (email,))
user = c.fetchone()
conn.close()
if not user or not bcrypt.checkpw(password.encode('utf-8'), user[1].encode('utf-8')):
return jsonify({"error": "Invalid email or password"}), 401
if user[2] != 'approved':
return jsonify({"error": f"Membership status: {user[2]}"}), 403
token = jwt.encode({'user_id': user[0], 'exp': datetime.utcnow() + timedelta(days=30)}, JWT_SECRET, algorithm='HS256')
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('INSERT INTO sessions (user_id, session_token, expires_at) VALUES (?, ?, ?)',
(user[0], token, datetime.utcnow() + timedelta(days=30)))
conn.commit()
conn.close()
return jsonify({"token": token, "user_id": user[0], "message": "Login successful"}), 200
except Exception as e:
return jsonify({"error": "Login failed"}), 500
@app.route('/api/auth/logout', methods=['POST'])
@require_auth
def logout():
token = request.headers.get('Authorization', '').replace('Bearer ', '')
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('DELETE FROM sessions WHERE session_token = ?', (token,))
conn.commit()
conn.close()
return jsonify({"message": "Logged out"}), 200
@app.route('/api/auth/check', methods=['GET'])
def check_auth():
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token: return jsonify({"authenticated": False}), 200
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('SELECT u.id, u.email, u.name, u.status FROM users u JOIN sessions s ON u.id = s.user_id WHERE s.session_token = ? AND s.expires_at > ?', (token, datetime.utcnow()))
user = c.fetchone()
conn.close()
if user: return jsonify({"authenticated": True, "user_id": user[0], "email": user[1], "name": user[2], "status": user[3]}), 200
except: pass
return jsonify({"authenticated": False}), 200
@app.route('/api/download/count', methods=['GET'])
def get_download_count():
ip_address = get_client_ip()
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('SELECT COUNT(*) FROM downloads WHERE ip_address = ?', (ip_address,))
count = c.fetchone()[0]
conn.close()
return jsonify({"count": count, "limit": 1, "unlimited": False}), 200
@app.route('/api/download/track', methods=['POST'])
@rate_limit(max_per_minute=10, window_minutes=1)
def track_download():
data = request.get_json()
file_path = sanitize_input(data.get('file_path', ''), max_length=500)
ip_address = get_client_ip()
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('INSERT INTO downloads (ip_address, file_path) VALUES (?, ?)', (ip_address, file_path))
conn.commit()
conn.close()
return jsonify({"message": "Tracked"}), 200
@app.route('/api/testimonials', methods=['GET', 'POST'])
def handle_testimonials():
if request.method == 'GET':
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('SELECT id, content, author_name, created_at FROM testimonials WHERE approved = 1 ORDER BY created_at DESC')
rows = c.fetchall()
conn.close()
return jsonify({"testimonials": [{"id": r[0], "content": r[1], "author_name": r[2], "created_at": r[3]} for r in rows]}), 200
else:
@require_auth
def submit():
data = request.get_json()
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('INSERT INTO testimonials (user_id, content, author_name, approved) VALUES (?, ?, ?, 0)', (request.user_id, data.get('content'), data.get('author_name')))
conn.commit()
conn.close()
return jsonify({"message": "Submitted"}), 201
return submit()
@app.route('/api/contact', methods=['POST'])
@require_auth
def contact():
data = request.get_json()
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('INSERT INTO contact_submissions (user_id, name, email, message) VALUES (?, ?, ?, ?)', (request.user_id, data.get('name'), data.get('email'), data.get('message')))
conn.commit()
conn.close()
return jsonify({"message": "Sent"}), 200
@app.route('/api/admin/login', methods=['POST'])
def admin_login():
data = request.get_json()
if data.get('password') == ADMIN_PASSWORD:
token = jwt.encode({'admin': True, 'exp': datetime.utcnow() + timedelta(hours=24)}, JWT_SECRET, algorithm='HS256')
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('INSERT INTO admin_sessions (session_token, expires_at) VALUES (?, ?)', (token, datetime.utcnow() + timedelta(hours=24)))
conn.commit()
conn.close()
return jsonify({"token": token}), 200
return jsonify({"error": "Unauthorized"}), 401
@app.route('/api/admin/pending', methods=['GET'])
@require_admin
def admin_pending():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('SELECT id, email, name, created_at FROM users WHERE status = "pending"')
rows = c.fetchall()
conn.close()
return jsonify({"members": [{"id": r[0], "email": r[1], "name": r[2], "created_at": r[3]} for r in rows]}), 200
@app.route('/api/admin/approve', methods=['POST'])
@require_admin
def admin_approve():
user_id = request.get_json().get('user_id')
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('UPDATE users SET status = "approved" WHERE id = ?', (user_id,))
conn.commit()
conn.close()
return jsonify({"message": "Approved"}), 200
@app.route('/api/health', methods=['GET'])
def health():
return jsonify({"status":"healthy"}), 200
# ============================================================================
# Static File Serving (CATCH-ALL)
# ============================================================================
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve(path):
# Ignore API routes
if path.startswith('api/'):
return "Not Found", 404
full_path = os.path.join(STATIC_FOLDER, path)
# If path is a directory or empty, serve index.html
if path == "" or os.path.isdir(full_path):
# Check if the directory has an index.html
if os.path.exists(os.path.join(full_path, 'index.html')):
return send_from_directory(full_path, 'index.html')
# Otherwise, check if it's a pretty URL like /about
elif os.path.exists(os.path.join(STATIC_FOLDER, path, 'index.html')):
return send_from_directory(os.path.join(STATIC_FOLDER, path), 'index.html')
return send_from_directory(STATIC_FOLDER, '404.html'), 404
# If file exists, serve it
if os.path.exists(full_path):
return send_from_directory(STATIC_FOLDER, path)
# Handle Hugo's pretty URLs: /about -> /about/index.html
if os.path.exists(os.path.join(STATIC_FOLDER, path, 'index.html')):
return send_from_directory(os.path.join(STATIC_FOLDER, path), 'index.html')
# Default to 404
if os.path.exists(os.path.join(STATIC_FOLDER, '404.html')):
return send_from_directory(STATIC_FOLDER, '404.html'), 404
return "404 Not Found", 404
if __name__ == '__main__':
print(f"Starting Membership API + Static Server on port {PORT}")
app.run(host='0.0.0.0', port=PORT, debug=False)

42
scripts/start_hugo_server.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
#
# Hugo Server Startup Script
#
# This script starts the Hugo development server with production-like settings.
# It's designed to be run as a systemd service.
#
# Configuration:
# - Site directory: /root/hydro-sterile
# - Port: 1313 (Hugo default)
# - Bind: 0.0.0.0 (all interfaces)
# - Base URL: From hugo.yaml config
#
set -e
SITE_DIR="/root/hydro-sterile"
PORT="${HUGO_PORT:-7575}"
BIND="${HUGO_BIND:-0.0.0.0}"
BASEURL="${HUGO_BASEURL:-$(grep '^baseURL:' hugo.yaml | sed 's/.*: //' | tr -d "'\"")}"
cd "$SITE_DIR" || exit 1
# Check if Hugo is available via npm
if command -v npm >/dev/null 2>&1 && [ -d "node_modules/.bin" ]; then
HUGO_CMD="./node_modules/.bin/hugo"
elif command -v hugo >/dev/null 2>&1; then
HUGO_CMD="hugo"
else
echo "ERROR: Hugo not found. Please install Hugo or run 'npm install' in $SITE_DIR" >&2
exit 1
fi
# Start Hugo server
# Using --noHTTPCache for development, remove for production
exec "$HUGO_CMD" server \
--bind "$BIND" \
--port "$PORT" \
--baseURL "$BASEURL" \
--appendPort=false \
--disableFastRender

403
static/js/membership.js Normal file
View File

@@ -0,0 +1,403 @@
/**
* Membership System Frontend
* Handles authentication, paywall checks, and download tracking
*/
const getApiBase = () => {
const host = window.location.hostname;
const isLocal = host === 'localhost' || host === '127.0.0.1' || host.startsWith('10.');
return isLocal ? 'http://' + host + ':5001' : window.location.origin.replace(/\/$/, '');
};
const API_BASE = getApiBase() + '/api';
let authState = {
authenticated: false,
user: null,
token: null
};
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
checkAuthStatus();
setupDownloadTracking();
setupContactForm();
});
/**
* Check authentication status
*/
async function checkAuthStatus() {
const token = localStorage.getItem('membership_token');
if (!token) {
updateAuthUI(false);
return;
}
try {
const response = await fetch(`${API_BASE}/auth/check`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (data.authenticated) {
authState = {
authenticated: true,
user: data,
token: token
};
updateAuthUI(true);
checkPaywallAccess();
} else {
localStorage.removeItem('membership_token');
updateAuthUI(false);
}
} catch (error) {
console.error('Auth check failed:', error);
updateAuthUI(false);
}
}
/**
* Update UI based on authentication state
*/
function updateAuthUI(authenticated) {
const loginElements = document.querySelectorAll('.membership-login');
const logoutElements = document.querySelectorAll('.membership-logout');
const memberElements = document.querySelectorAll('.membership-only');
const guestElements = document.querySelectorAll('.guest-only');
if (authenticated) {
loginElements.forEach(el => el.style.display = 'none');
logoutElements.forEach(el => el.style.display = 'block');
memberElements.forEach(el => el.style.display = 'block');
guestElements.forEach(el => el.style.display = 'none');
// Update user name display
const userNameElements = document.querySelectorAll('.user-name');
userNameElements.forEach(el => {
if (authState.user && authState.user.name) {
el.textContent = authState.user.name;
}
});
} else {
loginElements.forEach(el => el.style.display = 'block');
logoutElements.forEach(el => el.style.display = 'none');
memberElements.forEach(el => el.style.display = 'none');
guestElements.forEach(el => el.style.display = 'block');
}
}
/**
* Check paywall access and show/hide content
*/
async function checkPaywallAccess() {
if (!authState.authenticated) {
// Check download count for IP
try {
const response = await fetch(`${API_BASE}/download/count`);
const data = await response.json();
if (data.limit_reached || (data.count >= data.limit && !data.unlimited)) {
showPaywall();
}
} catch (error) {
console.error('Download count check failed:', error);
}
}
}
/**
* Show paywall message
*/
function showPaywall() {
const paywallElements = document.querySelectorAll('.paywall-message');
paywallElements.forEach(el => {
el.style.display = 'block';
});
const paywallContent = document.querySelectorAll('.paywall-content');
paywallContent.forEach(el => {
el.style.display = 'none';
});
}
/**
* Hide paywall message
*/
function hidePaywall() {
const paywallElements = document.querySelectorAll('.paywall-message');
paywallElements.forEach(el => {
el.style.display = 'none';
});
const paywallContent = document.querySelectorAll('.paywall-content');
paywallContent.forEach(el => {
el.style.display = 'block';
});
}
/**
* Setup download tracking
*/
function setupDownloadTracking() {
document.addEventListener('click', async function(e) {
const downloadLink = e.target.closest('a[download], a[href*="/library/"], a[href*="/pdfs/"]');
if (!downloadLink) return;
const filePath = downloadLink.getAttribute('href') || downloadLink.getAttribute('download');
if (!filePath) return;
// Check if user is authenticated
if (!authState.authenticated) {
// Check download count
try {
const response = await fetch(`${API_BASE}/download/count`);
const data = await response.json();
if (data.count >= data.limit && !data.unlimited) {
e.preventDefault();
alert('You have reached the free download limit. Please sign up for membership to download unlimited resources.');
window.location.href = '/membership/';
return;
}
} catch (error) {
console.error('Download check failed:', error);
}
}
// Track download
try {
await fetch(`${API_BASE}/download/track`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(authState.token ? { 'Authorization': `Bearer ${authState.token}` } : {})
},
body: JSON.stringify({ file_path: filePath })
});
} catch (error) {
console.error('Download tracking failed:', error);
// Don't prevent download if tracking fails
}
});
}
/**
* Setup contact form
*/
function setupContactForm() {
const contactForm = document.getElementById('contact-form');
if (!contactForm) return;
contactForm.addEventListener('submit', async function(e) {
e.preventDefault();
if (!authState.authenticated) {
alert('Please log in to submit the contact form.');
window.location.href = '/membership/';
return;
}
const formData = new FormData(contactForm);
const data = {
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message')
};
try {
const response = await fetch(`${API_BASE}/contact`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authState.token}`
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
alert('Message sent successfully!');
contactForm.reset();
} else {
alert(result.error || 'Failed to send message');
}
} catch (error) {
console.error('Contact form error:', error);
alert('Failed to send message. Please try again.');
}
});
}
/**
* User login
*/
async function login(email, password) {
try {
const response = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('membership_token', data.token);
authState = {
authenticated: true,
user: data,
token: data.token
};
updateAuthUI(true);
hidePaywall();
return { success: true };
} else {
return { success: false, error: data.error };
}
} catch (error) {
return { success: false, error: 'Login failed. Please try again.' };
}
}
/**
* User logout
*/
async function logout() {
const token = localStorage.getItem('membership_token');
if (token) {
try {
await fetch(`${API_BASE}/auth/logout`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
} catch (error) {
console.error('Logout error:', error);
}
}
localStorage.removeItem('membership_token');
authState = {
authenticated: false,
user: null,
token: null
};
updateAuthUI(false);
window.location.reload();
}
/**
* User registration
*/
async function register(email, password, name) {
try {
const response = await fetch(`${API_BASE}/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password, name })
});
const data = await response.json();
if (response.ok) {
return { success: true, message: data.message };
} else {
return { success: false, error: data.error };
}
} catch (error) {
return { success: false, error: 'Registration failed. Please try again.' };
}
}
/**
* Submit testimonial
*/
async function submitTestimonial(content, authorName) {
if (!authState.authenticated) {
return { success: false, error: 'Please log in to submit a testimonial' };
}
try {
const response = await fetch(`${API_BASE}/testimonials`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authState.token}`
},
body: JSON.stringify({ content, author_name: authorName })
});
const data = await response.json();
if (response.ok) {
return { success: true, message: data.message };
} else {
return { success: false, error: data.error };
}
} catch (error) {
return { success: false, error: 'Failed to submit testimonial' };
}
}
/**
* Load testimonials
*/
async function loadTestimonials() {
try {
const response = await fetch(`${API_BASE}/testimonials`);
const data = await response.json();
if (response.ok) {
return { success: true, testimonials: data.testimonials };
} else {
return { success: false, error: data.error };
}
} catch (error) {
return { success: false, error: 'Failed to load testimonials' };
}
}
/**
* Check if the user has reached their download limit (guests only)
*/
async function checkDownloadLimit() {
if (authState.authenticated) {
document.querySelectorAll('.paywall-message').forEach(el => el.style.display = 'none');
document.querySelectorAll('.paywall-content').forEach(el => el.style.display = 'block');
document.querySelectorAll('.guest-only').forEach(el => el.style.display = 'none');
document.querySelectorAll('.membership-only').forEach(el => el.style.display = 'block');
return;
}
try {
const response = await fetch(`${API_BASE}/download/count`);
const data = await response.json();
if (data.count >= data.limit && !data.unlimited) {
document.querySelectorAll('.paywall-message').forEach(el => el.style.display = 'block');
document.querySelectorAll('.paywall-content').forEach(el => el.style.display = 'none');
} else {
document.querySelectorAll('.paywall-message').forEach(el => el.style.display = 'none');
document.querySelectorAll('.paywall-content').forEach(el => el.style.display = 'block');
}
} catch (error) {
console.error('Download limit check failed:', error);
}
}
// Export functions for use in Alpine.js or other scripts
window.MembershipAPI = {
login,
logout,
register,
submitTestimonial,
loadTestimonials,
checkAuthStatus,
checkDownloadLimit,
getAuthState: () => authState
};

0
static/pdfs/cleaning.pdf Normal file
View File

View File

View File

View File

0
static/pdfs/lighting.pdf Normal file
View File

View File

0
static/pdfs/pests.pdf Normal file
View File

View File

0
static/pdfs/seeds.pdf Normal file
View File

View File

5
static/robots.txt Normal file
View File

@@ -0,0 +1,5 @@
User-agent: *
Allow: /
Sitemap: https://hydrolord.thetempleofdoom.com/sitemap.xml