Commit Graph

66 Commits

Author SHA1 Message Date
Deeman
84229e50f7 Merge branch 'worktree-supervisor-flags'
Python supervisor + DB-backed feature flags

- supervisor.py replaces supervisor.sh (topological wave scheduling, croniter)
- workflows.toml workflow registry (5 extractors, cron presets, depends_on)
- proxy.py round-robin + sticky proxy rotation via PROXY_URLS
- Feature flags: migration 0019, is_flag_enabled(), feature_gate() decorator
- Admin /admin/flags UI with toggle (admin-only)
- lead_unlock gate on unlock_lead route
- 59 new tests (test_supervisor.py + test_feature_flags.py)
- Fix is_flag_enabled bug (fetch_one instead of execute_fetchone)

# Conflicts:
#	CHANGELOG.md
#	web/pyproject.toml
2026-02-23 15:29:43 +01:00
Deeman
8b7d474ede docs: update CHANGELOG and PROJECT.md for supervisor + feature flags
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:27:23 +01:00
Deeman
024feeaac4 feat: SEO/GEO admin hub — GSC, Bing, Umami sync + search/funnel/scorecard views
# Conflicts:
#	CHANGELOG.md
#	uv.lock
#	web/src/padelnomics/admin/templates/admin/base_admin.html
#	web/src/padelnomics/core.py
2026-02-23 15:23:03 +01:00
Deeman
36b90eb4df docs: update CHANGELOG and PROJECT.md for SEO/GEO hub
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:09:25 +01:00
Deeman
325b897f38 Merge branch 'worktree-landing-backup'
# Conflicts:
#	CHANGELOG.md
2026-02-23 15:01:32 +01:00
Deeman
76814dade7 feat: landing zone backup to R2 via rclone + Litestream
Landing files (append-only JSON.gz) synced to R2 every 30 min via
systemd timer + rclone. Extraction state DB (.state.sqlite) continuously
replicated via Litestream (second DB entry). Auto-restore on container
startup for both app.db and .state.sqlite. Reuses existing R2 bucket
and credentials — no new env vars needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 14:06:16 +01:00
Deeman
49cadf6995 Merge branch 'worktree-sitemap-improvement'
# Conflicts:
#	web/src/padelnomics/admin/routes.py
2026-02-23 13:15:21 +01:00
Deeman
454b362c88 feat: admin email hub — sent log, inbox, compose, audiences, delivery tracking
Add full email management at /admin/emails with:
- email_log table tracking all outgoing emails with resend_id + delivery events
- inbound_emails table for Resend webhook-received messages
- Resend webhook handler (/webhooks/resend) updating delivery status in real-time
- send_email() returns resend_id (str|None) instead of bool; all 9 worker
  handlers pass email_type= for per-type filtering
- Admin UI: sent log with HTMX filters, email detail with API-enriched HTML
  preview, inbox with unread badges + reply, compose with branded wrapping,
  audience management with contact list/remove
- Sidebar Email section with unread badge via blueprint context processor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 13:00:23 +01:00
Deeman
1a6eae20d5 feat: pSEO CMS — SSG architecture with git templates + DuckDB
# Conflicts:
#	web/pyproject.toml
2026-02-23 12:51:30 +01:00
Deeman
f1181342ad feat: SSG-inspired pSEO CMS — git templates + DuckDB direct reads
Replace the old CSV-upload-based CMS with an SSG architecture where
templates live in git as .md.jinja files with YAML frontmatter and
data comes directly from DuckDB serving tables. Only articles and
published_scenarios remain in SQLite for routing/state.

- Content module: discover, load, generate, preview functions
- Migration 0018: drop article_templates + template_data, recreate
  articles + published_scenarios without FK references, add
  template_slug/language/date_modified/seo_head columns
- Admin routes: read-only template views with generate/regenerate/preview
- SEO pipeline: canonical URLs, hreflang (EN+DE), JSON-LD (Article,
  FAQPage, BreadcrumbList), Open Graph tags baked at generation time
