Commit Graph

429 Commits

Author SHA1 Message Date
Deeman
060cb9b32e feat(pipeline): scaffold Pipeline Console blueprint + sidebar + app registration
- New pipeline_routes.py blueprint (url_prefix=/admin/pipeline) with:
  - All 9 routes (dashboard, overview, extractions, catalog, query editor)
  - Data access functions: state DB (sync+to_thread), serving meta, landing FS, workflows.toml
  - execute_user_query() added to analytics.py (columns+rows+error+elapsed_ms)
  - Query security: blocklist regex, 10k char limit, 1000 row cap, 10s timeout
- Add 'Pipeline' sidebar section to base_admin.html (between Analytics and System)
- Register pipeline_bp in app.py
- Add run_extraction task handler to worker.py

Subtask 1 of 6

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

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

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

# Conflicts:
#	CHANGELOG.md
2026-02-25 12:34:03 +01:00
Deeman
683ca3fc24 docs: update CHANGELOG and PROJECT.md for JSONL landing format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:28:43 +01:00
Deeman
883d98167c feat(email-templates): Jinja2 template system + admin gallery + compose preview
Converts all 11 transactional emails from inline f-string HTML in worker.py
to standalone Jinja2 templates. Adds an admin gallery for rendered previews
and a live HTMX preview pane on the compose page.

Changes:
- email_templates.py: standalone render_email_template() + EMAIL_TEMPLATE_REGISTRY
- templates/emails/_base.html + _macros.html: branded shell + reusable macros
- 11 email templates: magic_link, quote_verification, welcome,
  waitlist_supplier, waitlist_general, lead_matched, lead_forward,
  lead_match_notify, weekly_digest, business_plan, admin_compose
- worker.py: all 10 handlers updated; _email_wrap/_email_button removed
- admin/routes.py: gallery routes + compose_preview endpoint
- admin gallery: email_gallery.html + email_gallery_preview.html
- email_compose.html: two-column layout with HTMX live preview
- base_admin.html: Gallery sidebar link
- 50 new tests (test_email_templates.py)
- CHANGELOG.md + PROJECT.md updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:26:33 +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
ec7f115f16 feat: add init_landing_seeds.py for empty-landing bootstrap
Creates minimal .jsonl.gz and .json.gz seed files so all SQLMesh staging
models can compile and run before real extraction data arrives.

Each seed has a single null record filtered by the staging model's WHERE
clause (tenant_id IS NOT NULL, geoname_id IS NOT NULL, type IS NOT NULL, etc).

Covers both formats (JSONL + blob) for the UNION ALL transition CTEs:
  playtomic/1970/01/: tenants.{jsonl,json}.gz, availability seeds (morning + recheck)
  geonames/1970/01/: cities_global.{jsonl,json}.gz
  overpass_tennis/1970/01/: courts.{jsonl,json}.gz
  overpass/1970/01/: courts.json.gz (padel, unchanged format)
  eurostat/1970/01/: urb_cpop1.json.gz, ilc_di03.json.gz
  eurostat_city_labels/1970/01/: cities_codelist.json.gz
  ons_uk/1970/01/: lad_population.json.gz
  census_usa/1970/01/: acs5_places.json.gz

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:24:48 +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
b5b8493543 feat(extract): regional overpass_tennis splitting + JSONL output
Replace single global Overpass query (150K+ elements, times out) with
10 regional bbox queries (~10-40K elements each, 150s server / 180s client).

- REGIONS: 10 bboxes covering all continents
- Crash recovery: working.jsonl accumulates per-region results;
  already_seen_ids deduplication skips re-written elements on restart
- Overlapping bbox elements deduped by OSM id across regions
- Retry per region: up to 2 retries with 30s cooldown
- Polite 5s inter-region delay
- Skip if courts.jsonl.gz or courts.json.gz already exists for the month

