Commit Graph

307 Commits

Author SHA1 Message Date
Deeman
6d18c52983 merge: live search + loading indicators on all admin filter forms 2026-02-24 16:59:03 +01:00
Deeman
4731a91d02 feat(admin): live search with loading indicator on all admin filter forms
Scenarios:
- Convert from plain GET form to HTMX live search (scenario_results
  route already existed, just needed wiring)
- Replace Filter submit button with JS-reset Clear button
- Update is_generating banner to match article_results.html style

Users:
- Add /admin/users/results HTMX partial route
- Extract user table into partials/user_results.html with HTMX pagination
- Convert search form to live-search (input delay:300ms)

Loading indicator (all 6 forms):
- Add hx-indicator pointing to a small arc spinner SVG
- Spinner fades in while the debounce + request is in flight
- CSS .search-spinner class in input.css (opacity 0 → 1 on htmx-request,
  spin-icon animation only runs when visible)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 16:58:28 +01:00
Deeman
3d02d1654a merge: fix scenario filter focus styling 2026-02-24 16:49:49 +01:00
Deeman
1510cad697 fix(admin): use form-input class on scenario filter fields
The search/country/venue-type inputs used class="input" which has no
definition in input.css — falls back to the browser's default focus
outline. Replaced with form-input to get the consistent focus ring
(ring-2 / ring-electric / border-electric) used everywhere else in admin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 16:49:25 +01:00
Deeman
4dcbb731b0 merge: spinner, batch commits, pre-compiled Jinja templates, timing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 16:44:13 +01:00
Deeman
af20c59ced feat(content): spinner, batch commits, pre-compiled templates, timing
Spinner:
- article_results.html: replace hidden polling div with a visible
  animated spinner banner; CSS spin keyframe added to input.css

Batch commits:
- generate_articles() now commits every 200 articles instead of
  holding one giant transaction; articles appear in the admin UI
  progressively without waiting for the full run

Performance (pre-compiled Jinja templates):
- Create one Environment + compile url/title/meta/body templates once
  before the loop instead of calling _render_pattern() per iteration;
  eliminates ~4 × N Environment() constructions and re-parses of the
  same template strings (N = articles, typically 500+)
- Reuse url_tmpl for hreflang alt-lang rendering

Scenario override passthrough:
- Pass just-computed scenario data directly to bake_scenario_cards()
  via scenario_overrides, avoiding a DB SELECT that reads an uncommitted
  row from a potentially separate connection

Timing instrumentation:
- Accumulate time spent in calc / render / bake phases per run
- Log totals at completion: "done — 500 total | calc=1.2s render=4.3s bake=0.1s"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 16:44:02 +01:00
Deeman
f7a753d2d7 merge: content improvement tasks (FAQ, Market Score, DE translations, country names, DB perf)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 16:34:51 +01:00
Deeman
482b4f9fca perf(content): batch article generation in single transaction + upsert
Replace SELECT-then-INSERT/UPDATE pairs in generate_articles() with
INSERT ... ON CONFLICT DO UPDATE statements, and wrap the entire loop in
a single transaction context manager. Eliminates ~1,500 individual SQLite
commits for a 500-article run (one commit per row replaced by one total).

Also fix _get_article_stats() returning None for live/scheduled/draft counts
when the articles table is empty: wrap SUM expressions in COALESCE(..., 0)
so they always return integers regardless of row count.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 16:34:16 +01:00
Deeman
d1b0e89261 fix(i18n): country_name filter now handles 2-letter ISO codes from DB
articles.country stores "CH"/"DE"/etc., not English names. Update
get_country_name() to try the input as an uppercase code first, falling
back to the reverse-name lookup for any English-name values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 16:16:45 +01:00
Deeman
dc10eeae29 merge: tiered proxy with circuit breaker for Playtomic extractor 2026-02-24 16:02:43 +01:00
Deeman
0b472e1a32 feat(extract): tiered proxy with circuit breaker for Playtomic availability
Adds a two-tier proxy system for the Playtomic availability extractor:
- Primary tier (PROXY_URLS): datacenter proxies, cheap and fast
- Fallback tier (PROXY_URLS_FALLBACK): residential rotating gateway, reliable

Circuit breaker opens after CIRCUIT_BREAKER_THRESHOLD (default: 10) consecutive
failures, permanently switching to the fallback tier for the rest of the run.
No auto-recovery — avoids flapping. If circuit opens with no fallback configured,
logs an error and writes partial results rather than continuing on a dead proxy pool.

