Commit Graph

349 Commits

Author SHA1 Message Date
Deeman
35fe934fec fix: dashboard quote link points to quote wizard instead of suppliers page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:41:51 +01:00
Deeman
156cd43a14 fix(deploy): restore router config to current slot before health check
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>
2026-02-22 14:22:22 +01:00
Deeman
e88c514376 fix(deploy): add --profile to blue-app log dump
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>
2026-02-22 14:12:34 +01:00
Deeman
13c264ca75 fix(deploy): split log dump by service, revert litestream to latest
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>
2026-02-22 14:01:32 +01:00
Deeman
ad5e2516c4 fix(infra): pin litestream to v0.5.8 for R2 compatibility
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>
2026-02-22 13:49:02 +01:00
Deeman
044dfd836b fix(deps): add duckdb to padelnomics production dependencies
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>
2026-02-22 13:43:34 +01:00
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