stg_tennis_courts: UNION ALL transition (jsonl_elements + blob_elements)
  - jsonl_elements: JSONL, explicit columns, COALESCE lat/lon with center coords
    (supports both node direct lat/lon and way/relation Overpass out center)
  - blob_elements: existing UNNEST(elements) pattern, unchanged
  - Removed osm_type='node' filter — ways/relations now usable via center coords
  - Dedup on (osm_id, extracted_date DESC) unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:19:37 +01:00
Deeman
a4f246d69a feat(extract): convert geonames to JSONL output
- cities_global.jsonl.gz replaces .json.gz (one city object per line)
- Empty placeholder writes a minimal .jsonl.gz (null row, filtered in staging)
- Eliminates the {"rows": [...]} blob wrapper and maximum_object_size workaround

stg_population_geonames: UNION ALL transition (jsonl_rows + blob_rows)
  - jsonl_rows: read_json JSONL, explicit columns, no UNNEST
  - blob_rows: existing UNNEST(rows) pattern with 40MB size limit retained

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:16:59 +01:00
Deeman
7b03fd71f9 feat(extract): convert playtomic_availability to JSONL output
- availability_{date}.jsonl.gz replaces .json.gz for morning snapshots
- Each JSONL line = one venue object with date + captured_at_utc injected
- Eliminates in-memory consolidation: working.jsonl IS the final file
  (compress_jsonl_atomic at end instead of write_gzip_atomic blob)
- Crash recovery unchanged: working.jsonl accumulates via flush_partial_batch
- _load_morning_availability tries .jsonl.gz first, falls back to .json.gz
- Skip check covers both formats during transition
- Recheck files stay blob format (small, infrequent)

stg_playtomic_availability: UNION ALL transition (morning_jsonl + morning_blob + recheck_blob)
  - morning_jsonl: read_json JSONL, tenant_id direct column, no outer UNNEST
  - morning_blob / recheck_blob: subquery + LATERAL UNNEST (unchanged semantics)
  - All three produce (snapshot_date, captured_at_utc, snapshot_type, recheck_hour, tenant_id, slots_json)
  - Downstream raw_resources / raw_slots CTEs unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:14:38 +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
Deeman
daf1945d5b feat(emails): subtask 1-2 — email_templates.py foundation + 3 simple templates
- Add email_templates.py: standalone Jinja2 env, render_email_template(),
  EMAIL_TEMPLATE_REGISTRY with sample_data functions for all 11 email types
- Add templates/emails/_base.html: direct transliteration of _email_wrap()
- Add templates/emails/_macros.html: email_button, heat_badge, heat_badge_sm,
  section_heading, info_box macros
- Add magic_link.html, welcome.html, supplier_enquiry.html templates
- Refactor 3 handlers in worker.py to use render_email_template()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:08:55 +01:00
Deeman
9bef055e6d feat(extract): convert playtomic_tenants to JSONL output
- playtomic_tenants.py: write each tenant as a JSONL line after dedup,
  compress via compress_jsonl_atomic → tenants.jsonl.gz
- playtomic_availability.py: update _load_tenant_ids() to prefer
  tenants.jsonl.gz, fall back to tenants.json.gz (transition)
- stg_playtomic_venues.sql: UNION ALL jsonl+blob CTEs for transition;
  JSONL reads top-level columns directly, no UNNEST(tenants) needed
- stg_playtomic_resources.sql: same UNION ALL pattern, single UNNEST
  for resources in JSONL path vs double UNNEST in blob path
- stg_playtomic_opening_hours.sql: same UNION ALL pattern, opening_hours
  as top-level JSON column in JSONL path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:07:53 +01:00
Deeman
6bede60ef8 feat(extract): add compress_jsonl_atomic() utility
Streams a JSONL working file to .jsonl.gz in 1MB chunks (constant memory),
atomic rename via .tmp sibling, deletes source on success. Companion to
write_gzip_atomic() for extractors that stream records incrementally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 11:50:17 +01:00
Deeman
e5960c08ff feat(admin): cross-section links across leads, suppliers, marketplace, emails
Lead detail:
- contact_email → 📧 email log (pre-filtered), mailto, Send Email compose
- country → leads list filtered by that country

