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>
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>
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>
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>
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
- 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>
- 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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
Three bugs causing 0 articles after generation:
1. Worker never called open_analytics_db() — fetch_analytics always
returned [] since _conn was None. Generation completed silently with
0 rows from DuckDB.
2. generate_articles upserted by url_path alone — after the URL prefix
fix, all languages share the same url_path (e.g. /markets/de/berlin).
The EN article was overwriting the DE article on every generation.
Fixed: deduplicate on (url_path, language).
3. article_page and _filter_articles didn't filter by language — with
shared url_paths a DE request could serve an EN article, and the
markets hub would show duplicate entries per city. Fixed: both now
filter by g.lang from the /<lang> blueprint prefix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- setup_server.sh now requires root, creates padelnomics_service user,
adds to docker group, generates deploy key in service user's home,
owns /opt/padelnomics and /data/padelnomics to service user
- supervisor service: User=padelnomics_service, updated PATH
- landing-backup service: User=padelnomics_service
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unescaped HTML attribute quotes inside YAML double-quoted scalars broke
YAML parsing, causing discover_templates() to silently skip both
templates and generate nothing.
Fixed by replacing \" with \" in the span style attributes within the
YAML frontmatter. The wordmark span is preserved; YAML now parses
correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Updated city-cost-de and country-overview templates
- All user-facing "Market Score" mentions now use full branded name
- "padelnomics" rendered in Bricolage Grotesque 800 wordmark style
- Stat strip labels kept short (space-constrained)
- SQL column names unchanged (market_score stays as-is)
- Also seeds markets feature flag as disabled (0) by default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pSEO template improvements:
- Fix double language prefix bug in article URLs
- Add German language to all 3 templates (city-cost-de, city-pricing, country-overview)
- Expand English content depth (~1500 words per template)
- Fix country-overview schema_type to [Article, FAQPage]
- Add cross-template links, scenario cross-references, extra FAQs, second CTAs
Subtask 5: Bilingual country-overview template with schema_type fix.
Schema fix:
- Changed schema_type from [Article] to [Article, FAQPage] — enables
FAQ rich results for existing FAQ content.
German variant:
- Full German prose: "Padel in {{ country_name_en }} — Marktüberblick"
- German stats-strip labels (Anlagen gesamt, Erfasste Städte, Ø Market Score,
Median Spitzenpreis)
- German market landscape analysis, pricing overview, FAQ (5 questions)
- German CTAs: "Zum Finanzplaner" throughout
- Peak/off-peak spread commentary for German readers
English expansion (~500 → ~1300 words):
- New "Market Landscape" section with market maturity analysis
(conditional on avg_market_score ranges)
- Expanded "Top Cities" with ranking methodology context
- Expanded "Pricing Overview" with peak/off-peak spread commentary
and utilization management implications
- Expanded "Build Your Business Plan" with more persuasive copy
- 2 new FAQ questions: "How fast is padel growing?" and "Which cities
have the best pricing data?"
- Planner links in FAQ answers + second CTA at bottom
- Language-prefixed internal links: /{{ language }}/markets/...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Subtask 4: Bilingual city-pricing template with language conditionals.
German variant:
- Full German prose: "Padel-Court-Preise in {{ city_name }}"
- German stats-strip labels (Hauptzeit, Nebenzeit, Preisspanne, Auslastung)
- German pricing tables, comparison section, pricing drivers
- 5 German FAQ questions with planner links
- German CTAs throughout
English expansion (~400 → ~1500 words):
- New "Pricing Trends" section with time-of-day analysis
- Expanded city comparison with venue density benchmarks
- Expanded pricing drivers (facility quality, market maturity)
- Investment Outlook section with scenario cross-reference
[scenario:city-cost-de-{{ city_key }}:operating]
- 2 new FAQ questions (city comparison + price trends)
- Cross-links to city-cost-de and country-overview articles
- Second CTA at bottom + planner links in FAQ answers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Full German prose variant (natural Du/Dein, not literal translation)
- German stats-strip labels, table headers, FAQ, CTAs
- Expanded English content (~1500 words): analytical commentary after
scenario markers, Market Context section, venue density analysis
- Cross-link to city-pricing page in both languages
- Planner links in FAQ answers (both languages)
- Second CTA at bottom of each language block
- Language-conditional title_pattern and meta_description_pattern
Subtask 2+3 of pSEO template improvements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
generate_articles() was storing url_path with lang prefix (/en/markets/...)
but the content blueprint is registered at /<lang>, producing double-prefix
URLs like /en/en/markets/italy. Fix: store url_path without prefix, build
full_url with prefix for SEO tags (canonical, OG, hreflang, breadcrumbs).
Also removes /markets from RESERVED_PREFIXES since article sub-paths under
/markets/ are valid pSEO content URLs, not blueprint routes.
Subtask 1 of pSEO template improvements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
File has evolved from exploratory research into operational documentation
(pipeline status tracker, extractor refs, staging model grains, impl notes).
Aligns with other architectural docs in docs/ (USER_FLOWS.md, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>