# Padelnomics — Project Tracker

> Move tasks across columns as you work. Add new tasks at the top of the relevant column.
> Last updated: 2026-02-28 (Affiliate programs management — centralised retailer config + URL template assembly).

---

## Done ✅

> Confirmed by full code audit (2026-02-22). More is built than the original strategy docs assumed.

### Infrastructure & Deploy
- [x] UV workspace monorepo structure (web/, transform/, extract/ members)
- [x] Docker + docker-compose production deploy
- [x] Litestream R2 backup (1-year retention, auto-restore on startup)
- [x] CI pipeline (GitLab, health check gated deploys)
- [x] SOPS + age encrypted secrets (`.env.dev.sops` / `.env.prod.sops`; `deploy.sh` auto-decrypts; `setup_server.sh` installs sops+age)
- [x] Pre-migration DB backup + auto-restore on failed deploy
- [x] Nginx router config

### Auth & Users
- [x] Magic link auth (signup + login)
- [x] User accounts + sessions
- [x] RBAC (admin role, supplier role)
- [x] User dashboard (stats, settings, account deletion)
- [x] Admin impersonation (login as any user, return to admin)

### Planner / Calculator
- [x] Full 60-var financial planner — **fully open, no login required**
- [x] HTMX refactor (server-rendered tabs, ~200 line JS)
- [x] Planner i18n (EN + DE, all strings translated)
- [x] Currency formatting by country (€ / £ / $)
- [x] Market data endpoint (`/planner/api/market-data`) — city-specific defaults from DuckDB
- [x] Scenario save/load (requires login)
- [x] "Get Supplier Quotes" button linking to quote wizard

### Leads / Quote Flow
- [x] 9-step quote wizard — no login required, heat scoring, email verification
- [x] Lead heat scoring (hot 35 credits / warm 20 / cool 8)
- [x] Guest email verification (magic link sent before lead activates)
- [x] Admin lead management (list, detail, status transitions, manual forwarding)
- [x] Supplier credit-based lead unlock (atomic credit spend + email notification to both sides)
- [x] i18n for full quote flow

### Supplier Directory & Dashboard
- [x] Directory — fully public, search/FTS/filter/pagination/detail/enquiry
- [x] Tier-based ordering (sticky > pro > growth > basic > free)
- [x] Boost badges on directory cards
- [x] Supplier signup wizard (4-step, plan selection, Paddle checkout, waitlist-gatable)
- [x] Supplier dashboard (4 tabs: overview, leads feed, listing editor, boosts)
- [x] Listing editor with live HTMX preview, logo/cover upload
- [x] Boost purchase from dashboard (Paddle checkout)
- [x] Credit pack purchase from dashboard
- [x] i18n for directory + supplier pages

### Billing & Credits
- [x] Paddle billing (SDK, 18 products, webhooks, checkout, subscription lifecycle)
- [x] Credit system (balance, ledger, atomic unlock, monthly refill on 1st of month)
- [x] Business Plan PDF purchase flow (Paddle one-time → webhook → async generation)
- [x] Boost purchases (logo, highlight, verified, card color, sticky week/month)
- [x] Credit pack purchases (25/50/100/250)
- [x] Supplier subscription tiers (Basic free / Growth €199 / Pro €499, monthly + annual)
- [x] **Stripe payment provider** — env-var toggle (`PAYMENT_PROVIDER=paddle|stripe`), Stripe Checkout Sessions + Billing Portal + webhook handling, `payment_products` table generalizes `paddle_products`, dual-path JS templates, `billing/paddle.py` + `billing/stripe.py` dispatch pattern, `setup_stripe.py` product creation script
- [x] **Feature flags** (DB-backed, migration 0019) — `is_flag_enabled()` + `feature_gate()` decorator replace `WAITLIST_MODE`; 5 flags (markets, payments, planner_export, supplier_signup, lead_unlock); admin UI at `/admin/flags` with toggle
- [x] **Pricing overhaul** — Basic free (no Paddle sub), card color €59, BP PDF €149; supplier page restructured value-first (why → guarantee → leads → social proof → pricing); all CTAs "Get Started Free"; static ROI line; credits-only callout
- [x] **Lead-Back Guarantee** (migration 0020) — 1-click credit refund for non-responding leads (3–30 day window); `refund_lead_guarantee()` in credits.py; "Lead didn't respond" button on unlocked lead cards
- [x] **Python supervisor** (`src/padelnomics/supervisor.py`) + `workflows.toml` — replaces `supervisor.sh`; topological wave scheduling; croniter-based `is_due()`; systemd service updated
- [x] **Proxy rotation** (`extract/padelnomics_extract/proxy.py`) — round-robin + sticky hash-based selector via `PROXY_URLS` env var
- [x] Resend email integration (transactional: magic link, welcome, quote verify, lead forward, enquiry)
- [x] Auto-create Resend audiences per blueprint (waitlist, planner nurture)

