Commit Graph

393 Commits

Author SHA1 Message Date
Deeman
5f7e8f1200 fix(deploy): move router config write to after health check passes
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>
2026-02-22 13:22:50 +01:00
Deeman
e39eaefb43 fix(deploy): dump app container logs on health check failure
Makes the crash reason visible in GitLab CI logs instead of just
"container is unhealthy".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 13:12:26 +01:00
Deeman
95888dedb9 fix(leads): fix undefined lang variable in quote validation error path
Used g.get("lang", "en") consistent with the rest of the file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 13:04:36 +01:00
Deeman
6e1e6b0484 fix(tests): fix all 10 e2e test failures
- 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>
2026-02-22 11:42:54 +01:00
Deeman
2521ba61b6 fix(test): correct article slug in test_article_url_and_title
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>
2026-02-22 10:48:43 +01:00
Deeman
ff31d35782 fix(ci): update paths after repo flatten
- 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>
2026-02-22 10:42:10 +01:00
Deeman
625c089284 fix: post-flatten dev scripts, lead form validation, copy improvements
- 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>
2026-02-22 10:29:59 +01:00
Deeman
22ad855c70 fix(deploy): update docker-compose.prod.yml paths after repo flatten
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>
2026-02-22 01:17:47 +01:00
Deeman
80b2148108 refactor: flatten padelnomics/padelnomics/ → repo root
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 00:49:37 +01:00
Deeman
4ae00b35d1 refactor: flatten padelnomics/padelnomics/ → repo root
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>
2026-02-22 00:44:40 +01:00
Deeman
5e471567b9 feat(seo): expand city coverage to 40 cities + DuckDB refresh script
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 00:02:13 +01:00
Deeman
f958f1e816 feat(seo): expand city coverage to 40 cities + DuckDB refresh script
- 22 new cities: Madrid, Barcelona, Valencia, Seville, Málaga (ES),
  Paris, Lyon, Marseille (FR), Milan, Rome (IT), Amsterdam (NL),
  Vienna (AT), Zurich (CH), Stockholm (SE), Lisbon, Porto (PT),
  Brussels (BE), Dubai (AE), Sydney, Melbourne (AU), Dublin (IE)
- Total: 40 cities × EN + DE = 80 articles
- refresh_from_daas.py: sync template_data from planner_defaults
  serving table; dry-run mode; graceful when analytics unavailable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 00:01:37 +01:00
Deeman
3710665f45 feat(planner): wire DuckDB analytics + market-data endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 23:42:51 +01:00
Deeman
a2de1a0206 feat(planner): wire DuckDB analytics + market-data endpoint
- analytics.py: open/close_analytics_db registered in app lifecycle
- GET /planner/api/market-data?city_slug=<slug>: returns per-city
  planner defaults from DuckDB planner_defaults serving table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 23:26:06 +01:00
Deeman
47db8c2418 feat(sqlmesh): merge 4-layer DuckDB pipeline 2026-02-21 22:08:50 +01:00
Deeman
9f8ca82505 feat(sqlmesh): add transform workspace member with 4-layer DuckDB pipeline
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>
2026-02-21 22:03:59 +01:00
Deeman
b8471c7f31 feat(daas): merge extraction pipelines (Overpass, Eurostat, Playtomic) 2026-02-21 21:48:26 +01:00
Deeman
af09597930 feat(daas): add extract workspace member with Overpass, Eurostat, Playtomic extractors
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>
2026-02-21 21:38:55 +01:00
Deeman
f18e788fc7 Merge feat/flatten-padelnomics: UV workspace monorepo structure
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>
2026-02-21 17:51:47 +01:00
Deeman
8221174683 feat(structure): flatten padelnomics to UV workspace monorepo layout
Mirrors the quart_saas_boilerplate template's new web/ workspace structure:
- git mv src/ → web/src/
- git mv tests/ → web/tests/
- git mv scripts/ → web/scripts/
- Split pyproject.toml into workspace root + web/pyproject.toml
- Dockerfile: COPY src/ → web/, update CSS build + output paths,
  add --package padelnomics to uv sync
