Files
padelnomics/PROJECT.md
2026-02-27 10:26:48 +01:00

20 KiB
Raw Blame History

Padelnomics — Project Tracker

Move tasks across columns as you work. Add new tasks at the top of the relevant column. Last updated: 2026-02-27 (opportunity score data quality improvements).


Done

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

Infrastructure & Deploy

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

Auth & Users

  • Magic link auth (signup + login)
  • User accounts + sessions
  • RBAC (admin role, supplier role)
  • User dashboard (stats, settings, account deletion)
  • Admin impersonation (login as any user, return to admin)

Planner / Calculator

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

Leads / Quote Flow

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

Supplier Directory & Dashboard

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

Billing & Credits

  • Paddle billing (SDK, 18 products, webhooks, checkout, subscription lifecycle)
  • Credit system (balance, ledger, atomic unlock, monthly refill on 1st of month)
  • Business Plan PDF purchase flow (Paddle one-time → webhook → async generation)
  • Boost purchases (logo, highlight, verified, card color, sticky week/month)
  • Credit pack purchases (25/50/100/250)
  • Supplier subscription tiers (Basic free / Growth €199 / Pro €499, monthly + annual)
  • 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
  • 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
  • Lead-Back Guarantee (migration 0020) — 1-click credit refund for non-responding leads (330 day window); refund_lead_guarantee() in credits.py; "Lead didn't respond" button on unlocked lead cards
  • Python supervisor (src/padelnomics/supervisor.py) + workflows.toml — replaces supervisor.sh; topological wave scheduling; croniter-based is_due(); systemd service updated
  • Proxy rotation (extract/padelnomics_extract/proxy.py) — round-robin + sticky hash-based selector via PROXY_URLS env var
  • Resend email integration (transactional: magic link, welcome, quote verify, lead forward, enquiry)
  • Auto-create Resend audiences per blueprint (waitlist, planner nurture)

Business Plan PDF Export

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

CMS / Programmatic SEO

  • Article template engine (Jinja2 Markdown + [scenario:slug:section] markers)
  • Seed script (seed_content.py) — 40 cities × EN + DE = 80 articles
  • 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
  • Per-city financial model overrides (rates, rent, utilities, permits, court config)
  • Admin CMS (template CRUD, data row management, bulk CSV upload, bulk generate, publish toggle, rebuild)
  • Admin CMS v2: HTMX filter/search/pagination, background generation, inline actions, sitemap invalidation, markdown editing
  • 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
  • URL prefix fix: articles stored without lang prefix (was causing /en/en/markets/...), all consumers updated
  • Markets hub (/<lang>/markets) — article listing with FTS + country/region filters
  • DuckDB refresh script (refresh_from_daas.py)
  • Opportunity Score integrationopportunity_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
  • 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
  • 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)
  • 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)
  • 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
  • overpass_tennis added to supervisor workflows — monthly schedule in workflows.toml; was only in combined extractor

Data Pipeline (DaaS)

  • Overpass API extractor (OSM padel courts)
  • Eurostat extractor (city demographics)
  • Playtomic unauthenticated tenant search extractor
  • SQLMesh 3-layer DuckDB pipeline (staging → foundation → serving)
  • dim_venues (OSM + Playtomic deduped), dim_cities (Eurostat population)
  • city_market_profile (market score OBT), planner_defaults (per-city calculator pre-fill)
  • DuckDB analytics reader in app lifecycle
  • 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
  • Regional Overpass tennis splitting — 10 regional bbox queries replace the single global 150K-element query that timed out; crash recovery via working.jsonl accumulation
  • init_landing_seeds.py — creates minimal seed files for both JSONL and blob formats so SQLMesh can run before real data arrives

i18n

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

Admin Panel

  • Comprehensive admin: users, tasks, leads, suppliers, CMS templates, scenarios, articles, feedback
  • Task queue management (list, retry, delete)
  • Lead funnel stats on admin dashboard
  • Email hub (/admin/emails) — sent log, inbox, compose, audiences, delivery event tracking via Resend webhooks
  • 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
  • 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
  • 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
  • Marketplace admin dashboard (/admin/marketplace) — lead funnel, credit economy, supplier engagement, live activity stream, inline feature flag toggles
  • 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)
  • Lead matching notificationsnotify_matching_suppliers task on quote verification + send_weekly_lead_digest every Monday; one-click CTA token in forward emails
  • Migration 0022status_updated_at, supplier_note, cta_token on lead_forwards; supplier respond endpoint; inline HTMX lead detail actions; extended quote form fields
  • 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
  • Outreach follow-up scheduling + activity timelinefollow_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
  • pSEO article noindexnoindex 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
  • Sitemap (both language variants, <lastmod> on all entries)
  • robots.txt
  • JSON-LD schemas (Organization, FAQPage, Article)
  • OG tags + canonical on all pages
  • German legal pages (Impressum, Datenschutz, AGB — DSGVO compliant)
  • English legal pages (GDPR, proper controller identity)
  • Cookie consent banner (functional/A/B categories, 1-year cookie)
  • Virtual office address on imprint
  • SEO/GEO admin hub — GSC + Bing + Umami sync, search/funnel/scorecard views, daily background sync
  • 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

  • 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
  • Unit test suite — auth, billing webhooks, planner calculator, PDF export, RBAC, scenarios, lead scoring

Other

  • A/B testing framework (@ab_test decorator + Umami data-tag)
  • Mobile nav (hamburger < 900px, full overlay panel)
  • Padel racket SVG logo/favicon
  • Feedback widget (HTMX POST, rate-limited)
  • Interactive ROI calculator widget on landing page (JS sliders, no server call)


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 35 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 dual score work)

🛠 Tech
Gemeinde-level pSEO article template — consumes location_opportunity_profile 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_opportunity_profile ORDER BY opportunity_score DESC

Week 12 — First Revenue

🛠 Tech 📣 Business
Email nurture sequence (3-email drip for planner users who save scenarios — Resend infra ready, just need content + scheduling) 3050 supplier outreach emails
23 founding member deals (free leads for 3 months)
"State of Padel Q1 2026" report written + published
First 3 priority SEO articles (see docs/MARKETING.md for titles)
LinkedIn: 5 posts published

Week 24 — 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 (€4999/mo, Phase 3 product)
  • Business Plan Pro subscription (€39/mo, saved scenarios + auto-updates)
  • Site Selection Reports (€499999 high-ticket, productized)
  • "State of Padel" quarterly report product (€299499, 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
  • Phase 2a — NUTS-1 regional incomenama_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
  • 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)
  • "Wirecutter for padel" affiliate site (racket reviews, gear guides)
  • "The Padel Business Report" newsletter
  • Equipment supplier affiliate partnerships (€5001,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.

  • 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, ~5001,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.