### Business Plan PDF Export
- [x] Full WeasyPrint PDF pipeline (not just a CTA — actually generates PDFs)
- [x] Paddle checkout for PDF purchase
- [x] Async generation via worker queue, email on completion
- [x] PDF content: executive summary, CAPEX, OPEX, 5-year P&L, financing, IRR/MOIC/payback/DSCR
- [x] EN + DE PDF localization

### CMS / Programmatic SEO
- [x] Article template engine (Jinja2 Markdown + `[scenario:slug:section]` markers)
- [x] Seed script (`seed_content.py`) — 40 cities × EN + DE = 80 articles
- [x] City coverage: DE (8), US (6), UK (4), ES (5), FR (3), IT (2), NL, AT, CH, SE, PT (2), BE, AE, AU (2), IE
- [x] Per-city financial model overrides (rates, rent, utilities, permits, court config)
- [x] Admin CMS (template CRUD, data row management, bulk CSV upload, bulk generate, publish toggle, rebuild)
- [x] Admin CMS v2: HTMX filter/search/pagination, background generation, inline actions, sitemap invalidation, markdown editing
- [x] pSEO template improvements: bilingual DE+EN (all 3 templates), expanded content depth (~1500 words), cross-template links, scenario cross-references, FAQPage schema fix, 2 extra FAQs per template, second CTAs
- [x] URL prefix fix: articles stored without lang prefix (was causing `/en/en/markets/...`), all consumers updated
- [x] Markets hub (`/<lang>/markets`) — article listing with FTS + country/region filters
- [x] DuckDB refresh script (`refresh_from_daas.py`)
- [x] **Opportunity Score integration** — `opportunity_score` (Marktpotenzial) wired into city + country templates; `geoname_id` threaded through SQL chain (dim_cities → city_market_profile → pseo_city_costs_de); 71.4% city match rate; stats strip, intro paragraphs, market tables, and FAQ updated in both DE + EN
- [x] **Market Score v3 recalibration** — fixes ranking inversion (Germany 1/100k was outscoring Spain 36/100k); log-scaled density + count gate replaces linear formula; saturation discount removed; template thresholds updated across all 3 pSEO templates; verified: Málaga 70.1, Barcelona 67.4, Madrid 66.9, Amsterdam 58.4, Bernau 43.9 (was 92.7), Berlin 42.2, London 44.1
- [x] **Opportunity Score v2** — supply gap ceiling raised 4→8/100k (gentler gradient, accounts for 87% data undercount); formula documentation added (DuckDB LEAST NULL behaviour, income saturation, tennis data gap)
- [x] **Opportunity Score v2 — income ceiling fix** — PPS normalisation `/200.0` → `/35000.0`; economic power component now differentiates countries (DE 13.2, ES 10.7, SE 14.3 pts; was 20.0 everywhere)
- [x] **dim_cities population coverage 70.5% → 98.5%** — GeoNames spatial fallback CTE (ST_Distance_Sphere, 0.14° bbox) resolves localization mismatches (Wien→Vienna 1.69M, Milano→Milan 1.37M); population cascade: Eurostat > Census > ONS > GeoNames string > GeoNames spatial > 0
- [x] **overpass_tennis added to supervisor workflows** — monthly schedule in `workflows.toml`; was only in combined extractor
- [x] **Phase 2a — NUTS-1 regional income** — `eurostat.py` adds `nama_10r_2hhinc` dataset + URL filter params; `stg_regional_income.sql` new staging model (NUTS-1 codes, `EL→GR`/`UK→GB` normalisation); `dim_locations.sql` wires German Bundesland income via 16-row `admin1_to_nuts1` VALUES CTE; verified income spread Bayern > Hamburg > Berlin > Sachsen
- [x] **Phase 2b — EU NUTS-2 spatial join + US state income** — all EU-27+EFTA+UK locations auto-resolve to NUTS-2 via `ST_Contains` on GISCO boundary polygons; Germany now uses 38 Regierungsbezirke; US state income from Census ACS with PPS normalisation (`income / 80610 × 30000`); replaces brittle admin1 mapping CTE with zero-config spatial join; new files: `download_gisco_nuts.py`, `stg_nuts2_boundaries.sql`, `census_usa_income.py`, `stg_income_usa.sql`