- Makefile: update CSS input/output paths
- pytest testpaths: tests/ → web/tests/

uv sync passes, 402/403 tests pass (1 pre-existing failure in test_content).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 17:51:14 +01:00
Deeman
7325fc905f docs: market research enhancement + data sources inventory
- market-research-padel-costs-2026.md: add sections 11-15 (demand-side
  city profiles, CAPEX breakdown by component, insurance benchmarks,
  seasonal utilisation patterns by climate zone, Playtomic GMV data)
- data-sources-inventory.md: 27 data sources across 7 categories
  (court locations, pricing, market reports, real estate, demographics,
  regulatory, tournaments) with access method, license, and update freq
- CMS.md, I18N.md: feature documentation from template analysis

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 15:51:42 +01:00
Deeman
9b23ab712c Merge branch 'currency-by-country'
Currency-aware formatting in the planner: UK → £, US → $, EU → €
with locale-native thousands separators.
2026-02-21 01:53:28 +01:00
Deeman
5662a7dce3 feat(planner): currency formatting by country (UK=£, US=$, EU=€)
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 &euro; replaced with {{ item.formatted_amount }}

1017 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 01:51:46 +01:00
Deeman
0b4f2f2180 fix(merge): resolve conflict in bake_scenario_cards — pass lang + t(lang)
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>
2026-02-21 01:41:39 +01:00
Deeman
ceee85caba feat(content): programmatic SEO seed script + lang bug fixes
- 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>
2026-02-21 01:38:33 +01:00
Deeman
9a71396524 fix(copy): honest data privacy claim, simplify quote CTA buttons
Replace misleading "Deine Daten bleiben privat" with
"Nur passende Anbieter sehen deine Anfrage". Simplify
"Anbieter-Angebote einholen" to "Angebote einholen" in DE/EN.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 01:33:03 +01:00
Deeman
2b0798c768 merge(i18n): i18n-foundation — complete i18n for all pages (Iterations 4–5)
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>
2026-02-21 01:29:12 +01:00
Deeman
420a2f063b feat(i18n): translate all auth-gated pages (Iteration 5)
- 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>
2026-02-21 01:24:22 +01:00
Deeman
7485bdffad feat(seo): add Google site verification meta tag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 01:18:13 +01:00
Deeman
8174b7f0c0 refactor(i18n): Phase 4 — eliminate ad-hoc features_de and is_en patterns
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>
2026-02-21 00:34:00 +01:00
Deeman
7c440a209a refactor(i18n): Batch 6 — eliminate {% if lang %} blocks in public templates
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>
2026-02-21 00:29:08 +01:00
Deeman
798ade2bc3 refactor(i18n): Batch 5 — eliminate {% if lang %} blocks in planner templates
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>
2026-02-21 00:13:32 +01:00
Deeman
43905b343b refactor(i18n): Batch 4 — eliminate {% if lang %} blocks in suppliers templates
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>
2026-02-21 00:08:25 +01:00
Deeman
dd6f73a7a2 refactor(i18n): Batch 3 — eliminate {% if lang %} blocks in directory templates
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>
2026-02-20 23:50:06 +01:00
Deeman
5c99b1bf44 refactor(i18n): Batch 2 — eliminate {% if lang %} blocks in leads templates
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>
2026-02-20 23:41:52 +01:00
Deeman
84df11aee7 refactor(i18n): Batch 1 — eliminate {% if lang %} blocks in content templates
Convert 63 inline lang blocks across 7 content templates to t.key references.
Add 51 new locale keys (scenario_*, markets_*, article_detail_*).

