Commit Graph

260 Commits

Author SHA1 Message Date
Deeman
831233cb29 fix(admin): add missing article_stats route, 500 handler, dev debug mode
- Add /admin/articles/stats HTMX partial endpoint that was referenced
  by article_stats.html but never created (caused 500 during generation)
- Add @app.errorhandler(500) to log exceptions with traceback
- Switch dev_run.sh from Granian to Quart debug mode for browser
  tracebacks and auto-reload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 09:34:22 +01:00
Deeman
c5327c4012 fix(maps): move VENUE_ICON creation after Leaflet loads
L.divIcon() was called at IIFE top level before the dynamic Leaflet
script loaded, throwing ReferenceError and preventing all maps from
rendering. Move icon creation into script.onload callback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 09:01:54 +01:00
Deeman
93c9408f6b fix(admin): render Leaflet maps in template preview
The .card wrapper has overflow:hidden which clips Leaflet's
absolutely-positioned tile layers. Override to overflow:visible
on the rendered-article card. Add .catch() to map fetch calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 22:58:27 +01:00
Deeman
e9b4faa05c fix(admin): move map scripts inline in template preview
Put Leaflet init scripts inside admin_content block instead of relying
on the scripts block inheritance chain through base_admin → base.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 22:33:16 +01:00
Deeman
9515ec8ae9 fix(admin): load Leaflet maps in template preview page
The /admin/templates/<slug>/preview/<key> page renders article HTML
directly but never loaded Leaflet CSS/JS, so country-map and city-map
divs appeared empty.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 22:29:00 +01:00
Deeman
4ee80603ef fix(articles): load Leaflet maps in article editor preview
The admin article preview iframe was missing Leaflet CSS/JS and had
scripts blocked by the sandbox policy, so map shortcodes rendered as
empty divs.

- Extract inline map script to static/js/article-maps.js (shared
  between article_detail.html and admin preview)
- Replace f-string preview doc with a proper Jinja template that
  includes Leaflet assets
- Add allow-scripts to iframe sandbox on both initial load and HTMX
  preview updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 22:03:50 +01:00
Deeman
2e42245ad5 fix(worker): use sqlmesh run prod instead of plan prod --auto-apply
`plan --auto-apply` only detects SQL model changes and won't re-run
for new data. `run prod` evaluates missing cron intervals and picks
up newly extracted data — matching the fix already applied to the
supervisor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 21:49:51 +01:00
Deeman
c54eb50004 fix(planner): prevent chart containers from overflowing on small screens
Grid children default to min-width:auto, letting the Chart.js canvas
push the container wider than its grid track. Adding min-width:0 and
overflow:hidden constrains charts to their column width.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 18:27:27 +01:00
Deeman
6b7fa45bce feat(admin): add pipeline diagnostic script + extraction card UX improvements
- Add scripts/check_pipeline.py: read-only diagnostic for pricing pipeline
  row counts, date range analysis, HAVING filter impact, join coverage
- Add description field to all 12 workflows in workflows.toml
- Parse and display descriptions on extraction status cards
- Show spinner + "Running" state with blue-tinted card border
- Display start time with "running..." text for active extractions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:40:12 +01:00
Deeman
b064e18aa1 fix(admin): resolve workflows.toml path via CWD instead of __file__
All checks were successful
CI / test (push) Successful in 54s
CI / tag (push) Successful in 3s
In prod the package is installed in a venv, so __file__.parents[4] doesn't
reach the repo root. Use CWD (repo root in both dev and prod via systemd
WorkingDirectory) with REPO_ROOT env var override.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 14:39:30 +01:00
Deeman
66c2dfce66 fix(billing): fetch line items for checkout.session.completed webhooks
_extract_line_items() was returning [] for all checkout sessions, which
meant _handle_transaction_completed never processed credit packs, sticky
boosts, or business plan PDF purchases. Now fetches line items from the
Stripe API using the session ID, with a fallback to embedded line_items.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:49:41 +01:00
Deeman
6e3c5554aa fix(admin): enable bulk actions in grouped articles view
All checks were successful
CI / test (push) Successful in 54s
CI / tag (push) Successful in 3s
- dev_run.sh: also remove app.db-shm and app.db-wal on reset to fix
  SQLite disk I/O error from stale WAL/SHM files