### Data Pipeline (DaaS)
- [x] Overpass API extractor (OSM padel courts)
- [x] Eurostat extractor (city demographics)
- [x] Playtomic unauthenticated tenant search extractor
- [x] SQLMesh 3-layer DuckDB pipeline (staging → foundation → serving)
- [x] `dim_venues` (OSM + Playtomic deduped), `dim_cities` (Eurostat population)
- [x] `city_market_profile` (market score OBT), `planner_defaults` (per-city calculator pre-fill)
- [x] DuckDB analytics reader in app lifecycle
- [x] **JSONL streaming landing format** — extractors write `.jsonl.gz` (one record per line); constant-memory compression via `compress_jsonl_atomic()`; eliminates `maximum_object_size` workarounds; all modified staging models use UNION ALL transition to support both formats
- [x] **Regional Overpass tennis splitting** — 10 regional bbox queries replace the single global 150K-element query that timed out; crash recovery via `working.jsonl` accumulation
- [x] **`init_landing_seeds.py`** — creates minimal seed files for both JSONL and blob formats so SQLMesh can run before real data arrives

### i18n
- [x] Full i18n across entire app (EN + DE)
- [x] URL prefixes (`/en/`, `/de/`) on all public blueprints
- [x] Language detection (cookie + Accept-Language header)
- [x] `tformat` Jinja2 filter for parameterized translations
- [x] German copy: informal "Du/Dein" throughout
- [x] hreflang tags + `x-default`

