Commit Graph

443 Commits

Author SHA1 Message Date
Deeman
5ade38eeaf feat(data): Phase 2a — NUTS-1 regional income for opportunity score
- eurostat.py: add nama_10r_2hhinc dataset config; append filter params to
  request URL so server pre-filters the large cube before download
- stg_regional_income.sql: new staging model — reads nama_10r_2hhinc.json.gz,
  filters to NUTS-1 codes (3-char), normalises EL→GR / UK→GB
- dim_locations.sql: add admin1_to_nuts1 VALUES CTE (16 German Bundesländer)
  + regional_income CTE; final SELECT uses COALESCE(regional, country) income
- init_landing_seeds.py: add empty seed for nama_10r_2hhinc.json.gz

Munich/Bayern now scores ~29K PPS vs Chemnitz/Sachsen ~19K PPS instead of
both inheriting the same national average (~25.5K PPS).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 10:26:15 +01:00
Deeman
e32f7ba4b8 docs: CHANGELOG + PROJECT.md for opportunity score data quality improvements
Documents Phase 0 (income ceiling fix), Phase 1b (overpass_tennis workflow),
and Phase 2b (dim_cities spatial population fallback, 70.5%→98.5% coverage).

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:58:57 +01:00
Deeman
721b2a37df docs: CHANGELOG + PROJECT.md for score recalibration (market_score v3 + opportunity_score v2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 06:58:48 +01:00
Deeman
10266c3a24 fix(sql): opportunity_score — supply gap ceiling 4→8/100k + doc findings
Raises supply gap ceiling from 4/100k to 8/100k in
location_opportunity_profile.sql. The original 4/100k hard cliff
truncated opportunity scores to 0 for any city with ≥4 courts/100k,
but our data undercounts ~87% of real courts (FIP: 17,300 Spanish
courts vs 2,239 in our DB). Raising to 8/100k gives a gentler gradient
and fairer partial credit when density data is incomplete.

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 06:57:57 +01:00
Deeman
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
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