Commit Graph

271 Commits

Author SHA1 Message Date
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 € 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
Deeman
0b0fc7ef50 fix: ADMIN_EMAIL -> ADMIN_EMAILS in CI .env heredoc
App reads ADMIN_EMAILS (CSV list); the heredoc had the wrong singular name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 13:32:25 +01:00
Deeman
86c4ebf25d fix: add missing env vars to CI .env heredoc
WAITLIST_MODE, LEADS_EMAIL, UMAMI_API_URL were set in GitLab CI but
never written to .env. Paddle vars made optional (:-) so deploys work
without them when in waitlist mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 13:10:48 +01:00
Deeman
358bc5c02f fix: use kill -0 1 for litestream healthcheck
pgrep may not be available in the litestream image. kill -0 1 checks
whether PID 1 (litestream, after exec) is alive — works in any container.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 13:08:00 +01:00
Deeman
76fc19c183 fix: litestream healthcheck gate + 1yr retention
Re-enable deploy gate on litestream: pgrep-based healthcheck with 6
retries (30s window) after a 15s start period — broken backups now
fail the deploy loudly instead of silently succeeding.

Extend retention from 7d to 1yr (8760h): WAL frames are tiny for a
low-traffic app, R2 free tier covers years of storage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 13:00:29 +01:00
Deeman
b0f36192a6 fix: litestream single replica + disable healthcheck gate
v0.5.8 dropped multi-replica support — remove the local path replica,
keeping only R2. Also disable litestream's healthcheck so deploy's
`up --wait` isn't gated on the backup service.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 12:54:19 +01:00
Deeman
dc02563e52 fix: write nginx config before container start to fix first-deploy health check
Router health check (nginx -t) fails when default.conf doesn't exist yet.
Move config write to before `up -d --wait` so nginx has a valid config
on first deploy or after a volume wipe. Router reload stays post-health-check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 12:45:37 +01:00
Deeman
449ff413e3 chore: update virtual office address in all legal pages
Replace home address with c/o COCENTER, Koppoldstr. 1, 86551 Aichach
in imprint_de, imprint_en, privacy_de, privacy_en, and terms_de.
Jurisdiction clause ("Gerichtsstand Oldenburg") left untouched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 11:44:14 +01:00
Deeman
c0c8607664 fix: migration atomicity + deploy hardening + Litestream R2
Migration atomicity:
- Remove conn.commit() and executescript() from all up() functions (0000,
  0011, 0012, 0013, 0014, 0015); executescript() issued implicit COMMITs
  which broke the batch-rollback guarantee of the migration runner
- Rewrite 0000 with individual conn.execute() calls (was a single
  executescript block)

Deploy hardening:
- Add pre-migration DB backup step to deploy.sh: saves
  app.db.pre-deploy-<timestamp> in the volume before every migration
- On health-check failure: restore the backup, then stop + exit
- On success: clean up old backups (keep last 3)

Litestream:
- Enable R2 as primary replica in litestream.yml (env-var placeholders)
- Add local /app/data/backups as secondary replica
- docker-compose: add auto-restore on empty volume (sh entrypoint runs
  'litestream restore' before 'litestream replicate' if app.db missing)
- Add LITESTREAM_R2_* vars to .gitlab-ci.yml .env block and .env.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:28:59 +01:00
Deeman
363f93885d fix: ruff clean + all visual tests passing
- Ruff: auto-fixed 43 errors (unused imports, unsorted imports, bare
  f-strings); manually fixed 6 remaining (unused vars, ambiguous `l`)
- Visual tests: server now builds schema via migrate() instead of the
  deleted schema.sql; fixes ERR_CONNECTION_REFUSED on all tests
- Visual tests: updated assertions for current landing page (text logo
  replacing img, .roi-calc replacing .teaser-calc, intentional dark
  sections hero-dark/cta-card allowed, card count >=6, i18n-prefixed
  logo href, h3 brightness threshold relaxed to 150)
- CSS: remove dead .nav-logo { line-height: 0 } (was for image logo,
  collapsed text logo to zero height) and .nav-logo img rules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 08:49:47 +01:00
Deeman
b416cd682a feat: i18n URL prefixes + German legal pages
All public-facing blueprints (public, planner, directory, content,
leads, suppliers) now serve under /<lang>/ (e.g. /en/, /de/). Internal
blueprints (auth, dashboard, admin, billing) are unchanged.

URL routing:
- Root / detects lang from cookie → Accept-Language → default 'en'
  and 301-redirects to /<lang>/
- Quart url_value_preprocessor pops <lang> into g.lang; url_defaults
  auto-injects it so existing url_for() calls need no changes
- Unsupported lang prefixes (e.g. /fr/) return 404
- Legacy bare URLs (/terms, /privacy, /imprint, /about, /features,
  /suppliers) redirect 301 to /en/ equivalents