- Example template: city-cost-de.md.jinja for German city market data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:25:44 +01:00
Deeman
894fd0c719 feat: email design & copy upgrade for all 9 transactional emails
Redesigned _email_wrap(): lowercase wordmark header matching website,
3px blue accent border, preheader text support, HR separators.
_email_button() now full-width block for mobile tap targets.

Rewrote copy: improved subject lines, urgency cues, quick-start links
in welcome, styled project recap in quote verify, heat badges on lead
forward, "what happens next" in lead matched, secondary CTAs.

~30 new/updated translation keys in both EN and DE.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:00:49 +01:00
Deeman
13c86ebf84 Merge branch 'worktree-extraction-overhaul'
# Conflicts:
#	transform/sqlmesh_padelnomics/models/foundation/dim_cities.sql
#	transform/sqlmesh_padelnomics/models/staging/stg_playtomic_venues.sql
2026-02-23 01:01:26 +01:00
Deeman
79f7fc6fad feat: Playtomic pricing/occupancy pipeline + email i18n + audience restructure
Three workstreams:

1. Playtomic full data extraction & transform pipeline:
   - Expand venue bounding boxes from 4 to 23 regions (global coverage)
   - New staging models for court resources, opening hours, and slot-level
     availability with real prices from the Playtomic API
   - Foundation fact tables for venue capacity and daily occupancy/revenue
   - City-level pricing benchmarks replacing hardcoded country estimates
   - Planner defaults now use 3-tier cascade: city data → country → fallback

2. Transactional email i18n:
   - _t() helper in worker.py with ~70 translation keys (EN + DE)
   - All 8 email handlers translated, lang passed in task payloads

3. Resend audiences restructured to 3 named audiences (free plan limit)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 00:54:53 +01:00
Deeman
e270d54f62 feat: sitemap hreflang alternates, caching, and lastmod cleanup
Extract sitemap generation to sitemap.py with xhtml:link hreflang
alternates (en/de/x-default) on every URL entry. Add 1-hour in-memory
TTL cache with Cache-Control header. Include supplier pages in both
languages (were EN-only). Drop misleading "today" lastmod from static
pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:13:32 +01:00
Deeman
cac3b3b324 docs: reorganize research docs + add project tracker and marketing strategy
Move historical docs from docs/ and .claude/ to research/. Add superseded
notice to research/PLAN.md. Add CHANGELOG entries for previous fixes.
New: PROJECT.md (task tracker), docs/MARKETING.md (marketing strategy).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:27:23 +01:00
Deeman
2db66efe77 feat: migrate transform to 3-layer architecture with per-layer schemas
Remove raw/ layer — staging models now read landing JSON directly.
Rename all model schemas from padelnomics.* to staging.*/foundation.*/serving.*.
Web app queries updated to serving.planner_defaults via SERVING_DUCKDB_PATH.
Supervisor gets daily sleep interval between pipeline runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 19:04:40 +01:00
Deeman
18ee24818b feat: copier update v0.9.0 — extraction docs, state tracking, architecture guides
Sync template from 29ac25b → v0.9.0 (29 template commits). Due to
template's _subdirectory migration, new files were manually rendered
rather than auto-merged by copier.

New files:
- .claude/CLAUDE.md + coding_philosophy.md (agent instructions)
- extract utils.py: SQLite state tracking for extraction runs
- extract/transform READMEs: architecture & pattern documentation
- infra/supervisor: systemd service + orchestration script
- Per-layer model READMEs (raw, staging, foundation, serving)

Also fixes copier-answers.yml (adds 4 feature toggles, removes stale
payment_provider key) and scopes CLAUDE.md gitignore to root only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 15:44:48 +01:00
Deeman
f958f1e816 feat(seo): expand city coverage to 40 cities + DuckDB refresh script
- 22 new cities: Madrid, Barcelona, Valencia, Seville, Málaga (ES),
  Paris, Lyon, Marseille (FR), Milan, Rome (IT), Amsterdam (NL),
  Vienna (AT), Zurich (CH), Stockholm (SE), Lisbon, Porto (PT),
  Brussels (BE), Dubai (AE), Sydney, Melbourne (AU), Dublin (IE)
