Commit Graph

476 Commits

Author SHA1 Message Date
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
Deeman
3a76a7a633 docs(research): add raw source HTML + full 2024 PDF extract
Sources fetched during Q1 2026 padel research session:
- Padel Magazine premier padel dotation/prize pool article
- premierpadel.com news page
- Playtomic/PwC gated report landing page
- padelfip.com prize money search results
- Fuller pdftotext extraction of FIP 2024 report (376K)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 23:53:14 +01:00
Deeman
bb514dcc13 docs(research): reorganise Q1 2026 padel research into subfolder
Moves brief into research/state-of-padel-q1-2026/ and adds source files:
- FIP 2024 PDF (8 MB)
- Extracted text from both FIP 2024 + 2025 PDFs
- README with download link for 2025 PDF (44 MB, not committed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 23:36:59 +01:00
Deeman
783da8db2a merge worktree-email-templates → master
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 23:33:13 +01:00
Deeman
4dbff0f388 docs(research): add State of Padel Q1 2026 research brief
FIP 2024 + 2025 report data: player population, courts, federations,
prize pools, broadcast stats, and data source scrapeability assessment.
Raw PDF text at /tmp/fip_2024_text.txt and /tmp/fip_2025_text.txt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 23:32:47 +01:00
Deeman
c772d814de fix(pipeline): query shortcuts + schema preview + serving meta fallback
- Add Shift+Enter shortcut to execute query (alongside Cmd/Ctrl+Enter)
- Add ▶ preview button to schema sidebar tables: populates editor with
  SELECT * FROM serving.<table> LIMIT 100 and auto-submits
- Update hint text to show "Shift+Enter to run"
- Overview tab: fall back to information_schema when _serving_meta.json
  is absent instead of showing error message; row counts show "—"
- Dashboard stat cards: same fallback — query DuckDB for table count

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 23:32:15 +01:00
Deeman
e61aaa574b merge: proxy-pinned UA identities + honest bot UA for public APIs
# Conflicts:
#	extract/padelnomics_extract/src/padelnomics_extract/_shared.py
2026-02-25 22:12:34 +01:00
Deeman
c5b46376af feat(extract): proxy-pinned UA identities + honest bot UA for public APIs
Replace single hardcoded Chrome 131 UA with:
- BOT_UA: honest padelnomics-bot UA for Overpass, Eurostat, GeoNames etc.
- _UA_POOL + ua_for_proxy(): deterministic browser UA per proxy URL so each
  IP presents a consistent, distinct fingerprint across runs.

Public-API extractors (shared session, no proxy) now send BOT_UA.
Playtomic extractors (proxy-backed) each get a stable pool UA keyed on
their proxy URL hash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 22:08:00 +01:00
Deeman
73330b1aaa fix: add Overpass mirror fallback to eliminate 504 failures
Adds OVERPASS_MIRRORS list (overpass-api.de, kumi.systems, openstreetmap.ru)
and a post_overpass() helper in _shared.py that tries mirrors in order,
logging a warning on each failure and re-raising the last RequestException
if all mirrors fail. Both overpass.py and overpass_tennis.py now call
post_overpass() instead of hard-coding the primary URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 21:29:51 +01:00
Deeman
61a3335197 fix(dev): launch Flatpak Chrome/Firefox for incognito browser window 2026-02-25 21:29:04 +01:00
Deeman
4235009db9 fix: CSV import drops contact_email; add incognito browser launch to dev_run.sh
- outreach_import(): contact_email was extracted + used for dedup but
  missing from the INSERT — added it to the column list and values tuple
- test_import_creates_prospects: strengthen to assert contact_email is
  actually persisted (regression test for the above bug)
- dev_run.sh: after server ready, open incognito/private browser window
  at dev-login URL; tries google-chrome → chromium → firefox in order

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 21:22:49 +01:00
Deeman
d9a645976d fix(tests): correct build path in test_article_create_manual
Route writes to BUILD_DIR/<language>/<slug>.html but test was checking
BUILD_DIR/<slug>.html (missing language subdirectory). Default language
is "en" so correct path is BUILD_DIR/en/manual-art.html.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 19:46:01 +01:00
Deeman
ad1da5c335 feat: outreach follow-up scheduling, activity timeline, and pSEO noindex (migration 0025)
Feature A — Outreach follow-up + activity timeline:
- follow_up_at column on suppliers (migration 0025)
- HTMX date picker on outreach rows, POST /admin/outreach/<id>/follow-up
- Amber due-today banner on /admin/outreach with ?follow_up=due filter
- get_follow_up_due_count() for dashboard widget
- Activity timeline on /admin/suppliers/<id>: merges sent + received emails by contact_email

Feature B — pSEO article noindex:
- noindex column on articles (migration 0025)
- NOINDEX_THRESHOLDS per-template lambdas in content/__init__.py
- generate_articles() evaluates threshold and stores noindex=1 for thin-data articles
- <meta name="robots" content="noindex, follow"> in article_detail.html
- Sitemap excludes noindex articles (AND noindex = 0)
- pSEO dashboard noindex count card + article row badge

Tests: 49 new tests (29 outreach, 20 noindex), 1377 total, 0 failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 17:51:38 +01:00
Deeman
3b248871c2 docs: update CHANGELOG and PROJECT.md for follow-up scheduling, activity timeline, and noindex features
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 16:12:56 +01:00
Deeman
ea06dd0689 feat(outreach+pseo): follow-up scheduling, activity timeline, noindex articles (subtasks 1-9)
Feature A — Outreach follow-up scheduling + activity timeline:
- Migration 0025: follow_up_at column on suppliers
- POST /admin/outreach/<id>/follow-up route (HTMX date picker, updates row)
- get_follow_up_due_count() query + amber banner on /admin/outreach
- ?follow_up=due / ?follow_up=set filters in get_outreach_suppliers()
- Follow-up column in outreach_results.html + outreach_row.html date input
- Activity timeline on supplier_detail.html — merges email_log (sent outreach)
  and inbound_emails (received) by contact_email, sorted by date

Feature B — pSEO article noindex:
- Migration 0025: noindex column on articles (default 0)
- NOINDEX_THRESHOLDS dict in content/__init__.py (per-template thresholds)
- generate_articles() upsert now stores noindex = 1 for thin-data articles
- <meta name="robots" content="noindex, follow"> in article_detail.html (conditional)
- sitemap.py excludes noindex=1 articles from sitemap.xml
- pSEO dashboard noindex count card; article_row.html noindex badge
- 73 new tests (test_outreach.py + test_noindex.py), 1377 total, 0 failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 16:12:21 +01:00
Deeman
b73386b9b6 fix: correct export_serving invocation in all docs
`-m padelnomics.export_serving` doesn't resolve because src/ is not
installed as a package in the workspace. Use the direct script path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 16:06:31 +01:00
Deeman
cee2e9babc merge: standardise recheck availability to JSONL + update docs 2026-02-25 15:45:23 +01:00
Deeman
606d6f7d4c merge(outreach): admin outreach pipeline + separate sending domain
# Conflicts:
#	CHANGELOG.md
2026-02-25 15:29:20 +01:00
Deeman
b33dd51d76 feat: standardise recheck availability to JSONL output
- extract_recheck() now writes availability_{date}_recheck_{HH}.jsonl.gz
  (one venue per line with date/captured_at_utc/recheck_hour injected);
  uses compress_jsonl_atomic; removes write_gzip_atomic import
- stg_playtomic_availability: add recheck_jsonl CTE (newline_delimited
  read_json on *.jsonl.gz recheck files); include in all_venues UNION ALL;
  old recheck_blob CTE kept for transition
- init_landing_seeds.py: add JSONL recheck seed alongside blob seed
- Docs: README landing structure + data sources table updated; CHANGELOG
  availability bullets updated; data-sources-inventory paths corrected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 14:52:47 +01:00
Deeman
efaba2cb76 feat(outreach): admin outreach pipeline + separate sending domain (all subtasks)
Adds cold B2B supplier outreach pipeline isolated from transactional emails.

Subtask 1 — Migration + constants:
- Migration 0024: 4 new columns on suppliers (outreach_status, outreach_notes,
  last_contacted_at, outreach_sequence_step); NULL status = not in pipeline
- EMAIL_ADDRESSES["outreach"] = hello.padelnomics.io (separate reputation domain)
- "outreach" added to EMAIL_TYPES

Subtask 2 — Query functions + routes:
- get_outreach_pipeline() — counts by status for pipeline cards
- get_outreach_suppliers() — filtered list with status/country/search
- GET /admin/outreach — pipeline dashboard
- GET /admin/outreach/results — HTMX partial
- POST /admin/outreach/<id>/status — inline status update
- POST /admin/outreach/<id>/note — inline note edit
- POST /admin/outreach/add-prospects — bulk set from supplier list

Subtask 3 — CSV import:
- GET/POST /admin/outreach/import
- Accepts name+contact_email (required), country_code/category/website (optional)
- Deduplicates by contact_email, auto-generates slug, capped at 500 rows

Subtask 4 — Templates:
- outreach.html (pipeline cards + HTMX filter + results table)
- outreach_import.html (CSV upload form)
- partials/outreach_results.html, partials/outreach_row.html
- base_admin.html: Outreach sidebar link
- suppliers.html + supplier_results.html: checkbox column + bulk action bar

Subtask 5 — Compose integration:
- email_compose() GET: ?from_key=outreach&email_type=outreach&supplier_id=<id>
  pre-fills from-addr, stores hidden fields, defaults wrap=0 (plain text)
- email_compose() POST: on outreach send, advances prospect→contacted,
  increments outreach_sequence_step, sets last_contacted_at
- email_compose.html: hidden email_type + supplier_id fields, outreach banner
- supplier_detail.html: outreach card (status, step, last contact, send button)

Subtask 6 — Tests:
- 44 tests in web/tests/test_outreach.py covering: constants, access control,
  query functions, dashboard, HTMX partial, status update, note update,
  add-prospects, CSV import, compose pre-fill, compose pipeline update

Subtask 7 — Docs:
- CHANGELOG.md and PROJECT.md updated

Manual step after deploy: add hello.padelnomics.io in Resend dashboard + DNS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 14:06:53 +01:00
Deeman
de67d41fd4 merge: bp-and-articles — 12 cornerstone articles + KfW PDF overhaul
From worktree-bp-and-articles:

Content (12 articles, Batch 1):
  C2 Cost Bible (DE+EN), C3 Business Plan for Banks (DE+EN),
  C5 Location Guide (DE+EN), C6 Financing Guide (DE+EN),
  C7 Risk Register (DE+EN), C8 Build Guide (DE+EN)
  All written natively (linguistic-mediation for DE), frontmatter complete.

CMS fix:
  Article form now includes language selector; seo_head generated +
  stored for manually created articles; build path is lang-prefixed.

Business Plan PDF overhaul (KfW Gründerkredit-ready):
  - compute_sensitivity() extracted as reusable function
  - matplotlib SVG charts (P&L + 12-month cash flow)
  - Opening balance sheet, use-of-funds, sensitivity analysis
  - Market analysis auto-populated from DuckDB city data
  - Pre-export details form (/planner/export/details)
  - Migration 0020: bp_details_json on scenarios table
  - Complete PDF redesign: Precision Finance aesthetic
    (navy/gold, Georgia headings, cover page, TOC, 15 sections)
  - 28 new translation keys in en.json + de.json

Docs:
  SPORTPLATZWELT_RESEARCH.md + CUSTOMER_CHANNELS.md updated
  with verified contacts and trade show dates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

# Conflicts:
#	web/src/padelnomics/admin/routes.py
#	web/src/padelnomics/locales/de.json
#	web/src/padelnomics/locales/en.json
2026-02-25 13:58:16 +01:00
Deeman
37d4886e79 feat(pdf): KfW Gründerkredit-ready business plan overhaul
Part B of the bp-and-articles plan. Full overhaul of the PDF generation
engine and template to produce bank-ready documents.

New sections added (all required by KfW application):
- Founder/Management Profile (Gründerprofil)
- Business Description (Vorhabensbeschreibung)
- Market Analysis (auto-populated from DuckDB city data when available)
- Use of Funds (Mittelverwendungsplan)
- Opening Balance Sheet (Eröffnungsbilanz)
- Sensitivity Analysis (utilization + price tables)
- Risk Analysis (templated 6-risk register)

Data & computation:
- Extract compute_sensitivity() from augment_d() → reusable function
  shared by web planner and PDF generator
- Add matplotlib chart generation: _generate_pnl_chart_svg() +
  _generate_cashflow_chart_svg() — SVG embedded in WeasyPrint HTML
- Add _compute_opening_balance() + _compute_use_of_funds() helpers
- Add _fetch_market_data() — queries DuckDB serving.city_market_overview

Business plan details form:
- New /planner/export/details route (GET/POST)
- New export_details.html template — 5 narrative sections with placeholders
- Migration 0020: bp_details_json TEXT column on scenarios table
- generate_business_plan() loads bp_details_json + calls _fetch_market_data()

Design (plan.html + plan.css):
- Complete redesign: "Precision Finance" aesthetic
- Navy (#0F2651) + warm gold (#C9922C) + Georgia serif headings
- Full-bleed branded cover page with sidebar
- Table of contents page
- Gold-accented section headers (h2 with left border)
- Professional @page running headers/footers
- Executive summary 3-column card grid
- 2-column opening balance sheet
- Sensitivity tables with highlight-row for target/base values
- All tables: navy thead, alternating rows, professional total rows

Translations: 28 new bp_ keys in en.json and de.json for all new
section headings and table labels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:53:32 +01:00
Deeman
a86f1ecd3a fix(staging): enforce grain dedup in resources + opening_hours + skip old blob in tenants
Both stg_playtomic_resources and stg_playtomic_opening_hours lacked QUALIFY ROW_NUMBER()
dedup despite declaring a grain. When both tenants.json.gz (old) and tenants.jsonl.gz (new)
exist for the same month, the UNION ALL produced exactly 2× rows.

Fixes:
- stg_playtomic_resources: QUALIFY ROW_NUMBER() OVER (PARTITION BY tenant_id, resource_id)
- stg_playtomic_opening_hours: QUALIFY ROW_NUMBER() OVER (PARTITION BY tenant_id, day_of_week)
- playtomic_tenants.py: skip if old blob OR new JSONL already exists for the month,
  preventing same-month dual-format writes that trigger the duplicate

Row counts after fix: ~43.8K resources, ~93.4K opening_hours (was 87.6K, 186.8K).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 13:41:23 +01:00
Deeman
bf04fa1da0 content: write all 12 Batch 1 cornerstone articles (C2, C3, C5, C6, C7, C8)
Six topics × 2 languages (DE + EN) = 12 markdown files in scratch/articles/:

C2 Cost Bible: padel-halle-kosten-de, padel-hall-cost-guide-en
C3 Business Plan for Banks: padel-business-plan-bank-de, padel-business-plan-bank-requirements-en
C5 Location Guide: padel-standort-analyse-de, padel-hall-location-guide-en
C6 Financing Guide: padel-halle-finanzierung-de, padel-hall-financing-germany-en
C7 Risk Register: padel-halle-risiken-de, padel-hall-investment-risks-en
C8 Build Guide: padel-halle-bauen-de, padel-hall-build-guide-en

All articles written natively (linguistic-mediation skill for DE), include
[scenario:padel-halle-6-courts:full] markers where relevant, frontmatter with
slug/language/url_path/meta_description/cornerstone fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:40:13 +01:00
Deeman
fe9ac1bf78 docs: add SPORTPLATZWELT_RESEARCH.md + update CUSTOMER_CHANNELS.md
SPORTPLATZWELT_RESEARCH.md: full research report on Sportplatzwelt (Stadionwelt GmbH)
— business model, audience, padel coverage, pricing, event contacts, recommendation.

CUSTOMER_CHANNELS.md updates:
- Verified DTB padel contacts (Zamani Badawere, Fabienne Bretz, Toralf Bitzer)
- Add mypadel.de, Tennis Magazin, Sportplatzwelt as publications
- Correct Padel Magazine to FR-only (not EN/ES)
- All trade show dates verified: Padel World Summit May 2026, FSB 2027,
  ISPO moved to Amsterdam Nov 2026, FIBO Apr 2026, Sportplatzwelt LIVE Jun 2026
- Elevate Padel World Summit to Tier 1 (dedicated padel B2B event)
- Update Top 10 Priority Actions with verified contacts and Padel World Summit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 13:25:25 +01:00
Deeman
6cb0fb32ec feat(cms): add language field + seo_head to manual article creation
- Add language selector (en/de) to article create/edit form
- Store language and generated seo_head in articles table on CREATE and UPDATE
- Write HTML build to BUILD_DIR/{lang}/{slug}.html (consistent with pSEO)
- article_detail.html: render article.seo_head when present (canonical,
  hreflang, OG, JSON-LD Article) — falls back to inline for legacy articles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 13:25:17 +01:00
Deeman
b177d2c377 feat(admin): Pipeline Console — 4-tab data pipeline operational dashboard
Adds a new "Pipeline" section to the admin panel at /admin/pipeline with
full visibility into the data extraction → transform → serve pipeline.

Tabs:
- Overview: per-extractor status grid, serving table freshness, landing
  zone file stats
- Extractions: filterable run history, mark-stale action for stuck runs,
  trigger-all button
- Catalog: serving schema browser with lazy-loaded column types and 10-row
  samples
- Query: dark-themed SQL editor with schema sidebar, keyword blocklist,
  1k-row cap, 10s timeout, HTMX result swapping

Also:
- Adds execute_user_query() to analytics.py for the query editor
- Registers pipeline_bp in app.py
- Adds run_extraction background task to worker.py
- 29 tests (all passing), CHANGELOG + PROJECT.md updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 13:05:53 +01:00
Deeman
d637687795 feat(pipeline): tests, docs, and ruff fixes (subtask 6/6)
- Add 29-test suite for all pipeline routes, data helpers, and query
  execution (test_pipeline.py); all 1333 tests pass
- Fix ruff UP041: asyncio.TimeoutError → TimeoutError in analytics.py
- Fix ruff UP036/F401: replace sys.version_info tomllib block with
  plain `import tomllib` (project requires Python 3.11+)
- Fix ruff F841: remove unused `cutoff` variable in pipeline_overview
- Update CHANGELOG.md with Pipeline Console entry
- Update PROJECT.md: add Pipeline Console to Admin Panel done list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 13:02:51 +01:00
Deeman
8f8f7f7acb feat(pipeline): query editor tab templates
- partials/pipeline_query.html: dark-themed SQL textarea (navy bg, Commit
  Mono, 12px border-radius, electric blue focus glow) + schema sidebar
  (collapsible per-table column lists with types) + controls bar (Execute,
  Clear, limit/timeout note) + Tab-key indent + Cmd/Ctrl+Enter submit
- partials/pipeline_query_results.html: results table with sticky headers,
  horizontal scroll, row count + elapsed time metadata, truncation warning,
  error display in red monospace card

Subtask 5 of 6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:55:20 +01:00
Deeman
5b48a11e01 feat(pipeline): catalog tab templates
- partials/pipeline_catalog.html: accordion list of serving tables with
  row count badges, column count, click-to-expand lazy-loaded detail
- partials/pipeline_table_detail.html: column schema grid + sticky-header
  sample data table (10 rows, truncated values with title attribute)
- JS: toggleCatalogTable() + htmx.trigger(content, 'revealed') for
  lazy-loading detail only on first open

Subtask 4 of 6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:54:21 +01:00
Deeman
947a1a778e feat(pipeline): extractions tab template
- Filterable extraction run history table (extractor + status dropdowns,
  HTMX live filter via 'change' trigger)
- Status badges with stale row highlighting (amber background)
- 'Mark Failed' button for stuck 'running' rows (with confirm dialog)
- 'Run All Extractors' trigger button
- Pagination via hx-vals

Subtask 3 of 6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:53:36 +01:00
Deeman
cac876e48f feat(pipeline): dashboard + overview tab templates
- pipeline.html: 4 stat cards (total runs, success rate, serving tables,
  last export) + stale-run warning banner + tab bar (Overview/Extractions/
  Catalog/Query) + tab container (lazy-loaded via HTMX on page load)
- partials/pipeline_overview.html: extraction status grid (one card per
  workflow with status dot, schedule, last run timestamp, error preview),
  serving freshness table (row counts per table), landing zone file stats

Subtask 2 of 6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:53:02 +01:00
Deeman
060cb9b32e feat(pipeline): scaffold Pipeline Console blueprint + sidebar + app registration
- New pipeline_routes.py blueprint (url_prefix=/admin/pipeline) with:
  - All 9 routes (dashboard, overview, extractions, catalog, query editor)
  - Data access functions: state DB (sync+to_thread), serving meta, landing FS, workflows.toml
  - execute_user_query() added to analytics.py (columns+rows+error+elapsed_ms)
  - Query security: blocklist regex, 10k char limit, 1000 row cap, 10s timeout
- Add 'Pipeline' sidebar section to base_admin.html (between Analytics and System)
- Register pipeline_bp in app.py
- Add run_extraction task handler to worker.py

Subtask 1 of 6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:44:03 +01:00
Deeman
1905844cd2 merge: bring worktree up to master before pipeline console work 2026-02-25 12:40:37 +01:00
Deeman
4fbe53d7ca merge: JSONL streaming landing format + regional overpass_tennis splitting
Converts extractors from large JSON blobs to streaming JSONL (.jsonl.gz),
eliminating in-memory accumulation, maximum_object_size workarounds, and
the playtomic availability consolidation step.

- compress_jsonl_atomic(): 1MB-chunk streaming compression, atomic rename
- playtomic_tenants → tenants.jsonl.gz (one tenant per line after dedup)
- playtomic_availability → availability_{date}.jsonl.gz (working file IS the output)
- geonames → cities_global.jsonl.gz (eliminates 30MB blob)
- overpass_tennis → 10 regional bbox queries + courts.jsonl.gz with crash recovery
- All modified staging SQL uses UNION ALL (JSONL + blob) for smooth transition
- init_landing_seeds.py: bootstrap seeds for both formats in 1970/01

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

# Conflicts:
#	CHANGELOG.md
2026-02-25 12:34:03 +01:00