Parallel mode submits futures in PARALLEL_BATCH_SIZE=100 batches so the circuit
breaker can stop new submissions after it opens.

New env vars added to .env.dev.sops (blank defaults):
  PROXY_URLS_FALLBACK          — residential/rotating gateway URL
  CIRCUIT_BREAKER_THRESHOLD    — consecutive failures before switching (default 10)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 16:01:50 +01:00
Deeman
cb08e04b0f merge: content improvements — country translations, FAQ collapsibles, Market Score branding, DE i18n fixes
- feat(i18n): add country name translations for article country badges
- feat(content): convert article FAQ sections to collapsible details/summary
- feat(content): rebrand stats-strip Market Score with padelnomics wordmark + color coding
- fix(i18n): improve German translation quality across 94 keys

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 14:26:33 +01:00
Deeman
1e0aa6002a fix(i18n): improve German translation quality across 94 keys
Systematic review of de.json: fix unnatural calques from English, inconsistent
register (Du/Sie mixing), awkward phrasing, and machine-translation artifacts.
Market Score and product names intentionally kept in English as brand names.
Du (capitalized) maintained consistently as product voice throughout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 14:24:21 +01:00
Deeman
64728f5995 feat(content): rebrand stats-strip Market Score with padelnomics wordmark + color coding
Replace plain "Ø Market Score" / "Market Score" / "Avg Market Score" labels with
the branded padelnomics wordmark (Bricolage Grotesque bold). Add color-coded value:
green (≥65), amber (40–64), red (<40). Applied to country-overview.md.jinja (DE+EN)
and city-cost-de.md.jinja (DE+EN). Articles need Rebuild All to regenerate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 14:17:03 +01:00
Deeman
3a147b78b6 feat(content): convert article FAQ sections to collapsible details/summary
Replace **Bold question?** / Answer markdown pattern with HTML <details>/<summary>
in all three article templates (city-pricing, city-cost-de, country-overview),
both DE and EN sections. Add .article-body details CSS for styled accordion look.
Articles need Rebuild All to regenerate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 14:16:12 +01:00
Deeman
2a1e8a781b feat(i18n): add country name translations for article country badges
Move COUNTRY_LABELS to i18n.py (shared). Add get_country_name(country_str, lang)
that maps English DB values (e.g. "Germany") to localised names via existing
dir_country_* translation keys. Register as Jinja filter country_name.
Apply to market_results.html country badge.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 14:12:51 +01:00
Deeman
1c21adc6a7 fix(logging): silence hypercorn.error and hypercorn.access child loggers
hypercorn sets its own level on child loggers directly, so silencing
the parent 'hypercorn' logger alone isn't sufficient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 11:27:00 +01:00
Deeman
fae3bbdb43 fix(logging): silence aiosqlite DEBUG noise in setup_logging()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 11:19:59 +01:00
Deeman
00ad0f7e63 feat(logging): replace all print() with Python logging module
Zero print() calls remain in the app and worker (scripts use
basicConfig for clean CLI output). Setup_logging() in core.py
reads LOG_LEVEL env var and configures the root logger once.

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

# Conflicts:
#	web/src/padelnomics/core.py
#	web/src/padelnomics/worker.py
2026-02-24 11:10:02 +01:00
Deeman
77ca817925 feat(logging): convert scripts and migrations from print() to logging
- migrations/migrate.py: module logger, basicConfig in __main__
- scripts/seed_dev_data.py: module logger, convert all 19 prints
- scripts/seed_content.py: module logger, convert all 13 prints
- scripts/refresh_from_daas.py: module logger, convert all 11 prints
- scripts/setup_paddle.py: module logger, convert all 20 prints

All scripts use basicConfig(level=INFO, format='%(levelname)-8s %(message)s')
in their __main__ blocks for clean CLI output without timestamps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 11:00:07 +01:00
Deeman
ac4ad3179d feat(logging): convert planner/routes.py nurture warning to logging
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:52:16 +01:00
Deeman
39181ba452 feat(logging): convert worker.py print() to logging
- Add module logger (padelnomics.worker) and scheduler_logger
  (padelnomics.worker.scheduler)
- Call setup_logging() at start of run_worker() and run_scheduler()
- Convert all 26 print() calls — drop manual [WORKER]/[SCHEDULER] prefixes
- Magic link + quote verification debug prints → logger.debug() (only
  shown when LOG_LEVEL=DEBUG)
