update README with local testing guide, fix feedback placeholder, sync .env.example
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,16 +12,20 @@ DATABASE_PATH=data/app.db
|
||||
MAGIC_LINK_EXPIRY_MINUTES=15
|
||||
SESSION_LIFETIME_DAYS=30
|
||||
|
||||
# Email (Resend)
|
||||
# Email (Resend) — leave blank for dev (emails print to console)
|
||||
RESEND_API_KEY=
|
||||
EMAIL_FROM=hello@padelnomics.io
|
||||
ADMIN_EMAIL=leads@padelnomics.io
|
||||
|
||||
# Paddle
|
||||
# Paddle — leave blank to skip checkout (overlay won't initialize)
|
||||
PADDLE_API_KEY=
|
||||
PADDLE_CLIENT_TOKEN=
|
||||
PADDLE_WEBHOOK_SECRET=
|
||||
PADDLE_PRICE_STARTER=
|
||||
PADDLE_PRICE_PRO=
|
||||
PADDLE_ENVIRONMENT=sandbox
|
||||
|
||||
# Umami — leave blank for dev (analytics tracking disabled)
|
||||
UMAMI_API_URL=https://umami.padelnomics.io
|
||||
UMAMI_API_TOKEN=
|
||||
|
||||
# Rate limiting
|
||||
RATE_LIMIT_REQUESTS=100
|
||||
|
||||
@@ -2,39 +2,295 @@
|
||||
|
||||
Plan, finance, and build your padel business.
|
||||
|
||||
## Development
|
||||
## Local Development Setup
|
||||
|
||||
### CSS (Tailwind)
|
||||
### Prerequisites
|
||||
|
||||
Uses the [Tailwind CSS standalone CLI](https://tailwindcss.com/blog/standalone-cli) — no Node.js required.
|
||||
- Python 3.12+
|
||||
- [uv](https://docs.astral.sh/uv/) (Python package manager)
|
||||
- WeasyPrint system dependencies (for PDF export):
|
||||
- Fedora/RHEL: `sudo dnf install pango gdk-pixbuf2 cairo`
|
||||
- Debian/Ubuntu: `sudo apt install libpango-1.0-0 libgdk-pixbuf2.0-0 libcairo2`
|
||||
- macOS: `brew install pango gdk-pixbuf cairo`
|
||||
|
||||
### 1. Install dependencies
|
||||
|
||||
```bash
|
||||
cd padelnomics
|
||||
uv sync
|
||||
```
|
||||
|
||||
### 2. Configure environment
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env if you want to change defaults — it works out of the box for dev
|
||||
```
|
||||
|
||||
Key defaults for local dev:
|
||||
- `DEBUG=true` — enables dev login, console email output, verbose errors
|
||||
- `ADMIN_PASSWORD=admin` — admin panel login at `/admin`
|
||||
- `DATABASE_PATH=data/app.db` — SQLite file, auto-created
|
||||
- Paddle/Resend keys left blank — checkout overlay and email sending disabled,
|
||||
magic links print to console
|
||||
|
||||
### 3. Run database migrations
|
||||
|
||||
```bash
|
||||
uv run python -m padelnomics.migrations.migrate
|
||||
```
|
||||
|
||||
This creates `data/app.db` with all tables. Fresh databases get the full
|
||||
schema in one shot; existing databases get incremental migrations applied.
|
||||
|
||||
### 4. (Optional) Set up Paddle sandbox products
|
||||
|
||||
Only needed if you want to test actual checkout flows:
|
||||
|
||||
```bash
|
||||
# Add your Paddle sandbox keys to .env first:
|
||||
# PADDLE_API_KEY=test_xxx
|
||||
# PADDLE_CLIENT_TOKEN=test_xxx
|
||||
# PADDLE_WEBHOOK_SECRET=pdl_ntfset_xxx
|
||||
|
||||
uv run python -m padelnomics.scripts.setup_paddle
|
||||
```
|
||||
|
||||
This creates all products/prices in Paddle sandbox and writes the IDs to the
|
||||
`paddle_products` table. Without this, checkout buttons will show an error
|
||||
("Invalid plan selected") but everything else works.
|
||||
|
||||
### 5. Start the app
|
||||
|
||||
```bash
|
||||
uv run python -m padelnomics.app
|
||||
```
|
||||
|
||||
App runs at **http://localhost:5000**.
|
||||
|
||||
### 6. Start the background worker (separate terminal)
|
||||
|
||||
```bash
|
||||
uv run python -m padelnomics.worker
|
||||
```
|
||||
|
||||
The worker processes background tasks: emails, lead forwarding, PDF generation,
|
||||
credit refills. Without it, queued tasks stay pending but the app still works.
|
||||
|
||||
### 7. CSS (Tailwind)
|
||||
|
||||
```bash
|
||||
make css-watch # rebuild on file changes (dev)
|
||||
make css-build # one-off minified build (CI/Docker)
|
||||
```
|
||||
|
||||
The first run downloads the Tailwind binary to `bin/tailwindcss` automatically.
|
||||
First run downloads the Tailwind standalone CLI to `bin/tailwindcss`.
|
||||
|
||||
Edit `src/padelnomics/static/css/input.css` for theme tokens, base styles, and component classes. Output is gitignored — Docker builds it in a dedicated stage.
|
||||
Edit `src/padelnomics/static/css/input.css` for theme tokens, base styles,
|
||||
and component classes.
|
||||
|
||||
---
|
||||
|
||||
## Testing Each Feature Locally
|
||||
|
||||
### Authentication
|
||||
|
||||
1. Go to http://localhost:5000/auth/login
|
||||
2. **Dev shortcut**: http://localhost:5000/auth/dev-login?email=test@example.com
|
||||
— instant login, no email needed (DEBUG mode only)
|
||||
3. Magic links: submit any email → link prints to console (no Resend key needed)
|
||||
|
||||
### Financial Planner
|
||||
|
||||
1. Log in → http://localhost:5000/planner/
|
||||
2. Create a scenario, fill in parameters, click Calculate
|
||||
3. View results across tabs (Investment, Revenue, Cash Flow, Metrics)
|
||||
|
||||
### Business Plan PDF Export
|
||||
|
||||
1. Create at least one scenario with calculated results
|
||||
2. Click "Export Business Plan (PDF)" in the planner sidebar, or go to
|
||||
http://localhost:5000/planner/export
|
||||
3. **Without Paddle**: checkout will fail (no products in DB). To test the
|
||||
PDF generation directly, you can:
|
||||
- Insert a test export record in the DB:
|
||||
```bash
|
||||
uv run python -c "
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('data/app.db')
|
||||
conn.execute('''INSERT INTO business_plan_exports
|
||||
(user_id, scenario_id, language, status)
|
||||
VALUES (1, 1, 'en', 'pending')''')
|
||||
conn.commit()
|
||||
print('Export record created with id:', conn.execute('SELECT last_insert_rowid()').fetchone()[0])
|
||||
"
|
||||
```
|
||||
- Then visit http://localhost:5000/planner/export (the generating page
|
||||
will show). The worker generates the PDF if running.
|
||||
4. **With Paddle sandbox**: complete checkout → webhook triggers worker →
|
||||
PDF generated → download link appears
|
||||
|
||||
### Quote Request (Lead Submission)
|
||||
|
||||
1. Go to http://localhost:5000/leads/quote
|
||||
2. Walk through the 9-step wizard
|
||||
3. If logged in: lead submitted directly
|
||||
4. If guest: verification email prints to console → click the link → lead
|
||||
goes live
|
||||
|
||||
### Supplier Directory
|
||||
|
||||
1. http://localhost:5000/directory/ — full-text search, tier-based ordering
|
||||
2. Click a supplier → profile page at `/directory/<slug>`
|
||||
3. Click tracking routes: `/directory/<slug>/website` and `/directory/<slug>/quote`
|
||||
redirect to supplier website / quote form
|
||||
|
||||
### Supplier Signup + Dashboard
|
||||
|
||||
1. Visit http://localhost:5000/suppliers — the "For Suppliers" landing page
|
||||
2. Click "Get Started" on a plan → redirects to `/suppliers/signup`
|
||||
3. Walk through the 4-step HTMX wizard (plan → boosts → credits → checkout)
|
||||
4. **Without Paddle**: the final checkout step will fail. To simulate a
|
||||
signed-up supplier, insert test data:
|
||||
```bash
|
||||
uv run python -c "
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('data/app.db')
|
||||
# Update an existing supplier to be claimed by user 1 with growth tier
|
||||
conn.execute('''UPDATE suppliers SET
|
||||
claimed_by = 1, tier = 'growth', credit_balance = 30
|
||||
WHERE id = 1''')
|
||||
conn.commit()
|
||||
print('Supplier 1 claimed by user 1 with growth tier')
|
||||
"
|
||||
```
|
||||
5. Log in as that user → http://localhost:5000/suppliers/dashboard
|
||||
6. Browse tabs: Overview, Lead Feed, My Listing, Boost & Upsells
|
||||
7. Lead Feed: unlock leads (costs credits), filter by heat/country/timeline
|
||||
8. My Listing: edit profile fields, upload logo
|
||||
9. Boosts: purchase boosts and credit packs (needs Paddle for actual checkout)
|
||||
|
||||
### Admin Panel
|
||||
|
||||
1. Go to http://localhost:5000/admin
|
||||
2. Log in with password: `admin` (default in dev)
|
||||
3. **Dashboard**: user stats, lead funnel, supplier stats, task queue health
|
||||
4. **Leads** (`/admin/leads`): filter by status/heat/country, view detail,
|
||||
update status, forward to supplier
|
||||
5. **Suppliers** (`/admin/suppliers`): filter by tier/country/name, view detail
|
||||
with credit ledger + boost history, adjust credits, change tier
|
||||
6. **Feedback** (`/admin/feedback`): view all feedback submissions
|
||||
7. **Users** (`/admin/users`): search, impersonate users
|
||||
8. **Tasks** (`/admin/tasks`): view/retry/delete background tasks
|
||||
|
||||
### Feedback Widget
|
||||
|
||||
1. Click the "Feedback" button in the top navbar (visible on every page)
|
||||
2. Type a message → Send
|
||||
3. View submissions at http://localhost:5000/admin/feedback
|
||||
|
||||
### Seeding Test Data
|
||||
|
||||
The database starts empty. To populate it with test data for development:
|
||||
|
||||
```bash
|
||||
uv run python -c "
|
||||
import sqlite3, json
|
||||
conn = sqlite3.connect('data/app.db')
|
||||
|
||||
# Create a test supplier
|
||||
conn.execute('''INSERT OR IGNORE INTO suppliers
|
||||
(name, slug, category, country_code, city, tier, credit_balance,
|
||||
short_description, website, contact_name, contact_email)
|
||||
VALUES
|
||||
('Padel Pro Courts', 'padel-pro-courts', 'court_manufacturer', 'DE', 'Munich',
|
||||
'free', 0, 'Premium padel court manufacturer', 'https://example.com',
|
||||
'Max Mueller', 'max@example.com')''')
|
||||
|
||||
# Create a test lead
|
||||
conn.execute('''INSERT OR IGNORE INTO lead_requests
|
||||
(lead_type, facility_type, court_count, glass_type, country, location,
|
||||
timeline, budget_estimate, heat_score, credit_cost, status,
|
||||
contact_name, contact_email, contact_phone, stakeholder_type,
|
||||
verified_at, build_context, financing_status)
|
||||
VALUES
|
||||
('quote', 'indoor_rent', 6, 'panoramic', 'Germany', 'Berlin',
|
||||
'3-6 months', 450000, 'hot', 35, 'new',
|
||||
'John Doe', 'john@example.com', '+49123456', 'investor',
|
||||
datetime('now'), 'lease_signed', 'loan_approved')''')
|
||||
|
||||
conn.commit()
|
||||
print('Test data seeded')
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
padelnomics/src/padelnomics/
|
||||
├── app.py # Application factory, blueprint registration
|
||||
├── core.py # Config, database, email, CSRF, rate limiting
|
||||
├── credits.py # Credit system (balance, ledger, unlock)
|
||||
├── businessplan.py # WeasyPrint PDF generation engine
|
||||
├── worker.py # SQLite-based background task queue
|
||||
├── migrations/
|
||||
│ ├── schema.sql # Full schema (source of truth for fresh DBs)
|
||||
│ ├── migrate.py # Migration runner
|
||||
│ └── versions/ # Incremental migrations (0001-0008)
|
||||
├── scripts/
|
||||
│ └── setup_paddle.py # Create Paddle products + write IDs to DB
|
||||
├── templates/
|
||||
│ ├── base.html # Layout: nav, footer, HTMX, Paddle.js, Umami
|
||||
│ └── businessplan/ # PDF templates (plan.html, plan.css)
|
||||
├── static/ # CSS, JS, images
|
||||
├── admin/ # Admin panel (dashboard, users, leads, suppliers, feedback)
|
||||
├── auth/ # Magic link auth, dev-login
|
||||
├── billing/ # Paddle checkout, webhooks, subscription management
|
||||
├── dashboard/ # User dashboard
|
||||
├── directory/ # Supplier directory (FTS5 search, profiles)
|
||||
├── leads/ # Quote request wizard
|
||||
├── planner/ # Financial planner + PDF export routes
|
||||
├── public/ # Landing, marketing, legal, feedback endpoint
|
||||
└── suppliers/ # Supplier signup wizard, dashboard (4 tabs), lead feed
|
||||
```
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- **HTMX everywhere**: server renders HTML partials, HTMX swaps them. No
|
||||
client-side state management. Forms use `hx-post`, filters use `hx-get`.
|
||||
- **Paddle.js overlay checkout**: server returns JSON `{items, customData,
|
||||
settings}`, frontend calls `Paddle.Checkout.open()`. Webhook handles the rest.
|
||||
- **Credit system**: heat-based pricing (hot=35, warm=20, cool=8). Suppliers
|
||||
unlock leads by spending credits. Ledger-based balance tracking.
|
||||
- **SQLite + aiosqlite**: single-file database with WAL mode. Background
|
||||
worker uses same DB file.
|
||||
- **Blueprints**: each domain has its own blueprint with template folder.
|
||||
|
||||
---
|
||||
|
||||
## Docker (Production)
|
||||
|
||||
```bash
|
||||
docker compose up -d # app + worker + scheduler + litestream
|
||||
docker compose logs -f app # tail logs
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
## CI/CD:
|
||||
Go to GitLab → padelnomics → Settings → CI/CD → Variables and add:
|
||||
|
||||
┌─────────────────┬────────────────────────────┬───────────────────────────────────────────┐
|
||||
│ Variable │ Value │ Notes │
|
||||
├─────────────────┼────────────────────────────┼───────────────────────────────────────────┤
|
||||
│ SSH_PRIVATE_KEY │ Your ed25519 private key │ Mask it, type "Variable" │
|
||||
├─────────────────┼────────────────────────────┼───────────────────────────────────────────┤
|
||||
│ DEPLOY_HOST │ Your Hetzner server IP │ e.g. 1.2.3.4 │
|
||||
├─────────────────┼────────────────────────────┼───────────────────────────────────────────┤
|
||||
│ DEPLOY_USER │ SSH username on the server │ e.g. deploy or root │
|
||||
├─────────────────┼────────────────────────────┼───────────────────────────────────────────┤
|
||||
│ SSH_KNOWN_HOSTS │ Server host key │ Run ssh-keyscan $YOUR_SERVER_IP to get it │
|
||||
└─────────────────┴────────────────────────────┴───────────────────────────────────────────┘
|
||||
| Variable | Value | Notes |
|
||||
|----------|-------|-------|
|
||||
| SSH_PRIVATE_KEY | Your ed25519 private key | Mask it, type "Variable" |
|
||||
| DEPLOY_HOST | Your Hetzner server IP | e.g. 1.2.3.4 |
|
||||
| DEPLOY_USER | SSH username on the server | e.g. deploy or root |
|
||||
| SSH_KNOWN_HOSTS | Server host key | Run `ssh-keyscan $YOUR_SERVER_IP` |
|
||||
|
||||
And on the server side (one-time setup):
|
||||
1. Add the matching public key to ~/.ssh/authorized_keys for the deploy user
|
||||
2. Clone the repo to /opt/padelnomics
|
||||
3. Create .env from padelnomics/.env.example with production values
|
||||
4. chmod +x deploy.sh && ./deploy.sh for the first deploy
|
||||
5. Point NPM to port 5000
|
||||
Server-side one-time setup:
|
||||
1. Add the matching public key to `~/.ssh/authorized_keys` for the deploy user
|
||||
2. Clone the repo to `/opt/padelnomics`
|
||||
3. Create `.env` from `padelnomics/.env.example` with production values
|
||||
4. `chmod +x deploy.sh && ./deploy.sh` for the first deploy
|
||||
5. Point reverse proxy to port 5000
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="page_url" id="feedback-page-url">
|
||||
<p style="font-size:0.8125rem;font-weight:600;color:#1E293B;margin:0 0 8px">Send Feedback</p>
|
||||
<textarea name="message" rows="3" required placeholder="What's on your mind?"
|
||||
<textarea name="message" rows="3" required placeholder="Ideas to improve this page..."
|
||||
style="width:100%;border:1px solid #E2E8F0;border-radius:6px;padding:8px;font-size:0.8125rem;font-family:inherit;resize:vertical"></textarea>
|
||||
<button type="submit" class="btn" style="width:100%;margin-top:8px;font-size:0.8125rem;padding:8px">Send</button>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user