Supplier detail:
- contact_email → 📧 email log (pre-filtered), mailto, Send Email compose
- claimed_by → user detail page (was plain "User #N")

Marketplace dashboard:
- Funnel card numbers are now links: Total → /leads, Verified New →
  /leads?status=new, Unlocked → /leads?status=forwarded, Won → /leads?status=closed_won
- Active suppliers number links to /suppliers

Marketplace activity stream:
- lead events → link to lead_detail
- unlock events → supplier name links to supplier_detail, "lead #N" links to lead_detail
- credit events → supplier name links to supplier_detail (query now joins
  suppliers table for name; ref2_id exposes supplier_id and lead_id per event)

Email detail:
- Reverse-lookup to_addr against lead_requests + suppliers; renders
  linked "Lead #N" / "Supplier Name" chips next to the To field

Email compose:
- Accepts ?to= query param to pre-fill recipient (enables Send Email links)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 10:15:25 +01:00
Deeman
55f179ba54 fix(transform): increase geonames object size limit and remove stale column ref
- stg_population_geonames: add maximum_object_size=40MB to read_json() call;
  geonames cities_global.json.gz is ~30MB, exceeding DuckDB's 16MB default
- dim_locations: remove stale 'population_year AS population_year' column ref;
  stg_population_geonames has ref_year, not population_year — caused BinderException

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:56:05 +01:00
Deeman
3c0f57c0fd feat(leads): 2-hour admin review window before leads appear in supplier feed
New visible_from column on lead_requests set to NOW + 2h on both the
direct insert (logged-in user) and the email verification update.

Supplier feed, notify_matching_suppliers, and send_weekly_lead_digest
all filter on visible_from <= datetime('now'), so no lead surfaces to
suppliers before the window expires.

Migration 0023 adds the column and backfills existing verified leads
with created_at so they remain immediately visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:53:19 +01:00
Deeman
607dc35a9d docs: add ADMIN.md — comprehensive admin panel guide
Covers all 10 admin sections: Dashboard, Marketplace (new), Leads,
Suppliers, Flags, Feedback, Emails (sent log, inbox, compose, audiences),
pSEO Engine, SEO Hub, CMS (Templates, Scenarios, Articles), Tasks, Users.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:44:33 +01:00
Deeman
db14684667 docs: update USER_FLOWS.md for marketplace + lead response flows
- Flow 11: note CTA token in forward email + matching notification tasks
- Flow 12 (new): supplier lead_respond endpoint + one-click CTA token flow
- Flow 13 (was 12): add Marketplace admin dashboard row, update Leads row
  with search/filter/HTMX inline actions, note HTMX partials

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:41:54 +01:00
Deeman
d834bdc59a feat(extract): recheck every 30 min with 30-min window for accurate occupancy
Each slot is now rechecked once, at most 30 min before it starts.
Worst-case miss: a booking made 29 min before start.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:39:30 +01:00
Deeman
5ba4cabcd8 docs: update CHANGELOG and PROJECT.md for marketplace + lead forward tracking
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:37:26 +01:00
Deeman
b7c8568265 fix(extract): recheck window 90→60 min (correct reasoning this time)
60-min window + hourly rechecks = each slot caught exactly once, 0-60 min
before it starts. 90-min window causes double-querying (T-90 and T-30).
Slot duration is irrelevant — it doesn't affect when the slot appears in
the window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:37:17 +01:00
Deeman
be8872beb2 revert: restore recheck window to 90 min
Data analysis of 5,115 venues with slots shows 24.8% have a 90-min minimum
slot duration. A 60-min window would miss those venues entirely with hourly
rechecks. 90 min is correct — covers 30/60/90-min minimum venues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:35:12 +01:00
Deeman
d15787caeb fix(extract): recheck window 90→60 min — matches hourly schedule and min slot duration
With hourly rechecks and 60-min minimum slots, a 90-min window causes each
slot to be queried twice. 60-min window = each slot caught exactly once in
the recheck immediately before it starts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:33:20 +01:00
Deeman
eca21dd147 chore(secrets): update PROXY_URLS in dev sops (tiered proxy config)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:31:56 +01:00
Deeman
5867c611f8 feat(admin): marketplace dashboard + HTMX lead management improvements
Admin marketplace (/admin/marketplace):
- Lead funnel cards: total / verified-new / unlocked / won / conversion rate
- Credit economy: issued / consumed / outstanding / 30-day burn
- Supplier engagement: active count / avg unlocks / response rate
- Feature flag toggles (lead_unlock, supplier_signup) with next= redirect
- Live activity stream (HTMX partial): last 50 lead / unlock / credit events