- Errors with exception context use logger.error() with %s formatting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:51:37 +01:00
Deeman
b4b486e908 feat(logging): add setup_logging() to core.py, wire into app startup
- Add logging import and setup_logging() that reads LOG_LEVEL env var
  (defaults DEBUG in dev, INFO in prod), sets format with timestamp +
  level + logger name, silences hypercorn/asyncio noise
- Add module-level logger to core.py
- Convert 3 [EMAIL] print() calls to logger.info / logger.error
- Call setup_logging() from app.py at import time

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:50:10 +01:00
Deeman
f0c041a00c merge: fix datetime.utcnow() deprecation warnings across all files
Replaces 94 occurrences of deprecated datetime.utcnow() and
datetime.utcfromtimestamp() across 22 files with utcnow()/utcnow_iso()
helpers. Zero DeprecationWarnings remain. All 1201 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:36:26 +01:00
Deeman
d9de9e4cda fix(planner): replace alert() error popups with inline error banner
Show API errors and network failures in a red inline div below the
export form instead of browser alert() dialogs. Error div is hidden
on each new submit attempt so stale messages don't linger.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:36:01 +01:00
Deeman
c5176d7d17 fix(admin): center confirm dialog with fixed position + transform
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:34:38 +01:00
Deeman
bd79617851 feat(admin): replace browser confirm() dialogs with native <dialog> modal
- Add styled <dialog id="confirm-dialog"> to base_admin.html — frosted
  backdrop, rounded card, Cancel / Confirm buttons
- Add confirmAction(message, form) JS helper — clones OK button to
  avoid listener accumulation, calls form.submit() on confirm
- Replace all 5 onclick="return confirm()" across templates with
  type="button" + confirmAction(..., this.closest('form'))
  · articles.html — Rebuild All
  · template_detail.html — Regenerate
  · generate_form.html — Generate Articles
  · scenario_results.html — Delete scenario
  · audience_contacts.html — Remove contact

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:33:38 +01:00
Deeman
d42c4790b4 chore: update CHANGELOG for datetime deprecation fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:30:31 +01:00
Deeman
e33b28025e fix: use SQLite-compatible space format in utcnow_iso(), fix credits ordering
utcnow_iso() now produces 'YYYY-MM-DD HH:MM:SS' (space separator) matching
SQLite's datetime('now') so lexicographic comparisons like
'published_at <= datetime(now)' work correctly.

Also add `id DESC` tiebreaker to get_ledger() ORDER BY to preserve
insertion order when multiple credits are added within the same second.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:30:18 +01:00
Deeman
6bd92c69ce fix(admin): use task_name column (not task_type) in _is_generating query
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:24:53 +01:00
Deeman
a05c230ce3 fix(tests): replace datetime.utcnow() with utcnow_iso() in test files
Also fixes test_supplier_webhooks.py fromisoformat() comparisons:
expires (naive, from DB) now compared against datetime.now(UTC).replace(tzinfo=None)
to avoid mixing naive/aware datetimes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:24:16 +01:00
Deeman
ff170d94ff merge: add padelnomics Market Score methodology page 2026-02-24 10:23:03 +01:00
Deeman
5644a1ebf8 fix: replace datetime.utcnow() with utcnow()/utcnow_iso() across all source files
Migrates 15 source files from the deprecated datetime.utcnow() API.
Uses utcnow() for in-memory math and utcnow_iso() (strftime format)
for SQLite TEXT column writes to preserve lexicographic sort order.
Also fixes datetime.utcfromtimestamp() in seo/_bing.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:22:42 +01:00
Deeman
abc8be12c3 docs: update CHANGELOG and PROJECT.md with Market Score page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 10:16:15 +01:00
Deeman
b7485902e6 test: add Market Score methodology page tests
Subtask 6/6: 8 tests covering EN/DE 200 status, legacy 301 redirect,
JSON-LD schema types, FAQ sections, OG tags, footer link.
Add footer_market_score to i18n parity allowlist (branded term).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 10:15:36 +01:00
Deeman
4033e13e05 feat(admin): live-poll Articles and Scenarios tabs during generation
- Add _is_generating() helper — queries tasks table for pending generate_articles tasks
- Pass is_generating to article_results partial (both full page and HTMX route)
- article_results.html: render invisible hx-trigger="every 3s" div when generating;
  polling stops naturally once generation completes and div is absent
