Commit Graph

479 Commits

Author SHA1 Message Date
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
Deeman
ebc944a6d3 fix(cms): open analytics DB in worker + fix per-language article upsert
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>
2026-02-24 03:04:01 +01:00
Deeman
7f3bde56b6 fix(infra): guard chown calls to make setup_server.sh fully idempotent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 02:53:50 +01:00
Deeman
619d1570ef fix(infra): run services as padelnomics_service user instead of root
- 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>
2026-02-24 02:51:09 +01:00
Deeman
189f04cc47 fix(infra): run rclone installer with sudo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 02:37:50 +01:00
Deeman
7fb6dd9b6f fix(pseo): escape double quotes in YAML meta_description_pattern
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>
2026-02-24 02:32:19 +01:00
Deeman
140b4f2c70 feat(pseo): rebrand market score to "padelnomics Market Score" with wordmark styling
- 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>
2026-02-24 02:28:42 +01:00
Deeman
ed7c667419 Merge branch 'worktree-pseo-template-improvements'
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
2026-02-24 02:15:49 +01:00
Deeman
c574fe7e62 docs: update CHANGELOG and PROJECT.md for pSEO template improvements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:14:14 +01:00
Deeman
71b003cb49 feat(pseo): add German language + expanded content + FAQPage schema to country-overview
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>
2026-02-24 02:13:06 +01:00
Deeman
02fcfae0ae feat(pseo): add German language + expanded content to city-pricing template
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>
2026-02-24 02:08:22 +01:00
Deeman
fb18d5a387 feat(pseo): add German language + expanded content to city-cost-de template
- 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>
2026-02-24 02:03:25 +01:00
Deeman
ffe4b111eb fix: remove double language prefix from article URLs
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>
2026-02-24 02:01:33 +01:00
Deeman
90a5ab0451 docs: move data-sources-inventory to docs/
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>
2026-02-24 01:36:08 +01:00
Deeman
8bb00ea9b0 docs(inventory): pipeline tracker, scores, impl notes, FX section
- Replace Priority Summary Table with Pipeline Status Tracker: status
  (/🔲/⏸/—), score (1-5), credential requirements, and extractor refs
  for all 30+ sources
- Add implementation notes to §1.1 (Overpass), §1.2 (Playtomic tenants +
  availability), §5.1 (Eurostat urb_cpop1 + ilc_di03), §5.2 (Census), §5.3 (ONS)
- Update §8 DuckDB integration table with extractor names and status
- Add §10 FX / Currency Rates: ECB SDMX endpoint and Frankfurter.app wrapper,
  proposed landing format and stg_fx_rates staging model design

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:33:32 +01:00
Deeman
86539e72b9 research: expand competitor pricing & pSEO analysis
Deep-dives on 12 competitors: sportstaettenrechner.de, padel-konzept.de,
padelonomics.com (FR brand conflict), padelplatz-kosten.de, padel-court.de,
padelcreations.com, dojobusiness.com, reformsports.com, wepadel.com,
projet-padel.fr, padelcourtsnearme.us, misitranoconsulting.com.

Covers business models, pSEO structures, lead funnels, weaknesses,
and differentiation opportunities for Padelnomics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:23:28 +01:00
Deeman
48e9169a4c Merge branch 'worktree-cms-admin-improvement' 2026-02-24 01:22:55 +01:00
Deeman
5808ba6490 fix: scenario preview rendering and double-encoded ampersands
- bake_scenario_cards() accepts scenario_overrides dict for preview mode
  (bypasses DB lookup when no published_scenario exists)
- preview_article() builds in-memory scenario dict and passes it through
- Fix double-encoded &amp; in locale strings (was &amp;amp; in rendered HTML)
- Fix ruff import sort in _datetimeformat
- Fix migration 0019 minor issue

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:22:25 +01:00
Deeman
a555331729 docs: update CHANGELOG and PROJECT.md for CMS admin improvement
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:20:37 +01:00
Deeman
a59c670e43 feat: HTMX-ify article publish/delete actions for inline updates
- Publish/Unpublish returns updated <tr> partial via HTMX
- Delete returns empty string to remove row without page reload
- Extract article_row.html partial (used by both results table and
  individual HTMX responses)
- article_results.html now includes article_row.html via loop

Subtask 7 of CMS admin improvement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:20:02 +01:00
Deeman
8ae5aa1935 feat: write markdown to disk during generation for admin editing
- generate_articles() now writes body_md alongside body_html
  to BUILD_DIR/{lang}/md/{slug}.md