- Total: 40 cities × EN + DE = 80 articles
- refresh_from_daas.py: sync template_data from planner_defaults
  serving table; dry-run mode; graceful when analytics unavailable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 00:01:37 +01:00
Deeman
a2de1a0206 feat(planner): wire DuckDB analytics + market-data endpoint
- analytics.py: open/close_analytics_db registered in app lifecycle
- GET /planner/api/market-data?city_slug=<slug>: returns per-city
  planner defaults from DuckDB planner_defaults serving table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 23:26:06 +01:00
Deeman
9f8ca82505 feat(sqlmesh): add transform workspace member with 4-layer DuckDB pipeline
Adds sqlmesh_padelnomics UV workspace member at transform/sqlmesh_padelnomics/.
DuckDB gateway, LANDING_DIR variable, @daily cron on all models.

Raw layer (reads landing zone gzip JSON):
  raw_overpass_courts    — OSM padel court elements (nodes with lat/lon/tags)
  raw_playtomic_tenants  — Playtomic venue records (tenant_id, location, name)
  raw_eurostat_population — Eurostat urb_cpop1 city population (unpivoted)

Staging layer (typed, deduped, country-resolved):
  stg_padel_courts       — OSM nodes only, ~100m bbox country approximation
  stg_playtomic_venues   — deduplicated Playtomic venues
  stg_population         — city population by year with integer types

Foundation layer:
  dim_venues             — deduped union of OSM + Playtomic (~100m grid)
  dim_cities             — Eurostat cities with population + venue counts

Serving layer (consumed by web app and SEO generation):
  city_market_profile    — OBT: market score, venue density, population per city
  planner_defaults       — per-city calculator pre-fill values with country median
                           fallbacks and competitive-pressure rate adjustments

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:03:59 +01:00
Deeman
af09597930 feat(daas): add extract workspace member with Overpass, Eurostat, Playtomic extractors
Adds padelnomics_extract UV workspace member at extract/padelnomics_extract/.
Implements three extractors in execute.py:
- extract_overpass(): global OverpassQL query for sport=padel OSM features
- extract_eurostat(): urb_cpop1 (city population) + ilc_di03 (NUTS2 income), etag-dedup
- extract_playtomic_tenants(): unauthenticated tenant search across 4 market bboxes,
  paginated, deduplicated by tenant_id, throttled at 1 req/2s

Landing zone at LANDING_DIR (default data/landing) with per-source subdirectories.
Entry point: `extract` script calls extract_dataset() for all three in sequence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 21:38:55 +01:00
Deeman
ceee85caba feat(content): programmatic SEO seed script + lang bug fixes
- Add scripts/seed_content.py: inserts EN + DE article templates and
  18 cities × 2 language data rows; run with --generate to produce 36
  pre-built SEO articles (Germany 8, USA 6, UK 4 cities) each with
  city-specific financial model overrides for unique content per article
- Fix bake_scenario_cards() to accept lang param and pass it to
  scenario card partials; German articles now render German labels
- Fix _generate_from_template() to extract language from data row and
  pass to calc() and bake_scenario_cards()
- Fix article_slug to use {template_slug}-{city_slug} preventing UNIQUE
  collision when multiple templates generate articles for same city
- Fix _rebuild_article() to pass lang to bake_scenario_cards()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 01:38:33 +01:00
Deeman
7c710ada6b refactor(planner): HTMX server-render refactor — eliminate JS SPA
Replace the 847-line client-side planner with an HTMX architecture:
- All tab content (CAPEX, Operating, Cash Flow, Returns, Metrics) rendered
  server-side as Jinja2 partials; slider changes POST to /planner/calculate
  which returns HTML; HTMX swaps into #tab-content
- Merge _PLANNER_TRANSLATIONS into _TRANSLATIONS; delete get_planner_translations()
  and window.__PADELNOMICS_LOCALE__; all strings now {{ t.key }} in templates
- New form_to_state() and augment_d() helpers in routes.py; calculate endpoint
  returns HTML instead of JSON; OOB swaps update header tag + wizard preview