Admin leads list (/admin/leads):
- Summary cards: total / new+unverified / hot pipeline credits / forward rate
- Search filter (name, email, company) with HTMX live update
- Period pills: Today / 7d / 30d / All
- get_leads() now returns (rows, total_count); get_lead_stats() includes
  _total, _new_unverified, _hot_pipeline, _forward_rate

Admin lead detail (/admin/leads/<id>):
- Inline HTMX status change returning updated status badge partial
- Inline HTMX forward form returning updated forward history partial
  (replaces full-page reload on every status/forward action)
- Forward history table shows supplier, status, credit_cost, sent_at

Quote form extended with optional fields:
- build_context, glass_type, lighting_type, location_status,
  financing_status, services_needed, additional_info
  (captured in lead detail view but not required for heat scoring)

Sidebar nav: "Marketplace" tab added between Leads and Suppliers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:31:44 +01:00
Deeman
7af612504b feat(marketplace): lead matching notifications + weekly digest + CTA tracking
- notify_matching_suppliers task: on lead verification, finds growth/pro
  suppliers whose service_area matches the lead country and sends an
  instant alert email (LIMIT 20 suppliers per lead)
- send_weekly_lead_digest task: every Monday 08:00 UTC, sends paid
  suppliers a table of new matching leads from the past 7 days they
  haven't seen yet (LIMIT 5 per supplier)
- One-click CTA token: forward emails now include a "Mark as contacted"
  footer link; clicking sets forward status to 'contacted' immediately
- cta_token stored on lead_forwards after email send
- Supplier lead_respond endpoint: HTMX status update for forwarded leads
  (sent / viewed / contacted / quoted / won / lost / no_response)
- Supplier lead_cta_contacted endpoint: handles one-click email CTA,
  redirects to dashboard leads tab
- leads/routes.py: enqueue notify_matching_suppliers on quote verification

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:31:23 +01:00
Deeman
c84a5ffdd1 feat(db): migration 0022 — add response tracking to lead_forwards
Adds status_updated_at, supplier_note, and cta_token columns to the
lead_forwards table. cta_token gets a unique partial index for fast
one-click email CTA lookups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 09:31:14 +01:00
Deeman
d5947af8d4 merge: maximum performance extraction (parallel pages + crash-safe partial JSONL)
# Conflicts:
#	.env.dev.sops
#	.env.prod.sops
#	extract/padelnomics_extract/src/padelnomics_extract/playtomic_tenants.py
2026-02-24 22:36:34 +01:00
Deeman
1ef22770aa docs: update CHANGELOG for extraction performance improvements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 22:31:19 +01:00
Deeman
9f010d8c0c perf(extract): parallel page fetching in tenants, drop EXTRACT_WORKERS env var
- playtomic_tenants.py: batch_size = len(proxy_urls) pages fired in parallel per
  batch; each page gets its own session + proxy; sorted(results) ensures
  deterministic done-detection; falls back to serial + THROTTLE_SECONDS when no
  proxies. Expected speedup: ~2.5 min → ~15 s with 10 proxies.
