generate_articles() was storing url_path with lang prefix (/en/markets/...)
but the content blueprint is registered at /<lang>, producing double-prefix
URLs like /en/en/markets/italy. Fix: store url_path without prefix, build
full_url with prefix for SEO tags (canonical, OG, hreflang, breadcrumbs).
Also removes /markets from RESERVED_PREFIXES since article sub-paths under
/markets/ are valid pSEO content URLs, not blueprint routes.
Subtask 1 of pSEO template improvements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- bake_scenario_cards() accepts scenario_overrides dict for preview mode
(bypasses DB lookup when no published_scenario exists)
- preview_article() builds in-memory scenario dict and passes it through
- Fix double-encoded & in locale strings (was &amp; in rendered HTML)
- Fix ruff import sort in _datetimeformat
- Fix migration 0019 minor issue
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Publish/Unpublish returns updated <tr> partial via HTMX
- Delete returns empty string to remove row without page reload
- Extract article_row.html partial (used by both results table and
individual HTMX responses)
- article_results.html now includes article_row.html via loop
Subtask 7 of CMS admin improvement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- generate_articles() now writes body_md alongside body_html
to BUILD_DIR/{lang}/md/{slug}.md
- article_edit GET checks both manual and generated markdown paths
- Fix pre-existing ruff import sort in _datetimeformat
Subtask 5 of CMS admin improvement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add invalidate_sitemap_cache() to sitemap.py and call it from
article_publish, article_delete, and article_new admin routes.
Subtask 6 of CMS admin improvement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add _get_article_list() with filters: status, template, language, search
- Add _get_article_stats() for header stats strip (total/live/scheduled/draft)
- Add /articles/results HTMX partial endpoint
- Add filter bar: search input + status/template/language dropdowns
- Paginate at 50 articles per page
- Add "View" link to live articles (opens public URL in new tab)
- Remove URL column (redundant), add Language column
Subtasks 2+3 of CMS admin improvement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Compute display_status (live/scheduled/draft) in SQL instead of broken
Jinja string comparison against undefined `now` variable
- Replace template_data_id (dropped in migration 0018) with template_slug
Subtask 1/8 of CMS admin improvement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DuckDB rows can have NULL columns (e.g. market_score, median_peak_rate).
Replace None with 0 in render context so numeric Jinja2 filters like
round() and int don't crash with "NoneType doesn't define __round__".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add _datetimeformat Jinja2 filter to _render_pattern() — templates
use {{ 'now' | datetimeformat('%Y') }} but the filter was never
registered, causing "No filter named 'datetimeformat'" on preview.
- Move Preview button to first column in template detail data table
so it's visible without scrolling on wide tables.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Part B: Calculator improvements
Fix 1 — annualRevGrowth was dead code. Now applied as compound multiplier
from Year 2 onwards across all revenue streams (court, ancillary, membership,
F&B, coaching, retail).
Fix 2 — IRR initial outflow bug (HIGH). Was using -capex but NCFs are
post-debt-service (levered). Using capex as denominator while using levered
cash flows understates equity returns by the leverage ratio. Fix: use -equity
as outflow for equity IRR, add separate projectIrr (unlevered, uses -capex
with EBITDA flows).
Fix 3 — NPV at hurdle rate. Discounts equity NCFs and exit proceeds at
hurdleRate (default 12%). Reports npv and npvPositive. NPV > 0 iff equity
IRR > hurdle rate. Added hurdleRate slider (5–35%) to Exit settings.
Fix 4 — Remaining loan: replaces heuristic with correct amortization math
(PV of remaining payments: monthlyPayment × (1 - (1+r)^-(n-k)) / r).
Fix 5 — Exit EBITDA: uses terminal year EBITDA (holdYears - 1) instead of
hardcoded Year 3. exitValue now reflects actual exit year, not always Y3.
Fix 6 — MOIC: moic is now equity MOIC (total equity CFs / equity invested).
projectMoic is the project-level multiple. Waterfall updated to show both.
Fix 7 — Return decomposition / value bridge. Standard PE attribution:
EBITDA growth value (operational alpha) + debt paydown (financial leverage).
Displayed in tab_returns.html as an attribution table.
Fix 8 — OPEX growth rate. annualOpexGrowth (default 2%) inflates utilities,
staff, insurance from Year 2 onwards. Without this Y4-Y5 EBITDA was
systematically overstated. Added annualOpexGrowth slider to Exit settings.
Fix 9 — LTV and DSCR warnings. ltvWarning (>75%) and dscrWarning (<1.25x)
with inline warnings in tab_metrics.html.
Fix 10 — Interest-only period. interestOnlyMonths (0–24) reduces early NCF
drag. Added slider to Financing section.
Updated test: test_stab_ebitda_is_year3 → test_stab_ebitda_is_exit_year.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously the admin role was only granted during dev-login via
ensure_admin_role(), but dev_run.sh resets the DB on each start,
so the role was never present when first visiting /admin.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The multi-line python3 -c heredoc in the Makefile caused
"missing separator" errors since Make runs each recipe line
in a separate shell. Moved to web/scripts/init_landing_seeds.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three deviations from the quart_saas_boilerplate methodology corrected:
1. Fix dim_cities LIKE join (data quality bug)
- Old: FROM eurostat_cities LEFT JOIN venue_counts LIKE '%country_code%'
→ cartesian product (2.6M rows vs ~5500 expected)
- New: FROM venue_cities (dim_venues) as primary table, Eurostat for
enrichment only. grain (country_code, city_slug).
- Also fixes REGEXP_REPLACE to LOWER() before regex so uppercase city
names aren't stripped to '-'
2. Rename fct_venue_capacity → dim_venue_capacity
- Static venue attributes with no time key are a dimension, not a fact
- No SQL logic changes; update fct_daily_availability reference
3. Add fct_availability_slot at event grain
- New: grain (snapshot_date, tenant_id, resource_id, slot_start_time)
- Recheck dedup logic moves here from fct_daily_availability
- fct_daily_availability now reads fct_availability_slot (cleaner DAG)
Downstream fixes:
- city_market_profile, planner_defaults grain → (country_code, city_slug)
- pseo_city_costs_de, pseo_city_pricing add city_key composite natural key
(country_slug || '-' || city_slug) to avoid URL collisions across countries
- planner_defaults join in pseo_city_costs_de uses both country_code + city_slug
- Templates updated: natural_key city_slug → city_key
Added transform/sqlmesh_padelnomics/CLAUDE.md documenting data modeling rules,
conformed dimension map, and source integration architecture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 18 new E2E tests from master: pricing, checkout, supplier signup,
supplier dashboard, and business plan export (sections J-N)
- Force WAITLIST_MODE=false in visual server subprocess — the root .env
sets WAITLIST_MODE=true, and since Config class attributes evaluate at
import time (before fork), the subprocess inherits the parent's value.
Patching both os.environ and core.config directly ensures feature pages
render instead of waitlist templates.
- All 77 visual tests now pass in ~59 seconds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
stg_playtomic_availability:
- Add maximum_object_size = 134217728 (128 MB) to both read_json calls;
daily files exceed the 16 MB default as venue count grows
- Add seed recheck file (1970-01-01_recheck_00.json.gz, gitignored with data/)
to avoid READ_JSON IOException when no recheck files exist
pseo_city_costs_de + pseo_city_pricing:
- Add QUALIFY ROW_NUMBER() OVER (PARTITION BY city_slug ...) = 1 to
deduplicate rows caused by dim_cities' loose LIKE join; reduces
pseo_city_costs_de from 2.6M → 222 rows (one per unique city)
content/__init__.py:
- DuckDB lowercases all column names at rest ("ratePeak" → "ratepeak"),
so calc_overrides dict comprehension never matched DEFAULTS keys.
Fix: build case-insensitive reverse map {k.lower(): k} and normalise
row keys before lookup. Applied in both generate_articles() and
preview_article().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Consolidate 3 duplicate server processes into 1 session-scoped
live_server fixture in conftest.py (port 5111, shared across all
visual test modules). Reduces startup overhead from ~3× to 1×.
- Fix init_db mock: patch padelnomics.app.init_db (where it's used)
instead of core.init_db (where it's defined). The before_serving
hook imported init_db locally — patching core alone didn't prevent
the real init_db from replacing the in-memory test DB.
- Keep patches active through app.run_task() so before_serving hooks
can't replace the test DB during the server's lifetime.
- Force RESEND_API_KEY="" in the visual test server subprocess to
prevent real email sends (dev mode: prints to stdout, returns "dev").
- Remove 4 screenshot-only no-op tests, replace with single
test_capture_screenshots that grabs all pages in one pass.
- Fix test_planner_tab_switching: remove nonexistent "metrics" tab.
- Delete ~200 lines of duplicated boilerplate from 3 test files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
city-cost-de.md.jinja:
- Lead with market score hook instead of raw venue count
- Stats strip hero (venues, market score, peak rate, population)
- Better section headings ("What Does a Padel Investment Cost in X?")
- Mid-body planner CTA after financial cards
- Expanded FAQ (6 questions incl. ROI and country comparison)
- Footer cross-link to country overview page
- Fixed url_pattern to use country_slug directly
country-overview.md.jinja:
- Country hub page at /markets/{country_slug}
- Aggregates: total venues, cities, avg market score, pricing
- Top-5 cities table with internal links to city pages
- Hub-and-spoke internal linking architecture
city-pricing.md.jinja:
- Per-city pricing deep-dive at /markets/{country_slug}/{city_slug}/court-prices
- Stats strip: peak rate, off-peak, P25-P75 range, occupancy
- Pricing table + market context (above/below national median)
- Occupancy-driven pricing explanation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Grid layout (2-col mobile, 4-col sm+) with label/value/unit slots.
Baked into static HTML at article generation time — no JS needed.
output.css is git-ignored (rebuild with: bin/tailwindcss -i ... -o ...).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pricing page (EN/DE, plan cards, no-auth access)
- Checkout success (auth required, renders for authed user)
- Supplier signup wizard (step 1, plan cards, DE variant, success page)
- Supplier dashboard (overview stats, boosts/credit packs, listing, leads tabs)
- Business plan export (auth required, form renders)
Also fixes:
- E2e server init_db mock scope — before_serving was calling real init_db
outside the patch context, overwriting the in-memory DB (fixes 3
pre-existing failures: markets_hub, markets_results, signup_page)
- Add _seed_billing_data() for supplier + feature flags in e2e server
- Mock RESEND_API_KEY="" in conftest + e2e server to prevent real emails
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pre-existing test failures after merge: enqueue payloads now include
'lang' key, and audience names changed from 'waitlist-auth' to
'newsletter' and 'waitlist-suppliers' to 'suppliers'.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- test_supervisor.py: 28 tests covering load_workflows, resolve_schedule,
is_due, topological_waves, and proxy round-robin / sticky selection
- test_feature_flags.py: 31 tests covering migration 0019, is_flag_enabled,
feature_gate decorator, admin toggle routes, and full toggle e2e flows
- conftest.py: seed feature flags with production defaults (markets=1,
others=0) so all routes behave consistently in tests
- Fix is_flag_enabled bug: replace non-existent db.execute_fetchone()
with fetch_one() helper
- Update 4 test_waitlist / test_businessplan tests that relied on
WAITLIST_MODE patches — now enable the relevant DB flag instead
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add full email management at /admin/emails with:
- email_log table tracking all outgoing emails with resend_id + delivery events
- inbound_emails table for Resend webhook-received messages
- Resend webhook handler (/webhooks/resend) updating delivery status in real-time
- send_email() returns resend_id (str|None) instead of bool; all 9 worker
handlers pass email_type= for per-type filtering
- Admin UI: sent log with HTMX filters, email detail with API-enriched HTML
preview, inbox with unread badges + reply, compose with branded wrapping,
audience management with contact list/remove
- Sidebar Email section with unread badge via blueprint context processor
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the old CSV-upload-based CMS with an SSG architecture where
templates live in git as .md.jinja files with YAML frontmatter and
data comes directly from DuckDB serving tables. Only articles and
published_scenarios remain in SQLite for routing/state.
- Content module: discover, load, generate, preview functions
- Migration 0018: drop article_templates + template_data, recreate
articles + published_scenarios without FK references, add
template_slug/language/date_modified/seo_head columns
- Admin routes: read-only template views with generate/regenerate/preview
- SEO pipeline: canonical URLs, hreflang (EN+DE), JSON-LD (Article,
FAQPage, BreadcrumbList), Open Graph tags baked at generation time
- Example template: city-cost-de.md.jinja for German city market data
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9 tests exercise the full handler→wrap→send_email→Resend API path
using Resend's @resend.dev test addresses. Skipped when RESEND_API_KEY
is not set.
With a full_access API key, tests also retrieve the sent email via
resend.Emails.get() and assert on the rendered HTML (wordmark, links,
project details, heat badges). With a sending_access key, send is
verified but HTML assertions are skipped gracefully.
Includes bounce handling test and 0.6s inter-test delay for Resend's
2 req/sec rate limit.
Run with: RESEND_API_KEY=re_xxx pytest -k resend_live
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mock send_email and call each handler directly. Covers recipient,
subject content, HTML design elements (wordmark, preheader, heat
badges), from_addr, skip-on-missing-data guards, and email_sent_at
timestamp updates.
Also fixes IndexError in handle_send_welcome when payload has no name
("".split()[0] → safe fallback).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Redesigned _email_wrap(): lowercase wordmark header matching website,
3px blue accent border, preheader text support, HR separators.
_email_button() now full-width block for mobile tap targets.
Rewrote copy: improved subject lines, urgency cues, quick-start links
in welcome, styled project recap in quote verify, heat badges on lead
forward, "what happens next" in lead matched, secondary CTAs.
~30 new/updated translation keys in both EN and DE.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add maximum_object_size=128MB to read_json for 14K-venue tenants file
- Rewrite opening_hours to use UNION ALL unpivot (DuckDB struct dynamic access)
- Add seed file guard for availability model (empty result on first run)
- Fix snapshot_date VARCHAR→DATE comparison in venue_pricing_benchmarks
- Fix export_serving to resolve SQLMesh physical tables from view definitions
(SQLMesh views reference "local" catalog unavailable outside its context)
- Add pyarrow dependency for Arrow-based cross-connection data transfer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add lang parameter to all enqueue() calls for email internationalization.
Restructure Resend audiences to 3 named audiences (owners, suppliers, waitlist).
Use _t() translation function in all email template handlers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Markets now sits left of logo with Planner and Quotes (investor/demand
side). Mobile section headers use i18n keys instead of hardcoded English.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three workstreams:
1. Playtomic full data extraction & transform pipeline:
- Expand venue bounding boxes from 4 to 23 regions (global coverage)
- New staging models for court resources, opening hours, and slot-level
availability with real prices from the Playtomic API
- Foundation fact tables for venue capacity and daily occupancy/revenue
- City-level pricing benchmarks replacing hardcoded country estimates
- Planner defaults now use 3-tier cascade: city data → country → fallback
2. Transactional email i18n:
- _t() helper in worker.py with ~70 translation keys (EN + DE)
- All 8 email handlers translated, lang passed in task payloads
3. Resend audiences restructured to 3 named audiences (free plan limit)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 requires_weasyprint marker to TestGenerateBusinessPlan and TestWorkerHandler
(these need libgobject/pango/cairo which CI python:3.12-slim lacks)
- Fix export route tests: use opaque tokens instead of integer IDs
- Replace deprecated datetime.utcnow() with datetime.now(UTC)
- Add missing jsonify/Response imports to admin routes
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>