Files
padelnomics/README.md
Deeman 4ae00b35d1 refactor: flatten padelnomics/padelnomics/ → repo root
git mv all tracked files from the nested padelnomics/ workspace
directory to the git repo root. Merged .gitignore files.
No code changes — pure path rename.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 00:44:40 +01:00

414 lines
14 KiB
Markdown

# Padelnomics
Plan, finance, and build your padel business.
## Quick Start
```bash
./scripts/dev_setup.sh # one-time: deps, .env, migrations, seed data, CSS
./scripts/dev_run.sh # start app + worker + CSS watcher
```
Then open:
- **App**: http://localhost:5000
- **Dev login**: http://localhost:5000/auth/dev-login?email=dev@localhost
- **Admin**: http://localhost:5000/admin (password: `admin`)
## Local Development Setup
### Prerequisites
- 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)
```
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.
---
## 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')
"
```
### Resend Test Emails
When `RESEND_API_KEY` is blank (default), all emails print to the console —
no Resend account needed.
When you have a Resend API key, you can use their test addresses to simulate
delivery outcomes without a verified domain:
| Address | Behavior |
|---------|----------|
| `delivered@resend.dev` | Accepted, simulates successful delivery |
| `bounced@resend.dev` | Simulates a hard bounce |
| `complained@resend.dev` | Simulates a spam complaint |
| `suppressed@resend.dev` | Simulates a suppressed recipient |
These support `+label` syntax (e.g. `delivered+test1@resend.dev`) for unique
recipients. You can also send **from** `onboarding@resend.dev` without a
verified domain.
---
## Waitlist Mode
Waitlist mode allows you to validate market demand before building features. When enabled, selected routes show waitlist signup forms instead of normal flows. Perfect for lean startup smoke tests and pre-launch validation.
### Quick Start
```bash
# In .env
WAITLIST_MODE=true
RESEND_AUDIENCE_WAITLIST=aud_xxx # Optional: for bulk launch campaigns
```
Restart the app, then visit:
- http://localhost:5000/auth/signup — entrepreneur waitlist
- http://localhost:5000/suppliers/signup — supplier waitlist
- http://localhost:5000/planner/export — feature gate example
Submit an email → see confirmation → check the `waitlist` table:
```bash
uv run python -c "
import sqlite3
conn = sqlite3.connect('data/app.db')
rows = conn.execute('SELECT * FROM waitlist ORDER BY created_at DESC').fetchall()
for row in rows: print(row)
"
```
### How It Works
Two abstractions eliminate code duplication:
1. **`@waitlist_gate(template, **context)` decorator** — intercepts GET requests when `WAITLIST_MODE=true`
2. **`capture_waitlist_email(email, intent, plan)` helper** — handles DB + email + Resend
Example usage:
```python
from padelnomics.core import capture_waitlist_email, waitlist_gate
@bp.route("/signup", methods=["GET", "POST"])
@csrf_protect
@waitlist_gate("waitlist.html", plan=lambda: request.args.get("plan", "free"))
async def signup():
if config.WAITLIST_MODE and request.method == "POST":
email = (await request.form).get("email", "").strip().lower()
await capture_waitlist_email(email, intent="signup")
return await render_template("waitlist_confirmed.html", email=email)
# Normal signup flow below...
```
### Features
- **Zero dependencies** — SQLite table + worker tasks, no SaaS required
- **Idempotent** — duplicate submissions ignored (same email + intent)
- **Silent failures** — DB/Resend errors don't break user flow
- **Resend integration** — auto-adds to audience for launch campaigns
- **Intent-based** — same email can sign up for multiple intents (entrepreneur + supplier)
- **Plan-aware** — captures plan selection for pricing validation
- **IP tracking** — records IP address for spam detection
- **Email confirmations** — sends plan-specific confirmation emails via worker
### Documentation
See **[docs/WAITLIST.md](docs/WAITLIST.md)** for:
- Full architecture explanation
- Adding waitlist to new routes
- Customizing email templates
- Database schema and queries
- Testing guide
- Troubleshooting
- Migration guide (refactoring duplicated code)
- FAQ
### Turn Off
```bash
# In .env
WAITLIST_MODE=false
```
Restart → all routes revert to normal flows.
---
## 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
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` |
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