- Add 5 Jinja2 filters: fmt_currency, fmt_k, fmt_pct, fmt_x, fmt_n
- Rewrite planner.js to ~200 lines: chart init on htmx:afterSettle, slider sync,
  toggle management, wizard nav, scenario save/load, reset to defaults
- Add 7 new template partials: tab_capex, tab_operating, tab_cashflow,
  tab_returns, tab_metrics, calculate_response, court_summary, wizard_preview
- Update test_phase0 to match new HTML-returning /calculate endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 17:12:28 +01:00
Deeman
e6f4c0a540 copy(de): Sie→Du overhaul, terminology & directory SEO copy
- All German UI copy switched from formal Sie/Ihr to informal Du/Dein
  across ~60 i18n.py keys and 10 template files
- Platz-Anbieter → Anbieter in CTAs; Anlage → Padel-Platz in planner
  wizard and planner translations (wiz_venue, sl_budget_target)
- Anlageplanung → Padelplatz-Planung in service checklist
- Directory H1: Padelplatz-Hersteller, Platzbauer & Anbieter (SEO)
- mkt_no_results / mkt_search_placeholder: Artikel → Märkte (EN + DE)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 16:15:07 +01:00
Deeman
9d1d42f6db feat: complete German translation of all public-facing content
Merges worktree-i18n-german branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 14:45:03 +01:00
Deeman
e66a55a8db fix: nav mobile layout + container width alignment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 14:44:53 +01:00
Deeman
ddbdc00a3d feat: complete German translation of all public-facing content
Translate the entire public-facing surface of the app to German,
using a hybrid approach: {{ t.key }} for short UI strings and
{% if lang == 'de' %} conditionals for prose blocks/FAQs.

Coverage:
- i18n.py: +300 UI keys, +200 planner JS locale strings, +35
  CAPEX/OPEX item name translations; new get_planner_translations()
  and get_calc_item_names() helpers
- base.html / _cookie_banner.html: nav, footer, cookie banner,
  feedback placeholder; JS toggle text injected via tojson
- public/: landing.html (hero, ROI calc, FAQ, SEO, JSON-LD),
  features.html, about.html — all with German meta tags
- planner/: planner.html (wizard, tabs, chart labels, CTAs),
  all export templates, scenario_list.html; window.__PADELNOMICS_LOCALE__
  injected server-side; planner.js all ~200 strings via tr()
- calculator.py: add lang param, translated CAPEX/OPEX item names,
  replace rent name-lookup with local rent_amount variable
- directory/: directory.html, supplier_detail.html, results.html,
  enquiry_result.html
- leads/: quote_step_1–9.html, quote_request.html, quote_submitted.html,
  quote_verify_sent.html; routes.py flash messages + _get_quote_steps(lang)
- suppliers/: signup flow (step 1–4), signup_success.html,
  waitlist.html, waitlist_confirmed.html
- content/: markets.html, article_detail.html, market_results.html,
  all scenario partials (summary, capex, cashflow, operating, returns)
- 629 tests pass, ruff clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 14:42:31 +01:00
Deeman
3c903bad97 fix: planner quote button — use lang-prefixed URL to prevent 404
The "Get Supplier Quotes" CTA in planner.js used a hardcoded
/leads/quote path which 404s because the route is registered at
/<lang>/leads/quote. Inject the correct URL server-side via
window.__PADELNOMICS_QUOTE_URL__ using url_for, consistent with
the existing __PADELNOMICS_CALC_URL__ / __PADELNOMICS_SAVE_URL__ pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 14:13:44 +01:00
Deeman
48587d6436 feat: responsive nav — hamburger menu + wider container (900px breakpoint)
- Widen nav container from 72rem to 80rem (1280px) — matches Zillow's
  nav container width, more breathing room for items on large monitors
- Raise collapse breakpoint from 768px to 899px — links stay visible
  until the screen is actually too narrow
- Add hamburger button (SVG 3-line icon) visible at < 900px
- Add mobile drop-down panel with all nav links grouped under Plan /
  Explore / Account sections; overlay + Escape key close it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 14:04:56 +01:00
