Adds a complete content generation system for producing SEO articles at scale with embedded financial scenario widgets. Includes DB schema (published_scenarios, article_templates, template_data, articles with FTS5), bulk generation pipeline with staggered publish dates, admin CRUD for templates/scenarios/articles, public markets hub with HTMX filtering, catch-all article serving from pre-rendered static HTML, sitemap integration, and 94 pytest tests covering the full stack. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Padelnomics
Plan, finance, and build your padel business.
Quick Start
./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 (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
- Fedora/RHEL:
1. Install dependencies
cd padelnomics
uv sync
2. Configure environment
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 errorsADMIN_PASSWORD=admin— admin panel login at/adminDATABASE_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
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:
# 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
uv run python -m padelnomics.app
App runs at http://localhost:5000.
6. Start the background worker (separate terminal)
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)
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
- Go to http://localhost:5000/auth/login
- Dev shortcut: http://localhost:5000/auth/dev-login?email=test@example.com — instant login, no email needed (DEBUG mode only)
- Magic links: submit any email → link prints to console (no Resend key needed)
Financial Planner
- Log in → http://localhost:5000/planner/
- Create a scenario, fill in parameters, click Calculate
- View results across tabs (Investment, Revenue, Cash Flow, Metrics)
Business Plan PDF Export
- Create at least one scenario with calculated results
- Click "Export Business Plan (PDF)" in the planner sidebar, or go to http://localhost:5000/planner/export
- 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:
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.
- Insert a test export record in the DB:
- With Paddle sandbox: complete checkout → webhook triggers worker → PDF generated → download link appears
Quote Request (Lead Submission)
- Go to http://localhost:5000/leads/quote
- Walk through the 9-step wizard
- If logged in: lead submitted directly
- If guest: verification email prints to console → click the link → lead goes live
Supplier Directory
- http://localhost:5000/directory/ — full-text search, tier-based ordering
- Click a supplier → profile page at
/directory/<slug> - Click tracking routes:
/directory/<slug>/websiteand/directory/<slug>/quoteredirect to supplier website / quote form
Supplier Signup + Dashboard
- Visit http://localhost:5000/suppliers — the "For Suppliers" landing page
- Click "Get Started" on a plan → redirects to
/suppliers/signup - Walk through the 4-step HTMX wizard (plan → boosts → credits → checkout)
- Without Paddle: the final checkout step will fail. To simulate a
signed-up supplier, insert test data:
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') " - Log in as that user → http://localhost:5000/suppliers/dashboard
- Browse tabs: Overview, Lead Feed, My Listing, Boost & Upsells
- Lead Feed: unlock leads (costs credits), filter by heat/country/timeline
- My Listing: edit profile fields, upload logo
- Boosts: purchase boosts and credit packs (needs Paddle for actual checkout)
Admin Panel
- Go to http://localhost:5000/admin
- Log in with password:
admin(default in dev) - Dashboard: user stats, lead funnel, supplier stats, task queue health
- Leads (
/admin/leads): filter by status/heat/country, view detail, update status, forward to supplier - Suppliers (
/admin/suppliers): filter by tier/country/name, view detail with credit ledger + boost history, adjust credits, change tier - Feedback (
/admin/feedback): view all feedback submissions - Users (
/admin/users): search, impersonate users - Tasks (
/admin/tasks): view/retry/delete background tasks
Feedback Widget
- Click the "Feedback" button in the top navbar (visible on every page)
- Type a message → Send
- View submissions at http://localhost:5000/admin/feedback
Seeding Test Data
The database starts empty. To populate it with test data for development:
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.
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 usehx-get. - Paddle.js overlay checkout: server returns JSON
{items, customData, settings}, frontend callsPaddle.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)
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:
- Add the matching public key to
~/.ssh/authorized_keysfor the deploy user - Clone the repo to
/opt/padelnomics - Create
.envfrompadelnomics/.env.examplewith production values chmod +x deploy.sh && ./deploy.shfor the first deploy- Point reverse proxy to port 5000