Commit Graph

460 Commits

Author SHA1 Message Date
Deeman
c345746fbc editorial: Feb 2026 content batch review + market maturity rewrite
5-pass editorial pipeline across 11 cornerstone articles (6 DE + 5 EN)
and 3 bilingual pSEO templates. All pieces scored ≥4.4 and cleared the
publish threshold.

Critical/High fixes applied:
- Ceiling height inconsistency: 7m → 8m in build guide tables (EN + DE)
- HTML <span> tags removed from meta_description_pattern in all 3 templates
- German gendering violations fixed in padel-halle-bauen-de (4 instances)
- Grammatical gender fix: "Das häufigste Vorabend-Fehler" → "Der häufigste Fehler"
- Noun capitalisation: "sport" → "Sport" in padel-standort-analyse-de

Medium fixes applied:
- Varied repeated "well-run padel halls" phrase in EN investment risks article
- Orphaned F&B note elevated to bold callout
- Colloquial idiom replaced in EN cost guide
- "analyze" → "analyse" (British English) in EN location guide

P4-A resolved: replaced static German city-tier lists in both location
guide articles with a universal "market maturity stages" framework section
(established / growth / emerging markets). Articles are now country-agnostic
and link to pSEO country overview pages for live market data.

7 open improvement items remain (P1-A/B, P2-A/B/C, P3-A, P4-B/C) — none
are publish blockers. See docs/editorial-review-2026-02.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 11:02:35 +01:00
Deeman
5fa8a98903 merge: opportunity score data quality improvements
Phase 0 — income ceiling fix (opportunity_score):
  PPS normalisation /200→/35000; economic power now differentiates
  countries (DE 13.2, ES 10.7, SE 14.3 pts; was 20.0 everywhere)

Phase 1b — overpass_tennis in workflows.toml:
  Monthly schedule added; was only in combined extractor

Phase 2b — dim_cities spatial population fallback:
  GeoNames spatial CTE (ST_Distance_Sphere, 0.14° bbox) resolves
  localization mismatches: Wien→1.69M, Milano→1.37M, München→1.49M
  Coverage: 70.5% → 98.5% (5,401/5,481 cities with population)
2026-02-27 08:52:35 +01:00
Deeman
e32f7ba4b8 docs: CHANGELOG + PROJECT.md for opportunity score data quality improvements
Documents Phase 0 (income ceiling fix), Phase 1b (overpass_tennis workflow),
and Phase 2b (dim_cities spatial population fallback, 70.5%→98.5% coverage).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:48:16 +01:00
Deeman
3aa30ab419 feat(sql): dim_cities — GeoNames spatial population fallback
Adds a coordinate-based population lookup as a fallback when string name
matching fails (~29% of cities). Uses bbox pre-filter (0.14° ≈ 15 km) then
ST_Distance_Sphere to find the nearest GeoNames location in the same country.

Fixes localization mismatches: Milano≠Milan, Wien≠Vienna, München≠Munich.

Population cascade: Eurostat EU > US Census > ONS UK > GeoNames string >
GeoNames spatial > 0.

Coverage: 70.5% → 98.5% (5,401 / 5,481 cities with population > 0).
Key cities before/after:
  Wien:   0 → 1,691,468
  Milano: 0 → 1,371,498
  München: already matched by string; verified still correct at 1,488,719

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:47:26 +01:00
Deeman
eef3ad2954 fix(tests): update stale test assertions to match current behavior
- test_draft/future_article: route intentionally redirects to parent (302) instead
  of bare 404 — rename tests and update assertion accordingly
- test_dashboard_has_content_links: /admin/templates and /admin/scenarios are
  subnav links shown only on content section pages, not the main dashboard;
  test now only checks /admin/articles which is always in the sidebar