- articles bulk: add checkboxes to grouped rows (data-ids holds all
  variant IDs); checking a group selects EN+DE together
- restore select-all checkbox in grouped <th>
- add toggleArticleGroupSelect() JS function
- fix htmx:afterSwap to re-check group checkboxes correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 09:48:54 +01:00
Deeman
ad02140594 fix(quote): add missing required asterisk and error hint to step 4
All checks were successful
CI / test (push) Successful in 54s
CI / tag (push) Successful in 3s
Step 4 (Project Phase) required location_status server-side but had no
visual "*" indicator and no error message when submitting without a
selection. All other steps already had both.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:21:57 +01:00
Deeman
5bcd87d7e5 fix(ci): replace non-existent quote.wizard endpoint with leads.quote_request
All checks were successful
CI / test (push) Successful in 53s
CI / tag (push) Successful in 3s
The CRO homepage overhaul (f4f8a45) introduced url_for('quote.wizard')
in landing.html, but that endpoint never existed — the actual route is
leads.quote_request. This broke CI runs #99–#109.

Also adds landing_vs_col_us to i18n allowlist (brand name, same in both
languages).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:12:45 +01:00
Deeman
77772b7ea4 feat(maps): beanflows-style divIcon bubbles + feature flag gate
Replace L.circleMarker with L.divIcon + .pn-marker CSS class (white
border, box-shadow, hover scale) matching the beanflows growing
conditions map pattern. Dark .map-tooltip CSS override (no arrow,
dark navy background). Small venue dots use .pn-venue class.

Add _require_maps_flag() to all 4 API endpoints (default=True so
dev works without seeding the flag row). Gate /opportunity-map route
the same way.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 20:51:00 +01:00
Deeman
6e936dbb95 feat(maps): Phase 5 — standalone opportunity map page
New route GET /<lang>/opportunity-map renders a full-width Leaflet map
with a country selector. On country change, fetches
/api/opportunity/{slug}.json and renders opportunity circles
(color-coded by score, sized by population) plus existing-venue gray
reference dots from /api/markets/{country}/cities.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:32:56 +01:00
Deeman
edf678ac4e feat(maps): Phase 4 — city venue dot map
New serving model: city_venue_locations joins dim_venues + dim_cities
to expose lat/lon/court_count per venue for the city dot map endpoint.

pseo_city_costs_de.sql: add c.lat, c.lon so city-cost articles have
city coordinates for the #city-map data attributes.

city-cost-de.md.jinja: add #city-map div (both DE and EN sections)
after the stats strip. Leaflet init handled by article_detail.html.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:07:06 +01:00
Deeman
0eef455543 feat(maps): Phase 3 — country overview city bubble map + article_detail Leaflet loader
Add #country-map div to country-overview.md.jinja (both DE/EN).
article_detail.html: always include Leaflet CSS, conditionally load
Leaflet JS only when #country-map or #city-map divs are present.
Initializes country city-bubble map and city venue-dot map from
/api/markets/{slug}/cities.json and /api/markets/{country}/{city}/venues.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:15:41 +01:00
Deeman
8e53fda283 feat(maps): Phase 2 — markets hub country bubble map
Add Leaflet map to /markets with country-level bubbles sized by
total_venues and colored by avg_market_score. Click navigates to
country overview page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:04:40 +01:00
Deeman
db0d7cfee9 feat(maps): Phase 1 — Leaflet vendor files, API blueprint, app registration
Self-host Leaflet 1.9.4 JS/CSS/images in static/vendor/leaflet/.
Create api.py blueprint with 4 JSON endpoints for map data.
Register api_bp at /api in app.py (before content catch-all).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:46:13 +01:00
Deeman
81487d6f01 feat(admin): bulk actions for articles and leads
Add bulk selection checkboxes and action bars to the articles and leads
admin pages, replicating the existing supplier bulk pattern.