- article_edit GET checks both manual and generated markdown paths
- Fix pre-existing ruff import sort in _datetimeformat

Subtask 5 of CMS admin improvement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:18:48 +01:00
Deeman
f0f6e7542f feat: move article generation to background worker queue
- Add generate_articles task handler to worker.py
- template_generate and template_regenerate now enqueue tasks instead
  of running inline (was blocking HTTP request for seconds with 1k articles)
- rebuild_all enqueues per-template + inline rebuilds manual articles
- Update tests to check task enqueue instead of immediate article creation

Subtask 4 of CMS admin improvement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:17:59 +01:00
Deeman
81b361859d feat: invalidate sitemap cache on article publish/delete/create
Add invalidate_sitemap_cache() to sitemap.py and call it from
article_publish, article_delete, and article_new admin routes.

Subtask 6 of CMS admin improvement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:15:44 +01:00
Deeman
0c69d9e1a0 feat: HTMX articles list with filter, search, pagination, and view links
- Add _get_article_list() with filters: status, template, language, search
- Add _get_article_stats() for header stats strip (total/live/scheduled/draft)
- Add /articles/results HTMX partial endpoint
- Add filter bar: search input + status/template/language dropdowns
- Paginate at 50 articles per page
- Add "View" link to live articles (opens public URL in new tab)
- Remove URL column (redundant), add Language column

Subtasks 2+3 of CMS admin improvement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:14:40 +01:00
Deeman
566c578770 fix: correct broken status display and source column in articles list
- Compute display_status (live/scheduled/draft) in SQL instead of broken
  Jinja string comparison against undefined `now` variable
- Replace template_data_id (dropped in migration 0018) with template_slug

Subtask 1/8 of CMS admin improvement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:13:22 +01:00
Deeman
1762188f08 docs(inventory): document population pipeline implementation findings
- Add section 9 to data-sources-inventory.md covering live API quirks:
  Eurostat SDMX city labels response shape, ONS CSV download path (observations
  API 404s), US Census ACS place endpoint, GeoNames cities15000 bulk format
- Add population coverage summary table and DuckDB glob limitation note
- fix(extract): census_usa + geonames write empty placeholder when credentials
  absent so SQLMesh staging models don't fail with "no files found"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:01:10 +01:00
Deeman
06cbdf80dc fix(extract): correct Eurostat + ONS response parsers against live APIs
eurostat_city_labels: API returns compact dimension JSON (category.label dict),
not SDMX 2.1 nested codelists structure. Fixed parser to read from
data["category"]["label"]. 1771 city codes fetched successfully.

ons_uk: observations endpoint (TS007A) is 404. Switched to CSV download via
/datasets/mid-year-pop-est/editions/mid-2022-england-wales — fetches ~68MB CSV,
filters to sex='all' + target year, aggregates population per LAD. 316 LADs ≥50K.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 00:26:13 +01:00
Deeman
bd1d29468a fix(content): handle None values in template pattern rendering
DuckDB rows can have NULL columns (e.g. market_score, median_peak_rate).
Replace None with 0 in render context so numeric Jinja2 filters like
round() and int don't crash with "NoneType doesn't define __round__".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 00:21:00 +01:00
Deeman
093c4c5d36 merge: data foundation + calculator v2
Part A — Population pipeline (Sprints 1–5):
- Eurostat SDMX city labels extractor (city_code → city_name)
- US Census ACS, ONS UK, GeoNames extractors
- 4 new staging models + stg_city_labels
- dim_cities: 5-source population cascade (Eurostat > Census > ONS > GeoNames > 0)
- city_market_profile: market score formula v2 (30/25/30/15 weights)

Part B — Calculator fixes 1–10:
- Fix 2 (HIGH): equity IRR uses -equity outflow, adds projectIrr (unlevered)
- Fix 8 (HIGH): OPEX inflates at annualOpexGrowth% from Y2
- Fix 1: annualRevGrowth now applied to all revenue streams
- Fix 3: NPV at hurdle rate (hurdleRate slider, npv/npvPositive)
- Fix 4: remaining loan via amortization math (not heuristic)
- Fix 5: exit EBITDA uses holdYears terminal year (not hardcoded Y3)
- Fix 6: leveraged MOIC + projectMoic
- Fix 7: value bridge (EBITDA growth vs debt paydown attribution)
- Fix 9: LTV/DSCR warnings in tab_metrics.html
- Fix 10: interest-only period slider

1229 tests pass.

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