### Admin Panel
- [x] Comprehensive admin: users, tasks, leads, suppliers, CMS templates, scenarios, articles, feedback
- [x] Task queue management (list, retry, delete)
- [x] Lead funnel stats on admin dashboard
- [x] Email hub (`/admin/emails`) — sent log, inbox, compose, audiences, delivery event tracking via Resend webhooks
- [x] **Email template system** — 11 transactional emails as Jinja2 templates (`emails/*.html`); standalone `render_email_template()` renderer works in worker + admin; `_base.html` + `_macros.html` shared shell; `EMAIL_TEMPLATE_REGISTRY` with sample data for gallery previews; `_email_wrap()` / `_email_button()` helpers removed
- [x] **Admin email gallery** (`/admin/emails/gallery`) — card grid of all templates, EN/DE preview in sandboxed iframe, "View in sent log" cross-link; compose page now has HTMX live preview pane
- [x] **pSEO Engine tab** (`/admin/pseo`) — content gap detection, data freshness signals, article health checks (hreflang orphans, missing build files, broken scenario refs), generation job monitoring with live progress bars
- [x] **Marketplace admin dashboard** (`/admin/marketplace`) — lead funnel, credit economy, supplier engagement, live activity stream, inline feature flag toggles
- [x] **Pipeline Console** (`/admin/pipeline`) — 4-tab operational dashboard: extraction status grid per source, filterable run history with stale-run management ("Mark Failed"), data catalog with column schema + 10-row sample, SQL query editor with dark-themed textarea + schema sidebar + read-only security sandboxing (keyword blocklist, 10s timeout, 1,000-row cap)
- [x] **Lead matching notifications** — `notify_matching_suppliers` task on quote verification + `send_weekly_lead_digest` every Monday; one-click CTA token in forward emails
- [x] **Migration 0022** — `status_updated_at`, `supplier_note`, `cta_token` on `lead_forwards`; supplier respond endpoint; inline HTMX lead detail actions; extended quote form fields
- [x] **Outreach pipeline** (`/admin/outreach`) — cold B2B supplier outreach with separate sending domain (`hello.padelnomics.io`), 6-stage pipeline cards, HTMX inline status + note editing, CSV import, bulk add-to-pipeline from supplier list, compose integration (auto-updates pipeline on send); migration 0024 adds 4 outreach columns to suppliers; 44 tests
- [x] **Outreach follow-up scheduling + activity timeline** — `follow_up_at` date column on suppliers (migration 0025), HTMX date picker on outreach rows, amber "follow-ups due" banner with `?follow_up=due` filter, activity timeline on supplier detail merging sent + received emails by contact email; 29 tests
- [x] **pSEO article noindex** — `noindex` column on articles (migration 0025), `NOINDEX_THRESHOLDS` per-template lambdas in `content/__init__.py`, robots meta tag in `article_detail.html`, sitemap exclusion, pSEO dashboard count card + article row badge; 20 tests
- [x] **group_key static article grouping** — migration 0020 adds `group_key TEXT` column; `_sync_static_articles()` auto-upserts `data/content/articles/*.md` on admin page load; `_get_article_list_grouped()` groups by `COALESCE(group_key, url_path)` so EN/DE static cornerstones pair into one row
- [x] **Email-gated report PDF** — `reports/` blueprint with email capture gate + PDF download; premium WeasyPrint PDF (full-bleed navy cover, Padelnomics wordmark watermark, gold/teal accents); `make report-pdf` target; EN + DE i18n (26 keys, native German); state-of-padel report moved to `data/content/reports/`
- [x] **Affiliate product system** — "Wirecutter for padel" editorial gear cards embedded in articles via `[product:slug]` / `[product-group:category]` markers, baked at build time; `/go/<slug>` click-tracking redirect (302, GDPR daily-rotated IP hash, rate-limited); admin CRUD with live preview, HTMX filter/search, status toggle; click stats dashboard (pure CSS charts); 10 German equipment review article scaffolds; 26 tests
- [x] **Affiliate programs management** — `affiliate_programs` table centralises retailer configs (URL template, tracking tag, commission %); product form uses program dropdown + product identifier (ASIN etc.); `build_affiliate_url()` assembles at redirect time; legacy baked-URL products still work; admin CRUD (delete blocked if products reference program); Amazon OneLink for multi-marketplace; article frontmatter preview bug fixed; 41 tests

### SEO & Legal
- [x] Sitemap (both language variants, `<lastmod>` on all entries)
- [x] robots.txt
- [x] JSON-LD schemas (Organization, FAQPage, Article)
- [x] OG tags + canonical on all pages
- [x] German legal pages (Impressum, Datenschutz, AGB — DSGVO compliant)
- [x] English legal pages (GDPR, proper controller identity)
- [x] Cookie consent banner (functional/A/B categories, 1-year cookie)
- [x] Virtual office address on imprint
- [x] SEO/GEO admin hub — GSC + Bing + Umami sync, search/funnel/scorecard views, daily background sync
- [x] Market Score methodology page (`/{lang}/market-score`) — Zillow-style explanation of the padelnomics Market Score; EN + DE; JSON-LD (WebPage + FAQPage + BreadcrumbList); hub-and-spoke internal linking from all article templates

### Testing
- [x] Playwright visual/E2E test suite — 77 tests across 3 files (visual, e2e flows, quote wizard); single session-scoped server + browser; mocked emails + waitlist mode; ~59s runtime
- [x] Unit test suite — auth, billing webhooks, planner calculator, PDF export, RBAC, scenarios, lead scoring