Deeman
32f29c53ec fix: improve German nav labels
Verzeichnis → Anbieterverzeichnis (self-explanatory, matches search terms)
Planer → Kostenrechner (unambiguous, signals purpose immediately)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 13:39:57 +01:00
Deeman
86c4ebf25d fix: add missing env vars to CI .env heredoc
WAITLIST_MODE, LEADS_EMAIL, UMAMI_API_URL were set in GitLab CI but
never written to .env. Paddle vars made optional (:-) so deploys work
without them when in waitlist mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 13:10:48 +01:00
Deeman
358bc5c02f fix: use kill -0 1 for litestream healthcheck
pgrep may not be available in the litestream image. kill -0 1 checks
whether PID 1 (litestream, after exec) is alive — works in any container.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 13:08:00 +01:00
Deeman
76fc19c183 fix: litestream healthcheck gate + 1yr retention
Re-enable deploy gate on litestream: pgrep-based healthcheck with 6
retries (30s window) after a 15s start period — broken backups now
fail the deploy loudly instead of silently succeeding.

Extend retention from 7d to 1yr (8760h): WAL frames are tiny for a
low-traffic app, R2 free tier covers years of storage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 13:00:29 +01:00
Deeman
b0f36192a6 fix: litestream single replica + disable healthcheck gate
v0.5.8 dropped multi-replica support — remove the local path replica,
keeping only R2. Also disable litestream's healthcheck so deploy's
`up --wait` isn't gated on the backup service.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 12:54:19 +01:00
Deeman
dc02563e52 fix: write nginx config before container start to fix first-deploy health check
Router health check (nginx -t) fails when default.conf doesn't exist yet.
Move config write to before `up -d --wait` so nginx has a valid config
on first deploy or after a volume wipe. Router reload stays post-health-check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 12:45:37 +01:00
Deeman
449ff413e3 chore: update virtual office address in all legal pages
Replace home address with c/o COCENTER, Koppoldstr. 1, 86551 Aichach
in imprint_de, imprint_en, privacy_de, privacy_en, and terms_de.
Jurisdiction clause ("Gerichtsstand Oldenburg") left untouched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 11:44:14 +01:00
Deeman
c0c8607664 fix: migration atomicity + deploy hardening + Litestream R2
Migration atomicity:
- Remove conn.commit() and executescript() from all up() functions (0000,
  0011, 0012, 0013, 0014, 0015); executescript() issued implicit COMMITs
  which broke the batch-rollback guarantee of the migration runner
- Rewrite 0000 with individual conn.execute() calls (was a single
  executescript block)

Deploy hardening:
- Add pre-migration DB backup step to deploy.sh: saves
  app.db.pre-deploy-<timestamp> in the volume before every migration
- On health-check failure: restore the backup, then stop + exit
- On success: clean up old backups (keep last 3)

Litestream:
- Enable R2 as primary replica in litestream.yml (env-var placeholders)
- Add local /app/data/backups as secondary replica
- docker-compose: add auto-restore on empty volume (sh entrypoint runs
  'litestream restore' before 'litestream replicate' if app.db missing)
- Add LITESTREAM_R2_* vars to .gitlab-ci.yml .env block and .env.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:28:59 +01:00
Deeman
363f93885d fix: ruff clean + all visual tests passing
- Ruff: auto-fixed 43 errors (unused imports, unsorted imports, bare
  f-strings); manually fixed 6 remaining (unused vars, ambiguous `l`)
- Visual tests: server now builds schema via migrate() instead of the
  deleted schema.sql; fixes ERR_CONNECTION_REFUSED on all tests
- Visual tests: updated assertions for current landing page (text logo
  replacing img, .roi-calc replacing .teaser-calc, intentional dark
  sections hero-dark/cta-card allowed, card count >=6, i18n-prefixed
  logo href, h3 brightness threshold relaxed to 150)
- CSS: remove dead .nav-logo { line-height: 0 } (was for image logo,
  collapsed text logo to zero height) and .nav-logo img rules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 08:49:47 +01:00
