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>
Replace parallel features/features_de lists in PLAN_FEATURES with
feature_keys (translation key references). Update waitlist.html and
signup_step_1.html to iterate over plan.feature_keys and render
{{ t[key] }} instead of raw strings.
Replace is_en ternaries in businessplan.py with t = get_translations(language)
and t["bp_*"] lookups for all 9 section headings.
Adds 25 new keys (1197 → 1222): plan_basic_f1-7, plan_growth_f1-4,
plan_pro_f1-5, bp_title, bp_exec_summary, bp_investment, bp_operations,
bp_revenue, bp_annuals, bp_financing, bp_metrics, bp_cashflow_12m.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert about.html (8 blocks), features.html (12 blocks), and landing.html
(28 blocks including JSON-LD structured data) to {{ t.key }} lookups.
Adds 49 new keys (1148 → 1197) covering page titles, meta tags, body copy,
FAQ answers, SEO paragraphs, and JSON-LD org/FAQ schema.
Collapses the duplicated EN/DE JSON-LD block into a single structure using
t.landing_faq_q*/a* and t.landing_jsonld_org_desc. Parameterized strings
(supplier counts, country counts) use | tformat.
All 250 {% if lang %} blocks now eliminated across 42 templates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
53 new keys added to en/de locale files (1095 → 1148). All {% if lang %}
blocks replaced with {{ t.key }} in planner.html, 4 tab partials,
export.html, and export_waitlist.html. Feature lists converted to
key-loop pattern, large waitlist block collapsed to single-language
structure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
65 new keys added to en/de locale files (1030 → 1095). All {% if lang %}
blocks replaced with {{ t.key }} / {{ t.key | tformat(...) }} in the 8
supplier signup, waitlist, and confirmation templates. JS-embedded strings
use | tojson. features_de pattern in step_1/waitlist deferred to Phase 4.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 19 new locale keys (dir_page_title, dir_page_meta_desc, dir_page_og_desc,
dir_results_count_singular/plural, dir_ex_*, sp_enquiry_placeholder,
sp_cta_basic_desc/btn, sp_locked_popover_desc, sp_cta_claim_desc,
enquiry_forwarded_msg, enquiry_received_msg) and replace all 23 {% if lang %}
blocks across directory.html, supplier_detail.html, partials/results.html,
and partials/enquiry_result.html.
directory.html hero description reuses existing dir_subheading key via
tformat(n=, c=). Results count split into singular/plural keys to handle
EN "supplier"/"suppliers" and DE "Anbieter"/"Anbietern" pluralization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 19 new locale keys (q_page_title, q_step_counter, q1/2/5/7/9 error
hints, qs_matched_*, qv_sent_msg, qv_instructions, qv_no_email,
qv_check_email_pre/post) and replace all 22 {% if lang %} blocks across
quote_request.html, quote_verify_sent.html, quote_submitted.html, and
all 9 quote_step_*.html partials.
Progress bar OOB counter shared across all 9 step partials now uses
{{ t.q_step_counter | tformat(step=step, total=steps|length) }}.
Complex project description in quote_submitted.html uses 5 fragment
keys (qs_matched_*) with qs_matched_facility_fmt="{type}" vs "{type}-"
to handle EN/DE compound-word suffix without empty-value keys.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds _tformat(s, **kwargs) filter registered as app.jinja_env.filters["tformat"].
Uses str.format_map() with named placeholders.
Usage: {{ t.key | tformat(count=total_suppliers, name=supplier.name) }}
JSON: "Browse {count}+ suppliers from {name}"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Button restyled from round icon-only to pill with speech-bubble icon + "Feedback" text
- Hidden umami_id field populated from localStorage.getItem('umami.uuid')
- Optional contact field (email/name) shown to anonymous users only
- Migration 0016 adds umami_id and contact columns to feedback table
- Route saves all three new fields (user_id was already captured)
- conftest.py: patch_config now sets WAITLIST_MODE=False to isolate tests from env
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Charts: augment_d() now emits full Chart.js 4.x config objects {type, data,
options} for all 7 charts. Previously raw data dicts were passed directly to
new Chart() which requires a proper config, causing silent render failures.
- Wizard footer: HTMX outerHTML OOB swap for #wizPreview was stripping
class="wizard-preview" on every recalc, collapsing the flex layout and
stacking CAPEX / Monatl. CF / IRR vertically. Added class back to the OOB
element in calculate_response.html.
- Wizard nav buttons: showWizStep() was generating wiz-btn--prev and
wiz-btn--calc classes that had no CSS. Changed to wiz-btn--back and
wiz-btn--next which are defined in planner.css.
- Tooltip translations: added 60 tip_* keys (EN + DE) to i18n.py and replaced
all hardcoded English strings in planner.html slider calls with t.tip_* refs.
German users now see German tooltip text on all "i" info spans.
- Summary label: added wiz_summary_label ("Live Summary" / "Aktuelle Werte")
as a full-width caption in the wizard preview bar so users understand the
three values reflect current slider state. Added flex-wrap + caption CSS.
- Tests: 384 new tests across test_planner_charts.py, test_i18n_tips.py,
test_planner_routes.py covering all fixed bugs. Full suite: 1013 passing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the 847-line client-side planner with an HTMX architecture:
- All tab content (CAPEX, Operating, Cash Flow, Returns, Metrics) rendered
server-side as Jinja2 partials; slider changes POST to /planner/calculate
which returns HTML; HTMX swaps into #tab-content
- Merge _PLANNER_TRANSLATIONS into _TRANSLATIONS; delete get_planner_translations()
and window.__PADELNOMICS_LOCALE__; all strings now {{ t.key }} in templates
- New form_to_state() and augment_d() helpers in routes.py; calculate endpoint
returns HTML instead of JSON; OOB swaps update header tag + wizard preview
- Add 5 Jinja2 filters: fmt_currency, fmt_k, fmt_pct, fmt_x, fmt_n
- Rewrite planner.js to ~200 lines: chart init on htmx:afterSettle, slider sync,
toggle management, wizard nav, scenario save/load, reset to defaults
- Add 7 new template partials: tab_capex, tab_operating, tab_cashflow,
tab_returns, tab_metrics, calculate_response, court_summary, wizard_preview
- Update test_phase0 to match new HTML-returning /calculate endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>