Articles: publish, unpublish, toggle noindex, rebuild, delete (with
confirmation dialog). Leads: set status, set heat. Both re-render the
results partial after action via HTMX, preserving current filters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 09:40:26 +01:00
Deeman
230406f34f fix(billing): period_end from Stripe items + test 2026-03-03 18:06:01 +01:00
Deeman
7da6a4737d fix(billing): extract current_period_end from Stripe subscription items
Stripe API 2026-02+ moved current_period_end from subscription to
subscription items. Add _get_period_end() helper that falls back to
items[0].current_period_end when the subscription-level field is None.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 18:05:55 +01:00
Deeman
710e21a186 fix(billing): handle customer.subscription.created + test isolation 2026-03-03 17:58:15 +01:00
Deeman
72c4de91b0 fix(billing): handle customer.subscription.created webhook + test isolation
- Add customer.subscription.created → subscription.activated mapping in
  stripe.parse_webhook so direct API subscription creation also creates DB rows
- Add customer.subscription.created to setup_stripe.py enabled_events
- Pin PAYMENT_PROVIDER=paddle and STRIPE_WEBHOOK_SECRET="" in test conftest
  so billing tests don't hit real Stripe API when env has Stripe keys
- Add 8 unit tests for stripe.parse_webhook covering all event types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:29:13 +01:00
Deeman
046be665db merge: fix remaining request_options in stripe.py 2026-03-03 16:46:48 +01:00
Deeman
7c5fa86fb8 fix(billing): remove remaining request_options from Price.retrieve calls
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:46:25 +01:00
Deeman
0a9f980813 merge: fix Stripe SDK request_options + webhook endpoint graceful failure 2026-03-03 16:36:58 +01:00
Deeman
2682e810fa fix(billing): remove invalid request_options from Stripe SDK calls
Stripe Python SDK doesn't accept request_options as a kwarg to create/retrieve/modify.
Timeouts are handled by the global max_network_retries setting.
Also gracefully handle webhook endpoint creation failure for localhost URLs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:36:47 +01:00
Deeman
10af6a284c fix(content): slug transliteration, article links, country overview ranking
Some checks failed
CI / test (push) Failing after 30s
CI / tag (push) Has been skipped
# Conflicts:
#	CHANGELOG.md
2026-03-03 16:29:41 +01:00
Deeman
fab16cb48f fix(billing): accept STRIPE_API_PRIVATE_KEY / STRIPE_API_PUBLIC_KEY env var names
Also normalise PAYMENT_PROVIDER to lowercase so STRIPE/stripe both work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:24:03 +01:00
Deeman
80c2f111d2 feat(billing): B4-B5 — tests, lint fixes, CHANGELOG + PROJECT.md
- Fix unused imports in stripe.py (hashlib, hmac, time)
- Update test_billing_routes.py: insert into payment_products table,
  fix mock paths for extracted paddle.py, add Stripe webhook 404 test
- Update CHANGELOG.md with Stripe provider feature
- Update PROJECT.md Done section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:07:30 +01:00
Deeman
7ae8334d7a feat(billing): B3 — setup_stripe.py product/price creation script
Mirrors setup_paddle.py structure:
- Creates 17 products + prices in Stripe (same keys, same prices)
- Writes to payment_products table with provider='stripe'
- Registers webhook endpoint at /billing/webhook/stripe
- tax_behavior='exclusive' (price + VAT on top, EU standard)
- Supports --sync flag to re-populate from existing Stripe products

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:53:38 +01:00
Deeman
032fe8d86c feat(billing): B2 — Stripe payment provider implementation
billing/stripe.py exports the same interface as paddle.py:
- build_checkout_payload() → Stripe Checkout Session with automatic_tax
- build_multi_item_checkout_payload() → multi-line-item sessions
- cancel_subscription() → cancel_at_period_end=True
- get_management_url() → Stripe Billing Portal session
- verify_webhook() → Stripe-Signature header verification
- parse_webhook() → maps Stripe events to shared format:
  checkout.session.completed → subscription.activated / transaction.completed
  customer.subscription.updated → subscription.updated
  customer.subscription.deleted → subscription.canceled
  invoice.payment_failed → subscription.past_due

