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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>