- robots.txt and sitemap.xml moved to app-level root; sitemap now
  includes both en and de variants of every SEO page
- lang cookie persisted 1 year, SameSite=Lax

i18n:
- New i18n.py: SUPPORTED_LANGS, LANG_BLUEPRINTS, flat translation dicts
  for ~20 nav/footer keys in en + de
- lang and t injected into every template context

Templates:
- base.html: <html lang="{{ lang }}">, hreflang tags (en/de/x-default)
  on lang-prefixed pages, nav/footer strings translated via t.*, footer
  language toggle EN | DE, SVG racket logo removed from footer
- 6 legal templates (terms/privacy/imprint × en/de) replacing old 3:
  - English: GDPR sections with correct controller identity (Hendrik
    Dreesmann, Zum Offizierskasino 1, 26127 Oldenburg), real sub-
    processors (Umami self-hosted, Paddle, Resend with SCCs), German-
    law jurisdiction
  - German: DSGVO-konforme Datenschutzerklärung, AGB, Impressum per
    § 5 DDG; Kleinunternehmer § 19 UStG; LfD Niedersachsen reference

Tests updated to use /en/ prefixed routes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 02:29:42 +01:00
Deeman
b19b6b907c fix: use inline SVG data URI for favicon — bypasses browser cache
Browsers were fetching favicon.ico and ignoring the external SVG link.
Inline data URI has no caching layer so the 'p' lettermark shows immediately.
Drops the .ico and .png favicon links (Apple touch icon kept for home screen).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 02:22:29 +01:00
Deeman
9e93df27e9 chore: bump favicon SVG cache-bust to v5
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 02:16:28 +01:00
Deeman
4f83fc1d36 fix: center nav logo, remove footer racket, replace favicon
- Nav: switch nav-inner from flex/space-between to grid (1fr auto 1fr)
  so logo is always geometrically centered regardless of link count per side;
  update input.css source of truth + inline override in base.html
- Footer: remove inline SVG racket, text-only wordmark matching the nav
- favicon.svg: replace padel racket with navy rounded square + white 'p'
  lettermark

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 02:12:51 +01:00
Deeman
8671facd7c chore: polish cookie banner — animation, UX, accessibility
- Slide-up entrance / slide-down exit animations (CSS keyframes)
- Upward box-shadow for visual depth from page content
- Larger toggle (44×24px) for easier tap target on mobile
- Split dual-state Manage/Save into: header button Manage↔Close + panel Save choices button
- ARIA: role=dialog, role=switch, aria-checked on toggle, focus moves to prefs on open
- Use btn-outline / btn component classes for secondary/primary buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 02:06:38 +01:00
Deeman
a7b38339a6 feat: cookie consent banner, defer Paddle.js to checkout pages
- Add cookie consent banner (_cookie_banner.html) — fixed bottom bar with
  "Accept all" and "Manage preferences" (toggles for Essential/Functional);
  consent stored in cookie_consent cookie (1 year); no-JS = only essential
  cookies set (privacy-safe default)
- Add "Manage Cookies" link to footer Legal section to re-open the banner
- Extract Paddle.js init into _paddle.html partial; add {% block paddle %}
  to base.html (empty by default); override on export, supplier signup, and
  supplier dashboard pages — Paddle.js no longer loads on every page visit
- Gate ab_test() on functional cookie consent: variant picked per-request
  always, but ab_* cookie only persisted when visitor has consented
- Update privacy policy section 6: full cookie disclosure (essential,
  functional, payment categories + Umami cookieless note); fix "Plausible"
  → "Umami" in service providers list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 01:48:08 +01:00
Deeman
23ca28613e chore: solid white nav — remove frosted glass backdrop 2026-02-20 01:16:14 +01:00
Deeman
6d622e7663 chore: self-host fonts (Bricolage Grotesque, DM Sans) and HTMX — remove Google Fonts, jsDelivr, unpkg dependencies 2026-02-20 01:08:30 +01:00
Deeman
3f8f0a7187 chore: replace nav SVG racket logo with text-only wordmark 2026-02-20 00:59:49 +01:00
Deeman
68cf03c026 refactor: rename ADMIN_EMAIL -> LEADS_EMAIL for clarity 2026-02-20 00:41:05 +01:00
Deeman
bd8976f344 fix: sending domain notification -> notifications.padelnomics.io 2026-02-20 00:37:56 +01:00
Deeman
2e666e84dc chore: add WAITLIST_MODE and RESEND_AUDIENCE_PLANNER to .env.example 2026-02-20 00:25:12 +01:00
Deeman
cb1fc00027 chore: ignore .hypothesis/ test output directory 2026-02-19 23:46:10 +01:00