Templates updated:
- content/partials/scenario_summary.html (6 blocks → t.keys)
- content/partials/scenario_returns.html (15 blocks → t.keys)
- content/partials/scenario_operating.html (17 blocks → t.keys)
- content/partials/scenario_cashflow.html (7 blocks → t.keys, tformat)
- content/partials/scenario_capex.html (9 blocks → t.keys)
- content/templates/markets.html (6 blocks → t.keys in title/meta/labels)
- content/templates/article_detail.html (2 blocks → t.keys)

Also: fix scenario card CTA links (href="/planner/" → url_for), add url_for
stub and tformat filter to _bake_env, pass lang+t to bake render calls,
update test_planner_cta_link assertion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 23:29:30 +01:00
Deeman
8732f5a5e0 feat(i18n): add tformat Jinja2 filter for parameterized translations
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>
2026-02-20 23:22:49 +01:00
Deeman
9957b27a77 refactor(i18n): extract translations to JSON locale files
Move 941 UI strings per language from inline Python dicts to
locales/en.json and locales/de.json. Rewrite i18n.py from 2062
lines to 140 lines (JSON loader + inline calc item names).

- Create src/padelnomics/locales/en.json (941 keys)
- Create src/padelnomics/locales/de.json (941 keys)
- Add load-time key parity assertion (EN/DE must match)
- Keep _CALC_ITEM_NAMES inline (36 keys, calc-only namespace)
- Fix stale docstring referencing non-existent function
- Add tests/test_i18n_parity.py: key parity, non-empty values,
  no untranslated copy-paste (with allowlist for proper nouns)

All 1013 existing tests pass + 5 new parity tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 23:21:59 +01:00
Deeman
7b1914839f feat(i18n): translate suppliers page, fix nav labels, Businessplan copy
- suppliers.html: 167 t.* references (was 0) — full DE translation across all 10 sections
- nav_planner DE: Kostenrechner → Finanzplaner
- nav_quotes DE: Angebote → Angebot erhalten
- businessplan.py + export_waitlist.html: Geschäftsplan → Businessplan
- suppliers waitlist + signup step 1: German feature lists via features_de

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:53:41 +01:00
Deeman
24f528a157 feat(feedback): pill button, umami ID capture, contact field, migration
- 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>
2026-02-20 21:10:02 +01:00
Deeman
23bed0d5f9 feat(iteration-2): i18n, UX & quote flow improvements
- Auth templates fully translated (DE/EN) with before_request lang detection
- Flash messages in auth routes use get_translations(g.lang) lookups
- Quote verify URL bug fixed: includes /<lang>/ prefix in worker email
- Sie→Du conversion across public/supplier/directory/leads templates
- Budget label: 'Budgetschätzung' → 'Budget', step=10000 on input
- Context option: 'Erweiterung' copy made more specific
- Footer reordered Brand|Product|Company|Legal and fixed grid-3→grid-4
- Quote sidebar visibility: display:none → display:block (media query hides <1400px)
- Floating feedback button: fixed bottom-right speech-bubble SVG
- Quote step 1: editable when pre-filled from planner, with 'Edit in Planner' link
- Quote step 6 & 8: financing_status, decision_process, services_needed mandatory
- Disposable email + fake phone filtering in core.py, applied at auth and leads
- Directory labels (category/country/region) translated via get_directory_labels(lang)
- Result tab tooltips for IRR, MOIC, RevPAH, EBITDA, Payback, DSCR, Debt Yield, etc.
- Markets hub gated behind waitlist decorator (POST handler + markets_waitlist.html)
- Email design refresh: brand blue #1D4ED8 button, monogram logo, proper footer
- USER_FLOWS.md documents all 12 user flows
- test_e2e_flows.py: 46 Playwright E2E tests across all flows (port 5113, -m visual)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:56:14 +01:00
Deeman
696581d57b fix(planner): charts, wizard footer layout, tooltip translations & summary label
- 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>
2026-02-20 19:11:24 +01:00
Deeman
7c710ada6b refactor(planner): HTMX server-render refactor — eliminate JS SPA
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>
2026-02-20 17:12:28 +01:00
Deeman
e6f4c0a540 copy(de): Sie→Du overhaul, terminology & directory SEO copy
- All German UI copy switched from formal Sie/Ihr to informal Du/Dein
  across ~60 i18n.py keys and 10 template files