Deeman
b416cd682a feat: i18n URL prefixes + German legal pages
All public-facing blueprints (public, planner, directory, content,
leads, suppliers) now serve under /<lang>/ (e.g. /en/, /de/). Internal
blueprints (auth, dashboard, admin, billing) are unchanged.

URL routing:
- Root / detects lang from cookie → Accept-Language → default 'en'
  and 301-redirects to /<lang>/
- Quart url_value_preprocessor pops <lang> into g.lang; url_defaults
  auto-injects it so existing url_for() calls need no changes
- Unsupported lang prefixes (e.g. /fr/) return 404
- Legacy bare URLs (/terms, /privacy, /imprint, /about, /features,
  /suppliers) redirect 301 to /en/ equivalents
- robots.txt and sitemap.xml moved to app-level root; sitemap now
  includes both en and de variants of every SEO page
- lang cookie persisted 1 year, SameSite=Lax

i18n:
- New i18n.py: SUPPORTED_LANGS, LANG_BLUEPRINTS, flat translation dicts
  for ~20 nav/footer keys in en + de
- lang and t injected into every template context

Templates:
- base.html: <html lang="{{ lang }}">, hreflang tags (en/de/x-default)
  on lang-prefixed pages, nav/footer strings translated via t.*, footer
  language toggle EN | DE, SVG racket logo removed from footer
- 6 legal templates (terms/privacy/imprint × en/de) replacing old 3:
  - English: GDPR sections with correct controller identity (Hendrik
    Dreesmann, Zum Offizierskasino 1, 26127 Oldenburg), real sub-
    processors (Umami self-hosted, Paddle, Resend with SCCs), German-
    law jurisdiction
  - German: DSGVO-konforme Datenschutzerklärung, AGB, Impressum per
    § 5 DDG; Kleinunternehmer § 19 UStG; LfD Niedersachsen reference

Tests updated to use /en/ prefixed routes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 02:29:42 +01:00
Deeman
a7b38339a6 feat: cookie consent banner, defer Paddle.js to checkout pages
- Add cookie consent banner (_cookie_banner.html) — fixed bottom bar with
  "Accept all" and "Manage preferences" (toggles for Essential/Functional);
  consent stored in cookie_consent cookie (1 year); no-JS = only essential
  cookies set (privacy-safe default)
- Add "Manage Cookies" link to footer Legal section to re-open the banner
- Extract Paddle.js init into _paddle.html partial; add {% block paddle %}
  to base.html (empty by default); override on export, supplier signup, and
  supplier dashboard pages — Paddle.js no longer loads on every page visit
- Gate ab_test() on functional cookie consent: variant picked per-request
  always, but ab_* cookie only persisted when visitor has consented
- Update privacy policy section 6: full cookie disclosure (essential,
  functional, payment categories + Umami cookieless note); fix "Plausible"
  → "Umami" in service providers list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 01:48:08 +01:00
Deeman
bc7fbcd595 chore: commit pending changes — logo, base template, scratch designs, changelog
- favicon.svg: pending logo tweaks
- base.html: pending template changes
- CHANGELOG.md: add waitlist mode and logo redesign entries missed in prior commits
- scratch/: add design prototype HTML/JSX files, remove old markdown notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 23:45:42 +01:00
Deeman
77d801e326 feat: auto-create Resend audiences per blueprint
Removes RESEND_AUDIENCE_WAITLIST env var. capture_waitlist_email() now
derives audience name from request.blueprints[0] (e.g. waitlist-auth,
waitlist-suppliers), lazily creates it via Resend API on first signup,
and caches the ID in a new resend_audiences table. Zero config beyond
RESEND_API_KEY — adding @waitlist_gate to any new blueprint auto-creates
its audience on first use.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 23:41:45 +01:00
Deeman
5de83d820c add simple A/B testing with @ab_test decorator and Umami data-tag
Assigns visitors to experiment variants via cookie (30-day), sets g.ab_variant
and g.ab_tag, and conditionally adds data-tag to the Umami script tag so all
pageviews and events are automatically tagged in the Umami dashboard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 20:07:50 +01:00
Deeman
b108a53ef3 updates 2026-02-19 19:16:23 +01:00
Deeman
321d321ba9 add image-first directory card redesign and cover image upload
- 4-tier visual ladder: Free (muted grey) → Basic (verified presence) →
  Growth (stats + quotes) → Pro (court media + full stats + green glow)