- .env.dev.sops, .env.prod.sops: remove EXTRACT_WORKERS (now derived from
  PROXY_URLS length)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 22:30:28 +01:00
Deeman
6116445b56 perf(extract): auto-detect workers from proxies, skip throttle on success, crash-safe partial JSONL
- proxy.py: delete unused make_sticky_selector()
- utils.py: add load_partial_results() + flush_partial_batch() for crash-resumable extraction
- playtomic_availability.py:
  - drop MAX_WORKERS / EXTRACT_WORKERS — worker_count = len(proxy_urls) or 1
  - skip time.sleep(THROTTLE_SECONDS) on success when proxy_url is set; keep sleeps for 429/5xx
  - replace cursor-based resumption with .partial.jsonl sidecar (flush every 50 records)
  - _fetch_venues_parallel accepts on_result callback for incremental partial-file flushing
  - mirror auto-detect worker count in extract_recheck()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 22:21:05 +01:00
Deeman
79d1b0e672 feat(extract): tiered proxy with circuit breaker + proxy provider research
- playtomic_tenants.py: simplify proxy cycler call (cycler() instead of
  cycler["next_proxy"]()) — matches refactored proxy API
- docs/proxy-provider-inventory.md: proxy provider comparison table for
  Playtomic scraping (~14k req/day, residential IPs, pay-per-GB)
- .env.*.sops: updated encrypted secrets (re-encrypted)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 22:15:11 +01:00
Deeman
19dd9843af fix(dev): scope granian --reload-paths to web/src to stop DB WAL triggering reloads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 21:49:14 +01:00
Deeman
77d4c02db3 chore: run dev server with granian --reload for dev/prod parity
Replaces `python -m padelnomics.app` (Quart's built-in Hypercorn-based
dev runner) with granian directly. Adds granian[reload] extra which
pulls in watchfiles for file-change detection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 21:42:30 +01:00
Deeman
c95d66982b fix(logging): restore hypercorn logger silencing (still used by Quart dev server)
Quart depends on Hypercorn and uses it in app.run() → run_task().
Removing the silencing caused hypercorn.error noise in dev logs.
Keep both granian and hypercorn logger config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 21:40:23 +01:00
Deeman
fda7da7d59 chore: replace hypercorn with granian (Rust ASGI server)
Granian is ~3-5x faster than Hypercorn in benchmarks. No code changes
needed — Quart is standard ASGI so any ASGI server works.

- web/pyproject.toml: hypercorn → granian>=1.6.0 (installed: 2.7.1)
- Dockerfile CMD: hypercorn → granian --interface asgi
- core.py setup_logging(): silence granian loggers instead of hypercorn's

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 21:26:19 +01:00
Deeman
aa7a8bad99 test: sync i18n tests to current translation values
- wiz_summary_label DE: "Aktuelle Werte" → "Aktuelle Zusammenfassung"
- add mscore_reife_chip + mscore_potenzial_chip to identical-value allowlist (branded product names)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 21:24:37 +01:00
Deeman
78ffbc313f feat(extract): parallel DAG scheduler + proxy rotation for tenants
- all.py: replace sequential loop with graphlib.TopologicalSorter + ThreadPoolExecutor
  - EXTRACTORS dict declares (func, [deps]) — self-documenting dependency graph
  - 8 extractors run in parallel immediately; availability starts as soon as
    tenants finishes (not after all others complete)
  - max_workers=len(EXTRACTORS) — all I/O-bound, no CPU contention
- playtomic_tenants.py: add proxy rotation via make_round_robin_cycler
  - no throttle when PROXY_URLS set (IP rotation removes per-IP rate concern)
  - keeps 2s throttle for direct runs
- _shared.py: add optional proxy_url param to run_extractor()
  - any extractor can opt in to proxy support via the shared session
- overpass_tennis.py: fix query timeout (out body → out center, timeout 180 → 300)
  - out center returns centroids only, not full geometry — fits within server limits