- Add /admin/scenarios/results HTMX partial route with same is_generating logic
- Extract scenario table into admin/partials/scenario_results.html partial
- scenarios.html: wrap table in #scenario-results div, include partial

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:14:21 +01:00
Deeman
815edf3cef feat(seo): link first Market Score mention to methodology page
Subtask 5/6: Wrap first "padelnomics Market Score" per language
section in anchor to /{language}/market-score. Updated templates:
- city-cost-de.md.jinja (DE intro + EN intro)
- city-pricing.md.jinja (DE comparison + EN comparison)
- country-overview.md.jinja (DE intro + EN intro)

Creates hub-and-spoke internal linking from hundreds of city
articles to the methodology page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 10:13:40 +01:00
Deeman
f76d2889e5 fix(core): add utcnow()/utcnow_iso() helpers, migrate core.py usages
Replace deprecated datetime.utcnow() with datetime.now(UTC).
- utcnow() -> datetime: for in-memory datetime math
- utcnow_iso() -> str: strftime format preserving existing SQLite TEXT format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:12:50 +01:00
Deeman
2a038e48be feat(seo): add Market Score to sitemap and footer
Subtask 4/6: Add /market-score to STATIC_PATHS for sitemap
generation (both lang variants + hreflang). Add footer link
in Product column between Markets and For Suppliers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 10:12:29 +01:00
Deeman
39fe025e5b feat(template): create Market Score methodology page
Subtask 3/6: Standalone informational page extending base.html.
Sections: hero, what it measures (4-card grid), score bands,
data sources, limitations, CTAs, FAQ with details/summary.
JSON-LD: WebPage + BreadcrumbList + FAQPage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 10:12:03 +01:00
Deeman
33aa705ef9 feat(routes): add /market-score route + legacy redirect
Subtask 2/6: Route handler in public blueprint, 301 redirect
from /market-score → /en/market-score for bookmarks without
lang prefix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 10:11:07 +01:00
Deeman
c2bf82917a feat(i18n): add Market Score methodology page keys (EN + DE)
Subtask 1/6: ~40 mscore_* keys per locale covering page title, meta,
section headings, category descriptions, score band interpretations,
data sources, limitations, CTAs, and 5 FAQ Q&A pairs.

DE content written as native German (Du-form), not translated from EN.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 10:10:39 +01:00
Deeman
3d99b8c375 fix(cms): change articles unique constraint to (url_path, language)
url_path UNIQUE prevented multilingual generation — the second language
(e.g. EN after DE) always failed with UNIQUE constraint, leaving tasks in
a retry loop and only the first 1-2 articles visible.

Migration 0020 recreates the articles table with UNIQUE(url_path, language)
and adds a composite index. Adds idx_articles_url_lang for the new lookup
pattern used by article_page and generate_articles upsert.

Also adds search/country/venue_type filters to the admin Scenarios tab
and clarifies what "Published Scenarios" means in the subtitle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 09:53:58 +01:00
Deeman
a35036807e merge: lazy-import duckdb to fix CI hang + mount analytics.duckdb in prod 2026-02-24 04:27:06 +01:00
Deeman
d5e99eead1 fix(ci): lazy-import duckdb to prevent pytest hang
DuckDB spawns non-daemon background threads on import. Since
analytics.py was imported at module level (transitively by the
test suite), these threads kept the pytest process alive after
all tests completed. Moving the import into open_analytics_db()
means duckdb is only loaded when actually needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 04:26:22 +01:00
Deeman
c2fc54b297 merge: mount analytics.duckdb in prod containers + fix CI pytest hang 2026-02-24 04:19:51 +01:00
Deeman
8c03b16e61 fix(ci): disable faulthandler to prevent pytest hang
pytest completes all tests but process never exits in GitLab CI.
Disabling the faulthandler plugin fixes this.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 04:19:24 +01:00
Deeman
a27bf77e99 fix(infra): mount analytics.duckdb into prod containers
The web containers had no access to the DuckDB serving layer —
analytics queries silently returned empty. Bind-mount the host
file read-only and set SERVING_DUCKDB_PATH in all app/worker/scheduler
services (both blue and green slots).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 04:17:40 +01:00
Deeman
0e2c1e19e6 merge: ci gate deploys on passing tags + fix markets feature flag test 2026-02-24 03:52:20 +01:00
Deeman
6c1dc90a8d fix(ci): gate deploys on passing tags + fix markets feature flag test
- CI now creates v<pipeline_iid> tag after tests pass on master
- Supervisor fetches tags and only deploys when a newer tag is available;
  skips if already on latest or no tags exist
- Fix test_seeds_markets_enabled: markets is seeded disabled (enabled=0),
  test was asserting the wrong value

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 03:36:59 +01:00