- test_seo_sidebar_link: sidebar labels the link "Analytics" (not "SEO Hub"
  which is the page title); test now checks for /admin/seo URL presence

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:44:52 +01:00
Deeman
9507767de1 fix: resolve post-merge test failures (group_key + i18n)
- Remove body_html from _sync_static_articles INSERT (no such column in articles table)
- Remove empty report_q1_stat*_unit keys from EN+DE locales (i18n parity test forbids empty values)
- Update report_landing.html to remove stats-strip__unit spans referencing deleted keys
- Fix 0020_articles_unique_url_language migration to preserve group_key when recreating articles table (migration clobbered the column added by the preceding 0020_articles_group_key migration)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:23:49 +01:00
Deeman
e1fbfdf40e feat(ci): add Gitea Actions workflow
Mirrors the existing GitLab CI: one test job (pytest + ruff) gated by a
tag job that creates v<run_number> on master. Supervisor polls for new
tags to deploy — no SSH keys or deploy credentials in CI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:20:31 +01:00
Deeman
6586eca921 feat(infra): add overpass_tennis to supervisor workflows
Tennis extraction was missing from workflows.toml — only ran via the combined
`uv run extract` command, not automatically in production.

Schedule: monthly (same cadence as padel courts, OSM tennis data updates slowly).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:59:12 +01:00
Deeman
9835176e87 fix(sql): opportunity_score income ceiling /200→/35000 (economic power)
PPS values are 18k–37k but /200 normalisation caused LEAST(1.0, 115)=1.0
for ALL countries — 20pts flat uplift, zero differentiation.

Fix: /35000 creates real country spread:
  LU 20.0pts, DE 15.2pts, ES 12.8pts, GB 10.5pts (vs 20.0 everywhere before)

