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>
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>
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>
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>
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>
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>
- 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>
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>
Browsers were fetching favicon.ico and ignoring the external SVG link.
Inline data URI has no caching layer so the 'p' lettermark shows immediately.
Drops the .ico and .png favicon links (Apple touch icon kept for home screen).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nav: switch nav-inner from flex/space-between to grid (1fr auto 1fr)
so logo is always geometrically centered regardless of link count per side;
update input.css source of truth + inline override in base.html
- Footer: remove inline SVG racket, text-only wordmark matching the nav
- favicon.svg: replace padel racket with navy rounded square + white 'p'
lettermark
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Slide-up entrance / slide-down exit animations (CSS keyframes)
- Upward box-shadow for visual depth from page content
- Larger toggle (44×24px) for easier tap target on mobile
- Split dual-state Manage/Save into: header button Manage↔Close + panel Save choices button
- ARIA: role=dialog, role=switch, aria-checked on toggle, focus moves to prefs on open
- Use btn-outline / btn component classes for secondary/primary buttons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
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>
Extract duplicated waitlist logic into two reusable abstractions:
1. @waitlist_gate(template, **context) decorator
- Intercepts GET requests when WAITLIST_MODE=true
- Passes through POST requests for form handling
- Evaluates callable context at request time
2. capture_waitlist_email(email, intent, plan, email_intent) helper
- Idempotent DB insertion (INSERT OR IGNORE)
- Enqueues confirmation email only for new signups
- Adds to Resend audience with silent error handling
- Supports separate email_intent for supplier messaging
Applied to auth, suppliers, and planner routes, reducing ~80+ lines
of duplicated code. Added comprehensive tests (55 total, all passing)
and documentation (25KB guide in docs/WAITLIST.md).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace globe lines with 3x2 dot grid (characteristic padel racket holes)
- Makes racket immediately recognizable vs globe/frying pan shape
- Add ?v=3 to all favicon links to force browser cache refresh
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace circle with rounded rectangle (rx=6, ~30% corner radius)
matching the typical padel racket head proportions
- Handle now connects from bottom edge of rect
- New favicon.svg plus regenerated ico/png/apple-touch-icon from same design
- Add SVG favicon link (modern browsers) with ico/png fallbacks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add /imprint route and template stub (fill in company details)
- Move About out of Legal into a new Company column
- Add /imprint to sitemap and RESERVED_PREFIXES
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- Fix CSS-only billing toggle: radio inputs were nested inside
.s-billing-toggle, so ~ siblings couldn't reach plan card price
elements inside <form>. Moved radios to be direct siblings of both
.s-billing-toggle and <form>. Updated CSS selectors accordingly.
Removed .price-yearly{display:block} override that broke monthly view.
- Set Most Popular badge on Growth (not Pro) in signup wizard — Growth
is the natural entry point for lead-receiving suppliers
- Update public pricing page: Growth €149→€199, Pro €399→€499
- Refresh Pro feature list: logo+cover photo, full stats, verified badge
(no longer described as add-ons since they're built into Pro)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
run_with_label piped output through a while loop, so $! tracked the
formatter PID instead of the actual command. Ctrl-C killed the formatters
but left app/worker/css-watch running as orphans. Switch to process
substitution so $! is the real command PID, and pkill -P children before
killing tracked PIDs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Webhook handler called Verifier().verify() with raw bytes instead of a
request object, so signature verification always failed. Replaced with
manual HMAC check matching Paddle's ts=...;h1=... format. Updated tests
to produce correct signature format, mock the SDK instead of httpx for
manage/cancel routes, and expect JSON for overlay checkout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a complete content generation system for producing SEO articles
at scale with embedded financial scenario widgets. Includes DB schema
(published_scenarios, article_templates, template_data, articles with
FTS5), bulk generation pipeline with staggered publish dates, admin CRUD
for templates/scenarios/articles, public markets hub with HTMX filtering,
catch-all article serving from pre-rendered static HTML, sitemap
integration, and 94 pytest tests covering the full stack.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
setup_paddle.py creates a notification destination in Paddle and writes
the webhook secret + setting ID to .env. dev_run.sh resets the DB, seeds
data, and starts an ngrok tunnel to update the webhook URL automatically
for end-to-end checkout testing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Quote wizard _accumulated hidden inputs used double-quote attribute delimiters
which broke on tojson output containing literal " characters — all step data was
lost by step 9. Admin dashboard crashed on credit_ledger queries referencing
wrong column names (amount/entry_type vs delta/event_type). Also: opaque nav bar,
pricing card button alignment, newsletter boost replaced with card color boost,
admin CRUD for suppliers/leads, dev seed data script, and playwright quote wizard
tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migrate all checkouts to Paddle.js overlay (no redirect), move Paddle price
IDs from env vars to DB table, add 4-tab supplier dashboard (overview, leads,
listing, boosts), business plan PDF export with WeasyPrint, enhanced supplier
landing page with live stats, admin supplier management + feedback widget.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Guest quote submissions now require email verification before the lead
goes live. The verification click also creates a user account and logs
them in. Logged-in users submitting with their own email skip verification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Nav: Zillow-style centered logo, solid blue Sign In button
- Planner: center app at 72rem, center wizard steps/header/preview
- Country pills: UK/USA labels, remove Other, show permits slider
inline under country so the effect is transparent and adjustable
- Reset button: inline confirm (red "Sure? Reset") instead of alert
- Worker: print magic link to console when DEBUG=true for local dev
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>