Commit Graph

194 Commits

Author SHA1 Message Date
Deeman
4e82907a70 refactor(transform): conform geographic dimension hierarchy via city_slug
Propagates the conformed city key (city_slug) from dim_venues through the
full pricing pipeline, eliminating 3 fragile LOWER(TRIM(...)) fuzzy string
joins with deterministic key joins.

Changes (cascading, task-by-task):
- dim_venues: add city_slug computed column (REGEXP_REPLACE slug derivation)
- dim_venue_capacity: join foundation.dim_venues instead of stg_playtomic_venues;
  carry city_slug alongside country_code/city
- fct_daily_availability: carry city_slug from dim_venue_capacity
- venue_pricing_benchmarks: carry city_slug from fct_daily_availability;
  add to venue_stats GROUP BY and final SELECT/GROUP BY
- city_market_profile: join vpb on city_slug = city_slug (was LOWER(TRIM))
- planner_defaults: add city_slug to city_benchmarks CTE; join on city_slug
- pseo_city_pricing: join city_market_profile on city_slug (was LOWER(TRIM))
- pipeline_routes._DAG: dim_venue_capacity now depends on dim_venues, not stg_playtomic_venues

Result: dim_venues.city_slug → dim_cities.(country_code, city_slug) forms a
fully conformed geographic hierarchy with no fuzzy string comparisons.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 13:23:03 +01:00
Deeman
160c2c6f7b feat(pipeline): add Lineage tab — server-rendered SVG DAG visualization
Adds a 5th tab to the admin pipeline page showing the full 3-layer
SQLMesh data lineage: 28 models, 35 edges across staging / foundation /
serving swim lanes.

- _DAG: canonical model dependency dict in pipeline_routes.py;
  update when models are added/removed
- _classify_layer(): derives layer from name prefix (stg_/dim_fct_/rest)
- _render_lineage_svg(): pure Python SVG generator — 3-column swim lane
  layout, bezier edges, color-coded per layer (green/blue/amber),
  no external dependencies
- /lineage route: HTMX tab handler
- pipeline_lineage.html: partial with SVG embed + vanilla JS hover
  effects (highlight connected edges, dim unrelated)
- pipeline.html: 5th "Lineage" tab button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 11:55:39 +01:00
Deeman
c269caf048 fix(lint): resolve all ruff E402/F401/F841/I001 errors
- Move logger= after imports in planner/routes.py and setup_paddle.py
- Add # noqa: E402 to intentional post-setup imports (app.py, core.py,
  migrate.py, test_supervisor.py)
- Fix unused cursor variables (test_noindex.py) → _
- Move stray csv import to top of test_outreach.py
- Auto-sort import blocks (test_email_templates, test_noindex, test_outreach)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 11:52:02 +01:00
Deeman
fa7604301a fix(pipeline): fix mark-failed CSS bug + add per-extractor run buttons
- Redirect pipeline_mark_stale to pipeline_dashboard (full page) instead
  of pipeline_extractions (partial), fixing the broken CSS on form submit
- pipeline_trigger_extract accepts optional 'extractor' POST field;
  validates against workflows.toml names to prevent injection, passes
  as payload to enqueue("run_extraction")
- handle_run_extraction dispatches to per-extractor CLI entry point
  (extract-overpass, extract-eurostat, etc.) when extractor is set,
  falls back to umbrella 'extract' command otherwise
- pipeline_overview.html: add Run button to each workflow card header,
  posting extractor name with CSRF token to pipeline_trigger_extract

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 11:37:39 +01:00
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
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
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
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
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
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
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
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
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
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
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
606d6f7d4c merge(outreach): admin outreach pipeline + separate sending domain
# Conflicts:
#	CHANGELOG.md
2026-02-25 15:29:20 +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
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
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
578a409893 feat(email-templates): tests, docs, and fix quote_verification sample data (subtask 8)
- Add 50 tests in test_email_templates.py:
  - TestRenderEmailTemplate: all 11 registry templates render in EN + DE
    without error; checks DOCTYPE, wordmark, font, CTA color, template-
    specific content (heat badges, brief rows, weekly digest loop, etc.)
    and registry structure
  - TestEmailGalleryRoutes: access control, gallery list (all labels
    present, preview links), preview pages (EN/DE/nonexistent/invalid-lang),
    compose preview endpoint (plain + wrapped + empty body)
- Fix _quote_verification_sample: add missing recap_parts key — StrictUndefined
  raised on the {% if recap_parts %} check when the variable was absent
- Update CHANGELOG.md: document email template system (renderer, base,
  macros, 11 templates, registry, gallery, compose preview, removed helpers)
- Update PROJECT.md: add email template system + gallery to Done section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:24:52 +01:00
Deeman
fb83f432db feat(emails): subtask 7 — remove _email_wrap() and _email_button() from worker.py
All 10 email handlers now use render_email_template(). The two legacy
inline-HTML helpers are no longer needed and have been removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:20:32 +01:00
Deeman
4fafd3e80e feat(emails): subtask 6 — admin gallery (routes, templates, sidebar link)
- Add GET /admin/emails/gallery — card grid of all 11 email types
- Add GET /admin/emails/gallery/<slug>?lang=en|de — preview with lang toggle
- Add email_gallery.html: 3-column responsive card grid
- Add email_gallery_preview.html: full-width iframe + EN/DE toggle + log link
- Add Gallery sidebar link to base_admin.html (admin_page == 'gallery')

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:13:35 +01:00
Deeman
536d5c8f40 feat(emails): subtask 5 — compose preview (admin_compose template + HTMX endpoint)
- Add emails/admin_compose.html: branded wrapper for ad-hoc compose body
- Update email_compose.html: two-column layout with HTMX live preview pane
  (hx-post, hx-trigger=input delay:500ms, hx-target=#preview-pane)
- Add partials/email_preview_frame.html: sandboxed iframe partial
- Add POST /admin/emails/compose/preview route (no CSRF — read-only render)
- Update email_compose POST handler to use render_email_template() instead
  of importing _email_wrap from worker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:12:09 +01:00
Deeman
c31d4a71a0 feat(emails): subtask 4 — 4 complex templates (lead_forward, match_notify, digest, business_plan)
- Add lead_forward.html (brief table + contact table + optional CTA token link)
- Add lead_match_notify.html (new matching lead alert with heat badge)
- Add weekly_digest.html (leads table with Jinja2 for loop)
- Add business_plan.html (PDF ready notification with download CTA)
- Refactor 4 handlers in worker.py: send_lead_forward_email,
  notify_matching_suppliers, send_weekly_lead_digest, generate_business_plan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:10:55 +01:00
Deeman
1c7cdc42f2 feat(emails): subtask 3 — 4 medium templates (quote_verification, waitlist, lead_matched)
- Add quote_verification.html (with optional project recap card)
- Add waitlist_supplier.html, waitlist_general.html
- Add lead_matched.html (with next-steps section + tip box)
- Refactor 3 handlers in worker.py: send_quote_verification,
  send_waitlist_confirmation, send_lead_matched_notification

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