- playtomic_availability.py: fix CIRCUIT_BREAKER_THRESHOLD empty string crash
  - int(os.environ.get(..., "10")) → int(os.environ.get(...) or "10")

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 21:17:00 +01:00
Deeman
e8fb8f51f7 feat(pseo): add pSEO Engine admin tab
Operational dashboard at /admin/pseo for the programmatic SEO system:
content gap detection, data freshness signals, article health checks
(hreflang orphans, missing build files, broken scenario refs), and
live generation job monitoring with HTMX progress bars.

- _serving_meta.json written by export_serving.py after atomic DB swap
- content/health.py: pure async query functions for all health checks
- Migration 0021: progress_current/total/error_log on tasks table
- generate_articles() writes progress every 50 articles + on completion
- admin/pseo_routes.py: 6 routes, standalone blueprint
- 5 HTML templates + sidebar nav + fromjson Jinja filter
- 45 tests (all passing); 2 bugs caught and fixed during testing

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

# Conflicts:
#	src/padelnomics/export_serving.py
2026-02-24 21:00:00 +01:00
Deeman
ec15012d00 test: update mock_fetch_analytics to handle COUNT(*) queries
count_template_data() uses fetch_analytics with a COUNT(*) query.
The pseo_env test fixture's mock returned TEST_ROWS for any unrecognized
query, causing a KeyError on rows[0]["cnt"]. Add a COUNT(*) branch that
returns [{cnt: len(TEST_ROWS)}].

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 20:55:38 +01:00
Deeman
a9b14b8f73 docs: update CHANGELOG + PROJECT.md for pSEO Engine
Records all Phase 1 deliverables: content gaps, data freshness,
health checks, generation job monitoring, 45 tests, bug fixes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 20:51:29 +01:00
Deeman
44c0dd0b8d refactor: minor TigerStyle cleanups
- export_serving.py: move `import re` to module level — was imported
  inside a loop body on every iteration
- sitemap.py: add comment documenting that the in-memory TTL cache is
  process-local (valid for single-worker deployment, Dockerfile --workers 1)
- playtomic_availability.py: use `or "10"` fallback for
  CIRCUIT_BREAKER_THRESHOLD env var to handle empty-string case

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 20:50:43 +01:00
Deeman
ee49862d91 test(pseo): add 45 tests for health checks + pSEO Engine admin routes
Covers content/health.py (get_template_stats, get_template_freshness,
get_content_gaps, check_hreflang_orphans, check_missing_build_files,
check_broken_scenario_refs, get_all_health_issues) and all 6 routes in
admin/pseo_routes.py (dashboard, health partial, gaps partial, generate
gaps, jobs list, job status polling).

Also fixes two bugs found while writing tests:
- check_hreflang_orphans: was grouping by url_path, but EN/DE articles
  have different paths. Now extracts natural key from slug pattern
  "{template_slug}-{lang}-{nk}" and groups by nk.
- pseo_job_status.html + pseo_jobs.html: | default('') | truncate() fails
  when completed_at is None (default() only handles undefined, not None).
  Fixed to (value or '') | truncate().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 20:50:03 +01:00
Deeman
83d148477d refactor: extract shared _query_scenarios() to remove duplication
scenarios() and scenario_results() both built the same WHERE clause and
ran the same filtered query. Extracted into _query_scenarios(search,
country, venue_type) -> (rows, total). Each handler is now ~10 lines
of param parsing + render_template.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 20:42:52 +01:00
Deeman
ad48f23cfc fix: add precondition assertions in extract pipeline
Assert landing_dir.is_dir() and year_month format (YYYY/MM) at the
entry point of each extract function — turning silent wrong-path bugs
into immediate AssertionError with a descriptive message.

Files changed:
- playtomic_availability.py: assert in _load_tenant_ids(), extract(),
  extract_recheck()
- eurostat.py: assert in extract()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 20:42:11 +01:00