Extract sitemap generation to sitemap.py with xhtml:link hreflang
alternates (en/de/x-default) on every URL entry. Add 1-hour in-memory
TTL cache with Cache-Control header. Include supplier pages in both
languages (were EN-only). Drop misleading "today" lastmod from static
pages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add /scenarios/<id>/pdf admin route for direct PDF generation via WeasyPrint.
Fix plan.html Jinja template: .items → ['items'] to avoid dict method collision.
Add scenario fixture in conftest.py and comprehensive test suite for business
plan sections, PDF generation, worker handler, and export routes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move historical docs from docs/ and .claude/ to research/. Add superseded
notice to research/PLAN.md. Add CHANGELOG entries for previous fixes.
New: PROJECT.md (task tracker), docs/MARKETING.md (marketing strategy).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eurostat JSON-stat format (4-7 dimension sparse dict with 583K values)
causes DuckDB OOM — pre-process in extractor to flat records.
Also fix dim_cities unused CTE bug and playtomic venue lat/lon path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Playtomic tenants API recycles results past its internal limit —
stop after 3 consecutive pages with zero new unique IDs.
Calculator tests: replace hardcoded default values (6 courts, specific
sqm/capex) with DEFAULTS references so tests don't break when
defaults change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove raw/ layer — staging models now read landing JSON directly.
Rename all model schemas from padelnomics.* to staging.*/foundation.*/serving.*.
Web app queries updated to serving.planner_defaults via SERVING_DUCKDB_PATH.
Supervisor gets daily sleep interval between pipeline runs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split monolithic execute.py into per-source modules with separate CLI
entry points. Each extractor now uses the framework from utils.py:
- SQLite state tracking (start_run / end_run per extractor)
- Proper logging (replace print() with logger)
- Atomic gzip writes (write_gzip_atomic)
- Connection pooling (niquests.Session)
- Bounded pagination (MAX_PAGES_PER_BBOX = 500)
New entry points:
extract — run all 4 extractors sequentially
extract-overpass — OSM padel courts
extract-eurostat — city demographics (etag dedup)
extract-playtomic-tenants — venue listings
extract-playtomic-availability — booking slots + pricing (NEW)
The availability extractor reads tenant IDs from the latest tenants.json.gz,
queries next-day slots for each venue, and stores daily consolidated snapshots.
Supports resumability via cursor and retry with backoff.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pulls in template changes: export_serving.py for atomic DuckDB swap,
supervisor export step, SQLMesh glob macro, server provisioning script,
imprint template, and formatting improvements.
Template scaffold SQL models excluded (padelnomics has real models).
Web app routes/analytics unchanged (padelnomics-specific customizations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix quote sidebar z-index (behind tab nav) and align top with tab content
- Fix bottom nav sticky positioning (move outside .planner-app)
- Fix wizard footer fixed positioning and width on mobile
- Fix bottom nav active state visibility (hardcoded colors outside CSS var scope)
- Fix country pills overflow with flex-wrap
- Fix tooltip clipping in collapsible sections
- Hide feedback button on mobile planner
- Add cache busting for static assets (_ASSET_VERSION)
- Convert export CTA to full clickable button
- Add CAPEX table section header, sort doughnut chart by size
- Cap data tables at 640px centered, horizontal scroll for wide tables
- Replace CAPEX jargon with plain German (Gesamtinvestition, Kostenaufschlüsselung)
- Update FAQ/landing copy to global language (not Europe-specific)
- Update default court sizes to realistic values (court + walkway only)
- Add missing planner_export_inline translation key (en + de)
- Revert wizard nav to client-side (HTMX broke on lang-prefixed routes)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sequential IDs in /planner/export/<id> and /leads/<id>/unlock leaked
business volume (e.g. export_id=47 reveals ~47 PDFs sold). Replace with
22-char URL-safe tokens that carry no countable information.
- Migration 0017: adds `token TEXT` to business_plan_exports and
lead_requests, backfills existing rows with secrets.token_urlsafe(16),
creates unique indexes for fast lookups
- billing/routes.py: INSERT into business_plan_exports includes token
- leads/routes.py: INSERT into lead_requests includes token; enqueue
payload includes lead_token; verify_quote() looks up by token
- planner/routes.py: /export/<token> route (was /export/<int:export_id>)
- suppliers/routes.py: /leads/<token>/unlock (was /leads/<int:lead_id>)
- worker.py: email links use token for both export and verify URLs
- Templates: url_for() calls use token= param
- test_phase0.py: _submit_guest_quote() returns (lead_id, auth_token,
lead_token); verify URL tests use opaque lead token
Integer PKs unchanged; admin routes unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sync template from 29ac25b → v0.9.0 (29 template commits). Due to
template's _subdirectory migration, new files were manually rendered
rather than auto-merged by copier.
New files:
- .claude/CLAUDE.md + coding_philosophy.md (agent instructions)
- extract utils.py: SQLite state tracking for extraction runs
- extract/transform READMEs: architecture & pattern documentation
- infra/supervisor: systemd service + orchestration script
- Per-layer model READMEs (raw, staging, foundation, serving)
Also fixes copier-answers.yml (adds 4 feature toggles, removes stale
payment_provider key) and scopes CLAUDE.md gitignore to root only.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add sticky bottom tab bar on mobile (<768px) with 5 tabs (Setup, CAPEX, P&L, Cash, Returns)
- Merge Metrics tab into Returns as collapsible <details> section
- Wrap wizard input groups in collapsible <details> elements to reduce scroll fatigue
- Add contextual CTA bar above bottom nav showing CAPEX estimate + "Get Quotes" button
- Simplify desktop sidebar CTA (remove checklist, add text export link)
- Convert loadScenario/resetToDefaults/saveScenario from client-side JS to HTMX/navigation
- Convert wizard nav buttons to server-rendered partial (removes i18n from JS)
- Remove 3 unused window.__*__ globals, reduce planner.js from 208 to 131 lines
- Increase slider thumb size to 20px on mobile for better touch targets
- Add bottom padding to main content for bottom nav clearance
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add enable_daas, enable_cms, enable_directory, enable_i18n (all true)
and remove stale payment_provider key before running copier update.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
nginx -t resolves upstream hostnames — if the config points to a stopped
slot from a previous failed deploy, the health check fails and the router
stays unhealthy indefinitely, blocking all future deploys.
Before up -d --wait, write the router config to point to the CURRENT live
slot (which is still running) and restart the router. This clears the
stale unhealthy state. After the new slot passes health checks, switch
the router config to the new slot and reload.
Also extracted _write_router_conf() to avoid duplicating the nginx config
template.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker compose requires --profile to access profiled services even for
the logs command. Without it, blue-app logs were empty in the failure
dump, hiding the actual crash reason.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 100-line combined log dump was entirely filled by litestream R2
errors, hiding the actual blue-app crash output. Now dumps blue-app
(60 lines), router (10 lines), and litestream (10 lines) separately.
Revert litestream image tag to latest — the R2 errors were caused by
misconfigured endpoint/bucket CI variables, not a litestream version
bug. The v0.5.8 tag may not exist on Docker Hub (tags omit 'v' prefix).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
latest tag may resolve to an older version that treats Cloudflare R2's
NoSuchKey response on empty-prefix ListObjectsV2 as a hard error instead
of an empty list, causing the replica sync to stall on first deployment.
v0.5.8 is the current stable release (2026-02-12).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
analytics.py imports duckdb at the top level. The Dockerfile runs
`uv sync --package padelnomics` which only installs padelnomics deps —
duckdb was missing, so hypercorn failed to import padelnomics.app
entirely and never bound to port 5000. The health check timed out and
the container was marked unhealthy. Tests passed because uv sync in CI
syncs all workspace members (including transform/ which has duckdb).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Router had no profile so it was always included in `up -d --wait`.
Writing the new target's config BEFORE the wait caused the router to become
unhealthy if the new slot failed — leaving it in a broken state for the next
deploy attempt.
Now: router keeps its old config (pointing to the still-running old slot)
during the health check wait, so it stays healthy throughout. Config is only
written and nginx -s reload triggered after the new slot passes its health
check. This is the correct blue-green pattern.
Also add `retries: 3` and `start_period: 10s` to the router health check
for resilience against transient startup failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- test_planner_calculate_htmx: click capex tab to reveal #tab-content
(it starts display:none and only shows after tab switch)
- test_planner_quote_sidebar_visible_wide: use browser.new_page() instead
of page.context.new_page() (default contexts don't support new_page)
- test_login/signup/quote_step1_loads: add .first to avoid strict mode
violation from the feedback popover form
- test_language_switcher_en_to_de: verify footer link href + navigate
directly instead of click (avoids off-screen element timing issues)
- test_landing_nav_no_overlap: filter display:none elements (zero-width
bounding rect) so mobile-only nav div doesn't skew overlap check
- test_quote_wizard_*: replace schema.sql (doesn't exist) with migrate()
approach matching test_visual.py and test_e2e_flows.py; fix URL from
/leads/quote to /en/leads/quote; use label click for display:none pill
radios; add missing required fields for steps 6 (financing_status +
decision_process) and 8 (services_needed); add contact_phone to step 9
All 1018 unit tests + 61 e2e tests now pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Article slug is template_slug + city_slug ("city-cost-miami"),
not just the city slug ("miami").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove `cd padelnomics` (subdir no longer exists)
- pytest and ruff now target web/tests/ and web/src/
- Deploy stage writes .env to repo root, not padelnomics/ subdir
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix dev_run.sh and dev_setup.sh cd path (../.. after repo flatten)
- Quote form: re-render step 9 inline on validation error instead of
flash + redirect to step 1; phone/email errors now show field-level
- Supplier FAQ: move differentiation Q to top, fix Q10 email to
hello@ (was leads@), rename Q1 to "How do I get listed?"
- Replace Innenhalle → Indoorhalle throughout DE locale and seed script
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
build context, env_file, and litestream volume mount all pointed at
./padelnomics/ which no longer exists after the flatten.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Adds sqlmesh_padelnomics UV workspace member at transform/sqlmesh_padelnomics/.
DuckDB gateway, LANDING_DIR variable, @daily cron on all models.
Raw layer (reads landing zone gzip JSON):
raw_overpass_courts — OSM padel court elements (nodes with lat/lon/tags)
raw_playtomic_tenants — Playtomic venue records (tenant_id, location, name)
raw_eurostat_population — Eurostat urb_cpop1 city population (unpivoted)
Staging layer (typed, deduped, country-resolved):
stg_padel_courts — OSM nodes only, ~100m bbox country approximation
stg_playtomic_venues — deduplicated Playtomic venues
stg_population — city population by year with integer types
Foundation layer:
dim_venues — deduped union of OSM + Playtomic (~100m grid)
dim_cities — Eurostat cities with population + venue counts
Serving layer (consumed by web app and SEO generation):
city_market_profile — OBT: market score, venue density, population per city
planner_defaults — per-city calculator pre-fill values with country median
fallbacks and competitive-pressure rate adjustments
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds padelnomics_extract UV workspace member at extract/padelnomics_extract/.
Implements three extractors in execute.py:
- extract_overpass(): global OverpassQL query for sport=padel OSM features
- extract_eurostat(): urb_cpop1 (city population) + ilc_di03 (NUTS2 income), etag-dedup
- extract_playtomic_tenants(): unauthenticated tenant search across 4 market bboxes,
paginated, deduplicated by tenant_id, throttled at 1 req/2s
Landing zone at LANDING_DIR (default data/landing) with per-source subdirectories.
Entry point: `extract` script calls extract_dataset() for all three in sequence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restructures padelnomics to match the quart_saas_boilerplate template:
web/ workspace member holds source, tests, and scripts.
Dockerfile, Makefile updated to web/src/ paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Planner now renders currency symbol and thousands-separator style based
on the selected country:
UK → £450,000 (pound, comma thousands)
US → $450,000 (dollar, comma thousands)
EU + SE → €450.000 (euro, dot thousands — unchanged)
Implementation:
- COUNTRY_CURRENCY mapping + CURRENCY_DEFAULT added to calculator.py;
5 info-string annotations updated to use derived sym variable
- _fmt_currency, _fmt_k, _fmt_n Jinja2 filters now read g.currency_sym
and g.currency_eu_style (safe EUR fallback via getattr)
- planner index + calculate routes set g.currency_* and pass
currency_sym to template context before render
- 16 slider label locale keys updated: (€) → ({currency}) in both
en.json and de.json; slider macro applies tformat(currency=…)
- businessplan.py: _fmt_eur renamed to _fmt_cur(n, sym, eu_style);
get_plan_sections derives currency from state and binds a fmt lambda;
capex/opex items gain formatted_amount field
- plan.html: inline € replaced with {{ item.formatted_amount }}
1017 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Combines master's get_translations() injection with the worktree's
lang parameter so German articles render translated scenario card labels.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add scripts/seed_content.py: inserts EN + DE article templates and
18 cities × 2 language data rows; run with --generate to produce 36
pre-built SEO articles (Germany 8, USA 6, UK 4 cities) each with
city-specific financial model overrides for unique content per article
- Fix bake_scenario_cards() to accept lang param and pass it to
scenario card partials; German articles now render German labels
- Fix _generate_from_template() to extract language from data row and
pass to calc() and bake_scenario_cards()
- Fix article_slug to use {template_slug}-{city_slug} preventing UNIQUE
collision when multiple templates generate articles for same city
- Fix _rebuild_article() to pass lang to bake_scenario_cards()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Merges 11 commits covering full i18n of the padelnomics app:
- JSON locale files replacing all inline {% if lang %} blocks
- tformat Jinja2 filter for parameterized translations
- All public-facing templates: content, leads, directory, suppliers,
planner (Iteration 4)
- Auth-gated pages: dashboard, billing, supplier dashboard (all tabs),
business plan PDF (Iteration 5)
- Language detection fix for non-lang-prefixed routes (dashboard/billing)
- 1533 keys in en.json and de.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Phase 0: Fix language detection for dashboard/billing routes — use
_detect_lang() fallback in inject_globals() so g.lang is always set;
use g.get("lang") or "en" in route handlers before template render
- Phase 1: Dashboard templates (~29 keys, dash_* prefix)
index.html, settings.html, flash messages in routes.py
- Phase 2: Billing templates (~31 keys, billing_* prefix)
pricing.html, success.html, flash message in routes.py
- Phase 3: Supplier dashboard (~171 keys, sd_* prefix)
dashboard shell + overview + boosts, lead feed tab + lead cards,
listing tab; BOOST_OPTIONS now use name_key/desc_key; _compute_order()
accepts t dict for translated billing labels; all flash messages
replaced with get_translations(g.lang)
- Phase 4: Business plan PDF (~64 keys, bp_* prefix)
_fmt_months() accepts t dict; get_plan_sections() translates
venue_type/own_type/courts_desc/loan_term; adds sections["labels"]
sub-dict with all template-level strings; plan.html uses s.labels.*
and s.lang for the html[lang] attribute
- Update test_i18n_parity.py allowlist for new identical-value keys
(financial abbreviations, brand names, terms same in both languages)
Locale files: 1469 → 1533 keys (en.json and de.json)
All 1018 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>