- Platz-Anbieter → Anbieter in CTAs; Anlage → Padel-Platz in planner
  wizard and planner translations (wiz_venue, sl_budget_target)
- Anlageplanung → Padelplatz-Planung in service checklist
- Directory H1: Padelplatz-Hersteller, Platzbauer & Anbieter (SEO)
- mkt_no_results / mkt_search_placeholder: Artikel → Märkte (EN + DE)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 16:15:07 +01:00
Deeman
9d1d42f6db feat: complete German translation of all public-facing content
Merges worktree-i18n-german branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 14:45:03 +01:00
Deeman
e66a55a8db fix: nav mobile layout + container width alignment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 14:44:53 +01:00
Deeman
ddbdc00a3d feat: complete German translation of all public-facing content
Translate the entire public-facing surface of the app to German,
using a hybrid approach: {{ t.key }} for short UI strings and
{% if lang == 'de' %} conditionals for prose blocks/FAQs.

Coverage:
- i18n.py: +300 UI keys, +200 planner JS locale strings, +35
  CAPEX/OPEX item name translations; new get_planner_translations()
  and get_calc_item_names() helpers
- base.html / _cookie_banner.html: nav, footer, cookie banner,
  feedback placeholder; JS toggle text injected via tojson
- public/: landing.html (hero, ROI calc, FAQ, SEO, JSON-LD),
  features.html, about.html — all with German meta tags
- planner/: planner.html (wizard, tabs, chart labels, CTAs),
  all export templates, scenario_list.html; window.__PADELNOMICS_LOCALE__
  injected server-side; planner.js all ~200 strings via tr()
- calculator.py: add lang param, translated CAPEX/OPEX item names,
  replace rent name-lookup with local rent_amount variable
- directory/: directory.html, supplier_detail.html, results.html,
  enquiry_result.html
- leads/: quote_step_1–9.html, quote_request.html, quote_submitted.html,
  quote_verify_sent.html; routes.py flash messages + _get_quote_steps(lang)
- suppliers/: signup flow (step 1–4), signup_success.html,
  waitlist.html, waitlist_confirmed.html
- content/: markets.html, article_detail.html, market_results.html,
  all scenario partials (summary, capex, cashflow, operating, returns)
- 629 tests pass, ruff clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 14:42:31 +01:00
Deeman
3c903bad97 fix: planner quote button — use lang-prefixed URL to prevent 404
The "Get Supplier Quotes" CTA in planner.js used a hardcoded
/leads/quote path which 404s because the route is registered at
/<lang>/leads/quote. Inject the correct URL server-side via
window.__PADELNOMICS_QUOTE_URL__ using url_for, consistent with
the existing __PADELNOMICS_CALC_URL__ / __PADELNOMICS_SAVE_URL__ pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 14:13:44 +01:00
Deeman
48587d6436 feat: responsive nav — hamburger menu + wider container (900px breakpoint)
- Widen nav container from 72rem to 80rem (1280px) — matches Zillow's
  nav container width, more breathing room for items on large monitors
- Raise collapse breakpoint from 768px to 899px — links stay visible
  until the screen is actually too narrow
- Add hamburger button (SVG 3-line icon) visible at < 900px
- Add mobile drop-down panel with all nav links grouped under Plan /
  Explore / Account sections; overlay + Escape key close it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 14:04:56 +01:00
Deeman
32f29c53ec fix: improve German nav labels
Verzeichnis → Anbieterverzeichnis (self-explanatory, matches search terms)
Planer → Kostenrechner (unambiguous, signals purpose immediately)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 13:39:57 +01:00