### Other
- [x] A/B testing framework (`@ab_test` decorator + Umami `data-tag`)
- [x] Mobile nav (hamburger < 900px, full overlay panel)
- [x] Padel racket SVG logo/favicon
- [x] Feedback widget (HTMX POST, rate-limited)
- [x] Interactive ROI calculator widget on landing page (JS sliders, no server call)
- [x] **CRO overhaul — homepage + supplier landing pages** — JTBD-driven copy rewrite (feature → outcome framing), proof strip, struggling-moments sections, "Why Padelnomics" comparison, rewritten FAQ, conditional supplier stats, data-backed proof points, tier-specific CTAs (EN + DE)
- [x] **Single-score simplification** — consolidated Market Score + Opportunity Score into one public "Padelnomics Score" (`opportunity_score`). Single-color map markers, unified methodology page at `/padelnomics-score`, i18n'd map tooltips, updated pSEO templates + business plan. Non-Latin city name filter in pipeline.

---

---

## Next Up 📋

> Two independent tracks — pick from either at any time, no sequencing between them.
> Tech tasks can be shipped in hours. Business tasks depend on other people and run in parallel.

### Go-Live (config, not code)

| 🛠 Tech | 📣 Business |
|--------|------------|
| Paddle: set production env vars + run `setup_paddle` against prod | First 3–5 supplier outreach emails |
| Publish SEO articles: run `seed_content --generate` on prod (or trigger from admin) | First LinkedIn post |
| Wipe 5 test suppliers (`example.com` entries from `seed_dev_data.py`) | |
| Verify Resend production API key — test magic link email | |
| Submit sitemap to Google Search Console | Set up Google Search Console + Bing Webmaster Tools (SEO hub ready — just add env vars) |
| Verify Litestream R2 backup running on prod | |

### Gemeinde-level pSEO (follow-up from single-score simplification)

| 🛠 Tech |
|--------|
| Gemeinde-level pSEO article template — consumes `location_profiles` data, targets "Padel in [Ort]" + "Padel bauen in [Ort]" queries (zero SERP competition confirmed) |
| "Top 50 underserved locations" ranking page — high-value SEO content, fully programmatic from `location_profiles` ORDER BY opportunity_score DESC |

### Week 1–2 — First Revenue

| 🛠 Tech | 📣 Business |
|--------|------------|
| Email nurture sequence (3-email drip for planner users who save scenarios — Resend infra ready, just need content + scheduling) | 30–50 supplier outreach emails |
| | 2–3 founding member deals (free leads for 3 months) |
| | "State of Padel Q1 2026" report — landing page live at `/en/reports/q1-2026`, email gate captures leads, PDF built with `make report-pdf` |
| | First 3 priority SEO articles (see `docs/MARKETING.md` for titles) |
| | LinkedIn: 5 posts published |

### Week 2–4 — Market Map

| 🛠 Tech | 📣 Business |
|--------|------------|
| Market map UI (geographic visualization over DuckDB city data — no map exists yet) | Follow up on founding member outreach |
| | More SEO articles |

### Month 2 — Market Intelligence

| 🛠 Tech | 📣 Business |
|--------|------------|
| Market Intelligence Dashboard (city analytics, occupancy estimates, demand map) | Explorer tier (€79/mo) promoted to email list |
| Explorer tier paywall (€79/mo subscription gate) | Email drip running |

---

## Backlog 🗂️

> Validated ideas not yet scheduled. Pick up when capacity allows.

### Product
- [ ] Market Intelligence Pro tier (€149/mo — hall-level data, competitor tracking, historical)
- [ ] Location Scorer (mechanical turk first: form → manual PDF delivery → automate if demand)
- [ ] Operational analytics for running venues (€49–99/mo, Phase 3 product)
- [ ] Business Plan Pro subscription (€39/mo, saved scenarios + auto-updates)
- [ ] Site Selection Reports (€499–999 high-ticket, productized)
- [ ] "State of Padel" quarterly report product (€299–499, gated)
- [ ] Enterprise / API tier (custom pricing)
- [ ] Padel Hall Accelerator (€999 — report + call + supplier intros)