All API calls have 10s timeout and max 2 retries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:48:08 +01:00
Deeman
bf69270913 feat(billing): A6 — planner/supplier routes use get_price_id() + _provider()
- planner/routes.py: import get_price_id instead of get_paddle_price,
  export_checkout uses _provider().build_checkout_payload()
- suppliers/routes.py: all get_paddle_price → get_price_id,
  signup_checkout uses _provider().build_multi_item_checkout_payload(),
  dashboard boosts use get_all_price_ids() bulk load

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:36:12 +01:00
Deeman
8f0a56079f feat(billing): A5 — dual-path JS templates for Paddle overlay / Stripe redirect
- New _payment_js.html: conditionally loads Paddle.js or nothing (Stripe
  uses server-side Checkout Session). Provides startCheckout() helper.
- All checkout templates use _payment_js.html instead of _paddle.html
- export.html, signup_step_4.html: Paddle.Checkout.open() → startCheckout()
- dashboard_boosts.html: inline onclick → buyItem() with server round-trip
- New /billing/checkout/item endpoint for single-item purchases (boosts, credits)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:31:52 +01:00
Deeman
7af9b2c82c feat(billing): A2+A4 — extract paddle.py + dispatch layer in routes.py
- New billing/paddle.py: Paddle-specific functions (build_checkout_payload,
  cancel_subscription, get_management_url, verify_webhook, parse_webhook)
- routes.py: _provider() dispatch function selects paddle or stripe module
- Checkout/manage/cancel routes now delegate to _provider()
- /webhook/paddle always active (existing subscribers)
- /webhook/stripe endpoint added (returns 404 until Stripe configured)
- Shared _handle_webhook_event() processes normalized events from any provider
- _price_id_to_key() queries payment_products with paddle_products fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:26:47 +01:00
Deeman
276328af33 feat(billing): A1+A3 — payment_products table + provider-agnostic price lookups
- Migration 0028: create payment_products table, copy paddle_products rows
- Add STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET config
- Make PAYMENT_PROVIDER read from env (was hardcoded "paddle")
- Add get_price_id() / get_all_price_ids() querying payment_products
- Keep get_paddle_price() as deprecated fallback alias

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:07:10 +01:00
Deeman
a00c8727d7 fix(content): slugify transliteration + article links + country overview ranking
- Add @slugify SQLMesh macro (STRIP_ACCENTS + ß→ss) replacing broken
  inline REGEXP_REPLACE that dropped non-ASCII chars (Düsseldorf → d-sseldorf)