- New card layout: 16:9 cover media, frosted category badge, logo avatar
  straddling media/body border (body-relative to avoid overflow:hidden clip)
- Pro default: CSS court visualization placeholder; Growth/Basic: dark-green
  grid placeholder; Free: grey/desaturated placeholder
- Cover image upload in supplier dashboard (saves to static/uploads/covers/)
- Migration 0013: cover_image TEXT column on suppliers table
- Updated prototype (scratch/design_directory_cards.html) with Basic tier
  cards, fixed logo-wrap positioning across all tiers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 18:17:54 +01:00
Deeman
536eefffdb add Basic tier, monthly/yearly billing, and supplier detail redesign
- New Basic tier (€39/mo or €349/yr): verified directory listing with
  enquiry form, contact sidebar, services checklist, social links, no leads
- Monthly + yearly billing for all paid tiers; yearly defaults selected
  in signup wizard with CSS-only price toggle (no JS state)
- Redesigned supplier_detail.html: navy hero with court-grid pattern,
  two-column body+sidebar for Basic+, tier-adaptive CTA strips
- Supplier enquiry form: HTMX-powered, rate-limited 5/24h, email relayed
  via worker task; supplier_enquiries table tracks all submissions
- New supplier columns: services_offered, contact_role, linkedin_url,
  instagram_url, youtube_url (migration 0012)
- _lead_tier_required decorator restricts lead feed to growth/pro;
  Basic users see overview + listing tabs only
- Admin: basic tier in dropdown, new fields in form/detail + enquiry count
- setup_paddle.py: adds 4 new products with yearly interval support
- Webhook handler strips _monthly/_yearly suffixes, Basic gets 0 credits
  and is_verified=1; existing growth/pro webhooks unchanged
- Sort order: pro > growth > basic > free
- 572 tests pass (+2 new for basic tier + yearly webhook variants)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 15:03:21 +01:00
Deeman
07c7e61049 refactor migration system: single source of truth via replay
Eliminated dual-maintenance of schema.sql + versioned migrations.
All databases (fresh and existing) now replay migrations in order
starting from 0000_initial_schema.py. Removed _is_fresh_db() and
the fresh-DB fast-path that skipped migration execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 00:23:28 +01:00
Deeman
0b8350c770 fix webhook crashes on null custom_data, migrate to SDK Verifier
Paddle sandbox sends lifecycle events (subscription.updated, etc.) with
"custom_data": null. The .get("custom_data", {}) default only applies
when the key is missing, not when the value is explicitly null, causing
AttributeError on the next .get() call. Also guarded subscription.activated
to skip when user_id is absent (was inserting user_id=0 → FK violation).

Replaced manual HMAC verification with paddle_billing.Notifications.Verifier
via a lightweight _WebhookRequest wrapper satisfying the SDK's Request Protocol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 22:43:40 +01:00
Deeman
df8a747463 add credit system and supplier webhook test suites, remove dead test
24 tests for credits.py (balance, spend, unlock, refill, ledger) and
10 integration tests for supplier webhook handlers (credit packs, sticky
boosts, subscription activation, business plan purchase). Removed
test_mobile_nav_no_overflow which never asserted its JS result.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 22:02:09 +01:00
Deeman
0fe5ab1259 fix supplier dashboard UX, Paddle integration, and dev tooling
Supplier dashboard: impersonate redirects to supplier dashboard when user
owns a supplier, sidenav active tab updates on click, listing preview
capped at 420px, insufficient-credits error shows balance and buy CTA,
boost buy buttons resolve Paddle price IDs server-side.

Dev tooling: dev_run.sh kills child processes on Ctrl-C (process
substitution fix), syncs Paddle products to DB on each run,
setup_paddle.py gains --sync mode and fixes Duration/Interval SDK change.

Seed data: each claimed supplier gets its own owner user and subscription.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 19:40:06 +01:00