487 Commits

Author SHA1 Message Date
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
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