- Apply @slugify to dim_venues, dim_cities, dim_locations
- Fix Python slugify() to pre-replace ß→ss before NFKD normalization
- Add language prefix to B2B article market links (/markets/germany → /de/markets/germany)
- Change country overview top-5 ranking: venue count (not raw market_score)
  for top cities, population for top opportunity cities

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 10:46:30 +01:00
Deeman
0fc0ca66b1 fix(i18n): replace smart quotes with straight quotes in sup_hero_sub
Some checks failed
CI / test (push) Failing after 29s
CI / tag (push) Has been skipped
Curly quotes (U+201C/U+201D) were used as JSON key/value delimiters
on line 894 of both locale files, making them invalid JSON.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 06:53:39 +01:00
Deeman
695e956501 feat(cro): German translations for all CRO copy changes
Native-quality DE translations for homepage + supplier page:
- Hero, ROI, features, FAQ, final CTA, meta/SEO
- Proof strip, struggling moments, "Why Padelnomics" comparison
- Supplier proof points, ROI line, struggling moments, pricing CTAs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 06:42:16 +01:00
Deeman
a862d21269 feat(cro): supplier page CRO — struggling moments, conditional stats, honest proof
Task 3: Add "Is this your sales team?" struggling-moments section.
Conditional stats display (hide if below thresholds). Replace anonymous
testimonials with data-backed proof points. Tier-specific pricing CTAs.
Tighter hero sub-headline. Move ROI callout above pricing grid.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 06:36:46 +01:00
Deeman
f4f8a45654 feat(cro): homepage structural overhaul — proof strip, struggling moments, comparison
Task 2: Remove journey timeline (3 "SOON" badges = incomplete signal).
Add proof strip below hero with live stats. Add "Sound familiar?"
section with 4 JTBD struggling-moment cards. Add "Why Padelnomics"
3-column comparison (DIY vs consultant vs us). Update hero secondary
CTA and supplier matching links to /quote. Route handler now passes
calc_requests and total_budget_millions to template.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 06:33:43 +01:00
Deeman
9e471f8960 feat(cro): rewrite homepage EN copy — outcome-focused JTBD framing
Task 1: Hero, features, FAQ, final CTA, supplier matching, meta/SEO
strings all rewritten. New keys added for proof strip, struggling-
moments section, and "Why Padelnomics" comparison section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:31:24 +01:00
Deeman
34f8e45204 merge(articles): iframe preview + collapsible meta + word count 2026-03-02 12:09:04 +01:00
Deeman
6b9187f420 fix(articles): iframe preview + collapsible meta + word count
Replace the auto-escaped `{{ body_html }}` div (showed raw HTML tags)
with a sandboxed `<iframe srcdoc>` pattern matching the email preview.
Both the initial page load and the HTMX live-update endpoint now build
a full `preview_doc` document embedding the public CSS and wrapping
content in `<div class="article-body">` — pixel-perfect against the
live article, admin styles fully isolated.

Also:
- Delete ~65 lines of redundant `.preview-body` custom CSS
- Add "Meta ▾" toolbar toggle to collapse/expand metadata strip
- Add word count footer in the editor pane (updates on input)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:01:16 +01:00
Deeman
94d92328b8 merge: fix article .md lookup + lighter editor
All checks were successful
CI / test (push) Successful in 51s
CI / tag (push) Successful in 3s
2026-03-02 11:47:13 +01:00
Deeman
100e200c3b fix(articles): find .md by slug scan + lighter editor theme
Two fixes:
- _find_article_md() scans _ARTICLES_DIR for files whose frontmatter
  slug matches, so padel-halle-bauen-de.md is found for slug
  'padel-halle-bauen'. The previous exact-name lookup missed any file
  where the filename ≠ slug (e.g. {slug}-{lang}.md naming convention).
- Editor pane: replace dark navy background with warm off-white (#FEFDFB)
  and dark text so it reads like a document, not a code editor.
2026-03-02 11:43:26 +01:00
Deeman
70628ea881 merge(pipeline-transform-tab): split article editor + frontmatter fix + transform tab features
All checks were successful
CI / test (push) Successful in 50s
CI / tag (push) Successful in 2s
2026-03-02 11:34:13 +01:00
Deeman
d619f5e1ef feat(articles): split editor with live preview + fix frontmatter bug
Bug: article_edit GET was passing raw .md file content (including YAML
frontmatter) to the body textarea. Articles synced from disk via
_sync_static_articles() had their frontmatter bled into the editor,
making it look like content was missing or garbled.

Fix: strip frontmatter (using existing _FRONTMATTER_RE) before setting
body, consistent with how _rebuild_article() already does it.
Also switch to _ARTICLES_DIR (absolute) instead of relative path.

New: split editor layout — compact metadata strip at top, dark
monospace textarea on the left, live rendered preview on the right
(HTMX, 500ms debounce). Initial preview server-rendered on page load.
New POST /admin/articles/preview endpoint returns the preview partial.
2026-03-02 11:10:01 +01:00