Default for missing data 100→15000 (developing-market assumption, ~0.43).
Header comment updated to document v2 formula behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:58:57 +01:00
Deeman
ce2171614b docs: CHANGELOG + PROJECT.md for group_key grouping + report PDF
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:56:34 +01:00
Deeman
2325e9b51e merge: group_key static article grouping + email-gated report PDF
Feature 1 — group_key for static article admin grouping:
- Migration 0020: group_key TEXT column + index on articles table
- _sync_static_articles(): auto-upserts data/content/articles/*.md on
  every /admin/articles load, reads cornerstone → group_key
- _get_article_list_grouped(): COALESCE(group_key, url_path) as group_id,
  so EN/DE static cornerstones pair into one row (pSEO unchanged)

Feature 2 — Email-gated State of Padel report PDF:
- data/content/articles/state-of-padel-q1-2026-{en,de}.md → reports/
- New reports/ blueprint: GET/POST /<lang>/reports/<slug> (email gate),
  GET /<lang>/reports/<slug>/download (PDF serve)
- Premium PDF: full-bleed navy cover, Padelnomics wordmark watermark at
  3.5% opacity (position:fixed, every page), gold/teal accents, Georgia
  headings, WeasyPrint CSS3 (no JS)
- make report-pdf target to build PDFs
- i18n EN + DE (26 keys each, native German via linguistic-mediation)
- /reports added to RESERVED_PREFIXES, data/content/reports/_build/ gitignored

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:53:15 +01:00
Deeman
dc58e96896 feat(i18n): add report_q1_* strings in EN and DE + gitignore _build/
- en.json: 26 new report_q1_* keys for landing page (eyebrow, subtitle,
  4 stat labels, TOC items, email gate copy, download CTA, privacy note)
- de.json: native German equivalents (Sprachmittlung — not calque)
- .gitignore: add data/content/reports/_build/ (generated PDFs, not committed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:52:06 +01:00
Deeman
b50ca5a8cd feat(reports): PDF build infrastructure — premium WeasyPrint template
- report.css: full-bleed navy cover, Padelnomics logo watermark at 3.5%
  opacity (position:fixed, repeats every page), gold/teal accents, Georgia
  headings, running headers via CSS named strings, metric boxes, insight-box
- report.html: Jinja2 template with cover stats, TOC, body, disclaimer
- build_report_pdf.py: builds EN+DE PDFs from data/content/reports/*.md
  (WeasyPrint, mistune, PyYAML; reads logo as file:// URI for watermark)
- Makefile: report-pdf target

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:49:40 +01:00
Deeman
336ca67fdc feat(reports): add email-gated report PDF blueprint
- New reports/ blueprint: GET/POST /<lang>/reports/<slug> (email gate),
  GET /<lang>/reports/<slug>/download (PDF serve)
- REPORT_REGISTRY dict for q1-2026 EN/DE PDFs
- report_landing.html: stat strip, TOC preview, email form/download CTA
- Registered in app.py as /<lang>/reports (before content catch-all)
- Added /reports to RESERVED_PREFIXES in content/routes.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:46:45 +01:00
Deeman
aea80f2541 feat(admin): add _sync_static_articles + group_key grouping
- _sync_static_articles(): auto-upserts data/content/articles/*.md into
  DB on every /admin/articles load; reads cornerstone → group_key
- _get_article_list_grouped(): now groups by COALESCE(group_key, url_path)
  so static EN/DE cornerstone articles pair into one row
- articles() route: calls _sync_static_articles() before listing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:44:04 +01:00
Deeman
36bd815525 fix(secrets): add secrets-updatekeys-prod target, use --input-type dotenv
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:40:03 +01:00
Deeman
250139598c feat: migration 0020 group_key + move state-of-padel to reports/
- Migration 0020: add group_key TEXT column + index to articles table
- DELETE state-of-padel rows from articles (slug collision, moving to reports)
- git mv state-of-padel-q1-2026-{en,de}.md → data/content/reports/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:36:00 +01:00
Deeman
dc2428eea4 fix(infra): fix setup_server.sh summary — correct bootstrap command + sops format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:31:14 +01:00
Deeman
834f9cb702 fix(infra): guard SSH config write, add ROTATE_KEYS for key rotation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:12:14 +01:00
Deeman
129a76e20a merge: market_score v3 + opportunity_score v2 recalibration 2026-02-27 07:03:08 +01:00
Deeman
721b2a37df docs: CHANGELOG + PROJECT.md for score recalibration (market_score v3 + opportunity_score v2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 06:58:48 +01:00
Deeman
10266c3a24 fix(sql): opportunity_score — supply gap ceiling 4→8/100k + doc findings
Raises supply gap ceiling from 4/100k to 8/100k in
location_opportunity_profile.sql. The original 4/100k hard cliff
truncated opportunity scores to 0 for any city with ≥4 courts/100k,
but our data undercounts ~87% of real courts (FIP: 17,300 Spanish
courts vs 2,239 in our DB). Raising to 8/100k gives a gentler gradient
and fairer partial credit when density data is incomplete.

Documents existing formula behaviour discovered during analysis:
- Income PPS: country-level constants (18k-37k range) saturate the
  /200 ceiling — all EU countries get flat 20/20 pts until city-level
  income data lands.
- Catchment NULL: DuckDB LEAST(1.0, NULL) = 1.0 (ignores nulls), so
  NULL nearest_padel_court_km already yields full 15 pts. COALESCE
  fallback is dead code but harmless.
- Tennis courts within 25km: dim_locations data is empty (all 0 rows)
  — 10-court threshold is correct for when data arrives, contributes
  0 pts everywhere for now.

Effective score impact: minimal (99% of locations have 0 courts/100k,
so supply gap was already at max). Only ~1,050 dense-court cities
see a score increase (from 0 gap pts to partial gap pts).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 06:57:57 +01:00
Deeman
5218717e8d refactor(infra): converge on setup+bootstrap pattern, fix systemd copy bug
- setup_server.sh: full rewrite to match materia/template pattern — adds Docker
  install, git/curl/ca-certificates apt install, age + sops install (arch-aware),
  uv install as service user, age keypair generation, SSH config write (root+chown);
  removes systemd unit copy (was buggy: copied before repo was cloned)
- NEW bootstrap_supervisor.sh: ~45 lines — age key check, clone/fetch, tag checkout,
  sops decrypt, uv sync, copy landing-backup + supervisor systemd units, enable + start
- deploy.sh: replace 53-line self-install preamble (sops/age install + keypair
  generation + exit-1 flow) with simple sops check + decrypt; Docker blue/green
  logic unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 06:57:00 +01:00
Deeman
88ed17484b feat(sql+templates): market_score v3 — log density + count gate
Fixes ranking inversion where Germany (1/100k courts) outscored Spain
(36/100k). Root causes: population/income were 55% of max before any
padel signal, density ceiling saturated 73% of cities, small-town
inflation (1 venue / 5k pop = 20/100k = full marks), and the saturation
discount actively penalised mature markets.

SQL (city_market_profile.sql):
- Supply development 40pts: log-scaled density LN(d+1)/LN(21) × count
  gate min(1, count/5). Ceiling 20/100k. Count gate kills small-town
  inflation without hard cutoffs (1 venue = 20%, 5+ = 100%).
- Demand evidence 25pts: occupancy if available; 40% density proxy
  otherwise. Separated from supply to avoid double-counting.
- Addressable market 15pts: population as context, not maturity.
- Economic context 10pts: income PPS (flat per country, low signal).
- Data quality 10pts.
- Removed saturation discount. High density = maturity.

Verified spot-check scores:
  Málaga (46v, 7.77/100k): 70.1  [was 98.9]
  Barcelona (104v, 6.17/100k): 67.4  [was 100.0]
  Amsterdam (24v, 3.24/100k): 58.4  [was 93.7]
  Bernau bei Berlin (2v, 5.74/100k): 43.9  [was 92.7]
  Berlin (20v, 0.55/100k): 42.2  [was 74.1]
  London (66v, 0.74/100k): 44.1  [was 75.5]

Templates (city-cost-de, country-overview, city-pricing):
- Color coding: green >= 55 (was 65), amber >= 35 (was 40)
- Intro/FAQ tiers: strong >= 55 (was 70), mid >= 35 (was 45)
- Opportunity interplay: market_score < 40 (was < 50) for white-space

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 06:40:12 +01:00
Deeman
8b74d6a76b merge: opportunity-score-integration — expose Marktpotenzial score in city + country articles 2026-02-26 20:46:42 +01:00
Deeman
0b3e1235fa docs: CHANGELOG + PROJECT.md for opportunity_score integration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 20:44:07 +01:00
Deeman
55d6c0ef15 feat(template): add opportunity_score to country-overview — stats strip, landscape, top-opp cities, FAQ
Both DE + EN language variants. All additions wrapped in {% if avg_opportunity_score %}
guards for graceful degradation.

Changes per language:
- Stats strip: avg Opportunity Score as 5th item (with auto-fit CSS now supporting this)
- Market Landscape section: paragraph on opportunity interplay (high opp + low market =
  first-mover signal; high both = proven demand + open sites)
- New section: "Top Locations by Investment Potential" — table of top_opportunity_names
  (distinct from top Market Score cities)
- New FAQ: explains Market Score vs Opportunity Score difference (avg values used)

DE copy written with linguistic mediation — native investor register, Du-form.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:35:57 +01:00
Deeman
1499dbeafe feat(template): add opportunity_score to city-cost-de — stats strip, intro, table, FAQ
Both DE + EN language variants. All additions wrapped in {% if opportunity_score %}
guards so cities without a GeoNames match degrade gracefully (score hidden).

Changes per language:
- Stats strip: Opportunity Score item after Market Score (same green/orange/red thresholds)
- Intro paragraph: contextual sentence with supply-gap / white-space interpretation
- Market Overview table: Opportunity Score row
- New FAQ: explains the difference between Market Score (maturity) and Opportunity Score
  (investment potential / supply gap)

DE copy written with linguistic mediation — native investor register, Du-form,
avoids calque from English.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:32:59 +01:00
Deeman
c6ce0aeaee feat(css): stats-strip auto-fit layout supports 4 or 5 metric items
Change from repeat(4, 1fr) to repeat(auto-fit, minmax(140px, 1fr)) so the
stats strip accommodates both 4-item (country overview) and 5-item (city
articles with opportunity score) layouts without breaking smaller widths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:29:57 +01:00
Deeman
7186d4582a feat(sql): thread opportunity_score from location_opportunity_profile into pSEO serving chain
- dim_cities: add geoname_id to geonames_pop CTE and final SELECT
  Creates FK between dim_cities (city-with-padel-venues) and dim_locations (all GeoNames),
  enabling joins to location_opportunity_profile for the first time.
- city_market_profile: pass geoname_id through base CTE and final SELECT
- pseo_city_costs_de: LEFT JOIN location_opportunity_profile on (country_code, geoname_id),
  add opportunity_score to output columns
- pseo_country_overview: add avg_opportunity_score, top_opportunity_score, top_opportunity_slugs,
  top_opportunity_names aggregates

Cities with no GeoNames name match get opportunity_score = NULL; templates guard with
{% if opportunity_score %}.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:29:57 +01:00
Deeman
88378cbfa7 chore: add secrets-encrypt-dev/prod targets to match template 2026-02-26 20:27:35 +01:00
Deeman
0c7b419fea merge: flat sidebar + horizontal subnav nav redesign 2026-02-26 20:21:28 +01:00
Deeman
98fc064a87 refactor: replace collapsible sidebar with flat nav + horizontal subnav
Simpler, clearer two-level navigation:
- Sidebar: 9 flat section-level links (no toggling), active at section level
- Horizontal subnav: compact tab strip renders above content for sections
  with multiple pages (Marketplace, Content, Email, System)
- Single-page sections (Dashboard, Suppliers, Billing, Analytics, Pipeline)
  get no subnav — one click, you're there
- Sidebar active state uses active_section not admin_page, so any sub-page
  correctly highlights its parent section
- Zero JS beyond the existing confirm dialog
- Unread badge remains on Email sidebar item + Inbox subnav tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 20:20:42 +01:00
Deeman
0fa2bf7c30 feat: admin articles grouped view, live stats, + bug fixes
Admin articles list:
- Group EN/DE language variants into a single row (grouped by url_path)
- Language chips (● EN/● DE) coloured by status: green=live, amber=scheduled, blue=draft
- Inline View ↗ (live only) and Edit buttons per variant — one-click access
- Filter by language switches back to flat single-row view
- Live HTMX polling of article counts while generation runs (every 3s, self-terminates)
- Table overflow fix: card gets overflow:hidden, table wrapped in overflow-x:auto scroll div

Bug fixes:
- X-Forwarded-Proto: pass $http_x_forwarded_proto through Nginx so Quart sees https
- pipeline_routes.py: fix relative import for analytics module (from .analytics → from ..analytics)
- Scheduled articles: redirect to parent path instead of 404 when not yet published
- city-cost-de: change priority_column from population to padel_venue_count
- Quote wizard step 4: make location_status required
- Article generation: use COUNT(*) instead of 501-sentinel hack for row counts
- Makefile: pin Tailwind v4.1.18, add dev/help targets, uv run python, .PHONY

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 20:17:28 +01:00
Deeman
ee488b6aca merge: admin nav collapsible sidebar + billing products page
# Conflicts:
#	web/src/padelnomics/admin/templates/admin/base_admin.html
2026-02-26 20:11:53 +01:00
Deeman
a028184a85 feat: admin billing products page — /admin/billing/products
Read-only overview of all Paddle products with live metrics:
- Stats cards: active subscriptions, estimated MRR (yearly÷12),
  active boosts, completed business plan exports
- Products grouped by category: Supplier Plans, Planner Plans,
  Boosts (sub + one-time), Credit Packs, One-time Products
- Per-product: name, key, price, type badge, active count, Paddle IDs
- Empty-state message when paddle_products table is unpopulated
- PRODUCT_CATEGORIES constant in routes.py defines grouping + ordering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 19:49:55 +01:00
Deeman
82591514cd feat: collapsible admin sidebar — groups, section-map, localStorage state
Replaces flat 20-link sidebar with collapsible section groups:
- Multi-item sections (Marketplace, Content, Email, System) are
  collapsible with animated chevron; active section always expands
- Single-item sections (Dashboard, Suppliers, Billing, Analytics,
  Pipeline) render as direct links — no toggle overhead
- pSEO merged into Content; Users moved into System; new Billing slot
- Unread badge surfaces on Email group header when collapsed
- localStorage persists per-section open/closed state (key: admin_sidebar_v1)
- Mobile: group headers hidden, all items shown in horizontal scroll
  (preserves existing mobile behavior exactly)
- section_map Jinja dict derives active_section from existing admin_page
  — no route changes needed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 19:49:46 +01:00
Deeman
a98903646d merge: pricing-overhaul — Basic free, card color €59, BP PDF €149, supplier page CRO, lead-back guarantee 2026-02-26 15:49:57 +01:00
Deeman
9dd0f30014 docs: pricing overhaul + lead-back guarantee — CHANGELOG + PROJECT.md
CHANGELOG: add Added section (guarantee, CRO restructure, credits-only callout,
ROI line) and Changed section (Basic free, card color €59, BP PDF €149,
hero CTA, comparison table, EN/DE translations, setup_paddle.py)

PROJECT.md:
- Correct Done section prices (Growth €199, Pro €499)
- Add Done entries: pricing overhaul, lead-back guarantee
- Add 3 Decisions Log entries (Basic free, guarantee credit-only, static ROI)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 15:24:06 +01:00
Deeman
cc43d936f0 feat: lead-back guarantee — one-click credit refund after 3 days no response
Backend:
- Migration 0020: add guarantee_claimed_at, guarantee_contact_method to lead_forwards
- credits.py: refund_lead_guarantee() — validates 3–30 day window, reverses credit
  spend via ledger entry (event_type='guarantee_refund'), sets status='no_response'
- GuaranteeAlreadyClaimed, GuaranteeWindowClosed exceptions
- Route: POST /suppliers/leads/<forward_id>/guarantee-claim — HTMX endpoint,
  returns updated lead card partial with success message
- _get_lead_feed_data: pull forward_id, forward_created_at, guarantee_claimed_at
  so dashboard feed can show/hide the guarantee button per-lead

UI:
- lead_card_unlocked.html: "Lead didn't respond" button rendered client-side via
  JS (3–30 day window check in browser), shows contact method radio + submit
- Success state and already-claimed state handled in partial

EN/DE: remove empty sup_credits_only_post key (fails i18n parity test)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 15:22:52 +01:00
Deeman
a1e2a5aa8d content: update EN+DE copy for pricing overhaul
EN changes:
- sup_meta_desc: remove "from €39/mo", lead with free listing + qualified leads
- sup_hero_cta / sup_cta_btn: "See Plans & Pricing" → "Get Started Free"
- sup_basic_dir: "Directory listing" → "Free forever"
- sup_basic_cta: "Get Listed" → "List Your Company Free"
- sup_yearly_note_basic: remove €349 price → "Free forever"
- sup_boosts_sub: add card color €59/mo note
- sup_faq_a2: update Basic from €39 to free, remove Basic yearly price
- sup_faq_q5/a5: rename to include Lead-Back Guarantee; add guarantee mechanic

New keys (EN + DE): sup_cta_btn, sup_basic_free_label, sup_pricing_eur_note,
sup_guarantee_h2/p/badge, sup_leads_section_h2/sub, sup_leads_unlock_cta,
sup_roi_line, sup_credits_only_pre/cta/post, sup_step1_free_forever,
sd_guarantee_btn/contact_label/email/phone/both/submit/success/window_error/already_claimed

DE: all above translated with native German register (du, compound nouns,
no calque constructions)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 15:17:30 +01:00
Deeman
548ea7c491 feat: supplier page value-first restructure + CRO elements
- Reorder sections: why → guarantee → lead preview → social proof → pricing
- Change hero/final CTA links to signup URL (not #pricing)
- Add lead-back guarantee section (shield, green accent)
- Add static ROI line (dark callout, grounded in research)
- Add credits-only callout below pricing grid
- Basic tier shows "Free" / "Free forever" instead of €0
- Card color boost shows €59/mo (was €19)
- Comparison table shows €1,799/yr with "(yearly plan)" annotation
- Remove credit mechanics explainer from How It Works (simpler)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 15:11:52 +01:00
Deeman
82567b53ff fix: align pricing with strategy — Basic free, card color €59, BP PDF €149
- supplier_basic: monthly_price/yearly_price → 0 (free tier, no Paddle subscription)
- boost_card_color: price 19 → 59 (aligns with MARKETING.md)
- setup_paddle.py: Basic products commented out, card_color 1900→5900, business_plan 9900→14900
- export.html: business plan PDF price €99 → €149

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 15:08:41 +01:00
Deeman
518a4e4fe2 docs(claude): add uv workspace management + data modeling patterns
- uv workspace section: sync all-packages, add deps, create new source package
- Data modeling patterns: foundation-as-ontology (dim_venues, dim_cities
  conform cross-source identifiers); extraction pattern notes (state SQLite)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:15:24 +01:00
Deeman
749fc27594 merge: bp-and-articles — C4 market report + launch marketing assets
C4 State of Padel Q1 2026:
- data/content/articles/state-of-padel-q1-2026-de.md (~2,500w DE)
- data/content/articles/state-of-padel-q1-2026-en.md (~2,500w EN)
- FIP WPR 2024/2025 + Playtomic/PwC + Padelnomics pipeline data embedded

All batch 1 cornerstone articles (C2/C3/C5/C6/C7/C8) moved from
scratch/articles/ to data/content/articles/ (CMS-readable location).

Launch marketing assets in marketing/:
- founding-member-deal.md
- supplier-outreach-emails.md (3 templates × DE + EN)
- linkedin-posts-launch-week.md (5 DE posts)
- linkedin-approach.md (company page setup + strategy)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 01:19:43 +01:00
Deeman
19fb939fec refactor: move articles to data/content/articles, marketing to marketing/
Articles now live at data/content/articles/{slug}.md — this is the path the
admin CMS reads from (admin/routes.py:1861) when rebuilding manual articles
via the publish pipeline.

Marketing assets moved to marketing/ at the project root.

All 14 article files (C2–C8 + C4 DE/EN) and 4 marketing files relocated from
scratch/ where they never belonged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:23:24 +01:00
Deeman
b4f3baceaa content: add C4 market report + launch marketing assets
C4 articles:
- scratch/articles/state-of-padel-q1-2026-de.md — German State of Padel Q1 2026
  report (~2,500w); DE version front-loads Germany section; Wirtschaftsjournalismus
  register; FIP + Playtomic/PwC + Padelnomics pipeline data embedded
- scratch/articles/state-of-padel-q1-2026-en.md — English adaptation (~2,500w);
  Germany as case study; international audience framing

Marketing assets:
- scratch/marketing/founding-member-deal.md — founding member deal structure
  (20 slots, €990/yr locked 3 years, Professional tier at Basic price, +rationale)
- scratch/marketing/supplier-outreach-emails.md — 3 email templates × DE + EN
  (cold intro, founding member pitch, day-7 follow-up); Sie-register throughout
- scratch/marketing/linkedin-posts-launch-week.md — 5 DE launch-week posts
  (<300w each, max 5 hashtags, company-page appropriate)
- scratch/marketing/linkedin-approach.md — company page setup guide + engagement
  strategy (no personal exposure, supplier tagging, SEO backlink approach)

Data sources used: FIP WPR 2024/2025, Playtomic/PwC Global Padel Report 2025,
Padelnomics DuckDB pipeline (12,441 venues / 80 countries / 5,492 cities).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:09:36 +01:00
Deeman
1fc348f10c feat(extract): lower US city population threshold to 10K
878 → 4212 cities. Broadens coverage to match the granularity of
Eurostat and GeoNames data for smaller metro markets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:02:40 +01:00
Deeman
f58421ff12 docs(research): remove external links from README 2026-02-25 23:55:59 +01:00
Deeman
4d5fd08201 docs(research): add Playtomic/PwC Global Padel Report 2025 extract
85K text extraction of the gated 39MB PDF (already in ~/Downloads).
Notes PDF location in README. Removes landing page placeholder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 23:55:28 +01:00