### Data & Intelligence
- [ ] Sports centre Overpass extract (`leisure=sports_centre`) — additional market signal for `dim_locations`
- [x] **Phase 2a — NUTS-1 regional income** — `nama_10r_2hhinc` extractor + `stg_regional_income` staging model + `admin1_to_nuts1` VALUES CTE in `dim_locations`; all 16 German Bundesländer mapped; Bayern ~29K vs Sachsen ~19K PPS differentiation; country-level fallback for ES/FR/IT/etc.
- [ ] Phase 2b — city-level income (NUTS-3 granularity) if NUTS-1 proves insufficient
- [ ] Interactive opportunity map / explorer in web app (map UI over `location_opportunity_profile` — bounding box queries via ST_Distance_Sphere)
- [ ] Multi-source data aggregation (add booking platforms beyond Playtomic)
- [ ] Google Maps signals (reviews, ratings)
- [ ] Weather + demographic overlays
- [ ] Voluntary data sharing from operating venues (benchmarking network effects)
- [ ] ML/forecasting layer (demand forecasting, pricing optimization)
- [ ] Scraping risk mitigation: rotate sources, voluntary sharing fallback

### Bugs / Tech Debt
- [ ] Resend audiences: two segments both using "waitlist-auth" — review audience/segment model and fix duplication
- [ ] Transactional emails not all translated to German — some emails still sent in English regardless of user language
- [x] ~~Resend inbound emails enabled~~ — integrated: webhook handler + admin inbox with reply (done in email hub)
- [ ] Extraction: Playtomic API only returns ~20 venues per bbox — investigate smaller/targeted bboxes

### Marketing & Content
- [ ] LinkedIn presence (ongoing — founder posts, thought leadership)
- [ ] "The Padel Business Report" newsletter
- [ ] Equipment supplier affiliate partnerships (€500–1,000/lead or 5%)
- [ ] Padel podcasts (guest appearances)
- [ ] Sports business media outreach
- [ ] National padel associations (DTB, LTA — co-distribution)
- [ ] Franchise partnerships (market data / leads)
- [ ] Lender distribution (banks recommending Padelnomics plans)

### Geographic Expansion
- [ ] Austria + Switzerland (language done, cities seeded — just outreach + supplier onboarding)
- [ ] France (cities seeded in CMS)
- [ ] Italy, Netherlands, Sweden (cities seeded)
- [ ] UAE / Middle East (cities seeded)
- [ ] Pan-European supplier directory

---

## Human Tasks 🧑

> Tasks that require manual setup, external accounts, or human judgement.

- [x] Set up ntfy.sh for supervisor notifications — topic `gWMeiHxj8ZqLbbqT`, token in `.env.prod.sops`

---

## Decisions Log

| Date | Decision | Rationale |
|------|----------|-----------|
| 2026-02-22 | Two-sided marketplace framing (Side A = aspiring owners, Side B = suppliers) | Suppliers are the main revenue engine; calling them "secondary" was wrong |
| 2026-02-22 | Tennis/sports club owner as beachhead | Faster decision cycle, talk to each other, ~500–1,000 targets, small enough to dominate |
| 2026-02-22 | Credit system over pay-per-lead blast | Suppliers self-select → higher quality perception; scales without manual intervention |
| 2026-02-22 | No soft email gate on planner | Planner already captures emails at natural points (scenario save → login, quote wizard step 9). Gate would add friction without meaningful list value. Revisit if data shows a gap. |
| 2026-02-22 | Wipe test suppliers before launch | 5 `example.com` entries from seed_dev_data.py — empty directory with "Be the first" CTA is better than obviously fake data |
| 2026-02-24 | Split market score into two branded scores | Marktreife-Score (existing market maturity, cities with ≥1 venue) vs Marktpotenzial-Score (greenfield opportunity, all GeoNames locations globally). SERP analysis confirmed zero competition for hyperlocal Gemeinde-level market intelligence pages. |
| 2026-02-26 | Basic tier free, no Paddle subscription | Simplest onboarding — signup without payment flow; MARKETING.md already said free, code said €39 |
| 2026-02-26 | Lead-Back Guarantee: supplier-initiated, credits back (not cash) | Risk reduction beats ROI projection (competitor research: #1 complaint is paying for silent leads). Credits-only keeps cash while removing the psychological barrier. |
| 2026-02-26 | Static ROI line, not interactive calculator | No lead marketplace uses inline ROI calcs; B2B SaaS trend away from them (Zendesk, Intercom, Gong all removed theirs). One bold number grounded in research beats a widget. |
