# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Added - i18n URL prefixes: all public-facing blueprints (`public`, `planner`, `directory`, `content`, `leads`, `suppliers`) now live under `//` (e.g. `/en/`, `/de/`); internal blueprints (`auth`, `dashboard`, `admin`, `billing`) unchanged; root `/` detects language from cookie / Accept-Language header and 301-redirects; legacy URLs (`/terms`, `/privacy`, etc.) redirect to `/en/` equivalents - German legal pages: full DSGVO-compliant `Datenschutzerklärung` (`/de/privacy`), `AGB` (`/de/terms`), and `Impressum` (`/de/imprint`) per § 5 DDG — populated with Hendrik Dreesmann's details, Kleinunternehmer § 19 UStG, Oldenburg address - Rewritten English legal pages (`/en/terms`, `/en/privacy`, `/en/imprint`) with expanded GDPR sections, correct controller identity, proper data-processing details (Umami self-hosted, Paddle, Resend with SCCs), and German-law jurisdiction - Language toggle (`EN | DE`) in footer; hreflang `en`, `de`, and `x-default` tags in `` on all lang-prefixed pages - `lang` cookie (1-year, SameSite=Lax) persisted on first visit; `lang` and `t` (translation dict) injected into every template context - `i18n.py`: flat translation dicts for ~20 nav/footer keys in `en` and `de`; `LANG_BLUEPRINTS` and `SUPPORTED_LANGS` constants - `sitemap.xml` and `robots.txt` moved to app-level root routes (not under `/`); sitemap now includes both language variants of every SEO page - Cookie consent banner: fixed bottom bar with "Accept all" and "Manage preferences" (toggle for functional/A/B cookies); consent stored in `cookie_consent` cookie for 1 year; "Manage Cookies" link added to footer Legal section ### Changed - Defer Paddle.js loading to only the 3 pages that use checkout (export, supplier signup, supplier dashboard) — removed from global `base.html` head; all other pages no longer receive Paddle's third-party cookies - Gate A/B test cookie (`ab_*`) on functional cookie consent: variant is still picked per-request for rendering, but the cookie is only persisted when the visitor has accepted functional cookies - Privacy policy section 6 (Cookies): full disclosure of all cookie categories (essential, functional, payment); fix "Plausible" → "Umami" in service providers list ### Changed - Auto-create Resend audiences per blueprint: `capture_waitlist_email()` now derives the audience name from `request.blueprints[0]` (e.g., `waitlist-auth`, `waitlist-suppliers`) and lazily creates audiences via the Resend API on first use, caching IDs in a new `resend_audiences` table; removes `RESEND_AUDIENCE_WAITLIST` env var — only `RESEND_API_KEY` needed ### Added - Waitlist mode (lean startup smoke test): `WAITLIST_MODE` config flag intercepts `/auth/signup`, `/suppliers/signup`, and `/planner/export` to capture emails or show "coming soon" messaging before Paddle billing is ready; confirmation emails sent via Resend; optional `RESEND_AUDIENCE_WAITLIST` for bulk launch blast; flip flag to `false` and all flows revert to normal ### Changed - Redesign padel racket SVG logo/favicon: filled dark silhouette with white punched-through holes (3×3 grid), proper throat tapering to handle at bottom, grip tape lines — replaces outline-only stroke approach; bump favicon cache to v4 ### Added - Simple A/B testing with `@ab_test` decorator and Umami `data-tag` integration - SEO defaults in `base.html`: canonical, og:url, og:type, og:image (logo fallback), og:title, og:description, twitter:card — every page gets these automatically, child templates override as needed - `robots.txt` route: disallows `/admin/`, `/auth/`, `/dashboard/`, `/billing/`, `/directory/results`; includes Sitemap reference - Meta descriptions and OG tags on all public pages: about, terms, privacy, pricing, features, suppliers, directory, supplier detail - `og:image` on landing and features pages pointing to planner screenshot - JSON-LD Organization schema on homepage and supplier detail pages - JSON-LD FAQPage schema on homepage (5 FAQ entries) - JSON-LD Article schema on article detail pages - Sitemap supplier slugs: all supplier detail pages now indexed - Sitemap `` on all entries (static pages: today, articles: `COALESCE(updated_at, published_at)`, suppliers: `created_at`) - `rel="preconnect"` for Google Fonts to reduce font-load latency - `X-Robots-Tag: noindex` header on `/directory/results` HTMX partial ### Fixed - Render-blocking Paddle.js: added `defer` attribute and wrapped `Paddle.Initialize` in `DOMContentLoaded` listener - Render-blocking Chart.js on planner page: added `defer` attribute - Broken `og:image` on planner page (`og-planner.png` → `planner-screenshot.png`) - Homepage meta description trimmed to under 155 characters - Duplicate `og:url` and `canonical` tags removed from landing, markets, and article detail pages ### Changed - Rewrote supplier page hero: pain-first headline ("Stop Chasing Cold Leads"), differentiator copy, micro-proof under CTA - Added "Problem With Finding Padel Clients Today" section with trade show / Google Ads / cold outreach pain points - Tightened "How It Works" step titles: "Claim Your Listing", "Browse Pre-Qualified Leads", "Win Projects Faster" - Added Basic tier (€39/mo / €29/mo yearly) to public pricing section on suppliers page - Pricing section now shows 3-column grid (Basic / Growth / Pro) with CSS-only billing period toggle (monthly / yearly, no JS) - Added "How We Compare" table: Padelnomics vs trade show / Google Ads / cold directory - Upgraded social proof section: two testimonial cards with decorative quote mark, stat bar in subtitle - Updated supplier FAQ: three tiers with correct prices (€39/€199/€499), two new entries (what makes leads different, pricing vs alternatives), removed stale €149/€399 figures - Strengthened final CTA: "Your Next Client Is Already Building a Business Plan" - Fixed stale prices in landing page FAQ: Basic €39/mo, Growth €199/mo, Pro €499/mo (was €149/€399) ### Changed - Redesigned directory cards: image-first 16:9 layout with cover photos, frosted category badge, CSS court/grid placeholders, and logo avatar straddling the media/body border - 4-tier visual ladder: Free (62% opacity, grey placeholder, unverified chip) → Basic (full opacity, green placeholder, verified chip, description, "View Listing →") → Growth (green placeholder, Growth chip + stats, "Request Quote →") → Pro (green 2.5px top border, CSS court media, full stats, green hover glow) - Pro card media defaults to CSS court visualization when no cover image; Growth/Basic default to dark-green grid placeholder ### Added - Cover image upload for suppliers in the dashboard listing form (saves to `static/uploads/covers/`, 16:9 thumbnail preview) - Migration 0013: `cover_image TEXT` column on suppliers table ### Added - **Basic subscription tier** — verified directory listing with contact info, services checklist, social links, and enquiry form; no lead credits - **Monthly + yearly billing** — all paid supplier tiers now offer yearly pricing with annual discount (Basic: €349/yr, Growth: €1,799/yr, Pro: €4,499/yr) - **Billing period toggle** in supplier signup wizard — monthly/yearly pill switch, defaults to yearly; price cards update in real time via CSS sibling selectors - **Redesigned supplier detail page** — navy hero section with court-grid CSS pattern, two-column body (main + 320px sidebar), contact card with avatar/role/social links, stats grid, services checklist, enquiry form for Basic+ listings, tier-adaptive CTA strip - **Supplier enquiry form** on Basic+ listing pages — HTMX-powered inline form, rate-limited at 5 per email per 24 h, email relayed to supplier via worker task - **New DB columns on suppliers**: `services_offered`, `contact_role`, `linkedin_url`, `instagram_url`, `youtube_url` - **`supplier_enquiries` table** for tracking incoming enquiries from listing pages - **Basic card variant** in directory results — shows verified badge, logo, website, short description; sits between growth and free in sort order - **Dashboard access for Basic tier** — overview + listing tabs; leads/boosts tabs hidden; upgrade CTA in sidebar - **Listing form: new fields** — services offered (multi-select checkboxes), contact role, LinkedIn/Instagram/YouTube social links - **Admin: Basic tier support** — tier dropdown updated, new fields in supplier form and detail view, enquiry count shown ### Changed - Supplier Growth monthly price adjusted to €199/mo (yearly: €150/mo billed at €1,799/yr) - Supplier Pro monthly price adjusted to €499/mo (yearly: €375/mo billed at €4,499/yr) - Directory sort order updated: pro → growth → basic → free - `_supplier_required` decorator now grants access to basic, growth, and pro tiers - New `_lead_tier_required` decorator restricts lead feed, unlock, and dashboard leads to growth/pro only ### Fixed - **Supplier detail page: locked quote CTA** — "Request Quote" button is now visually disabled (greyed-out) for unverified/free-tier suppliers; clicking opens an inline popover explaining the limitation and linking to the general quote wizard instead - **Supplier signup Paddle checkout** — form now intercepts submit, fetches checkout config via JS, and opens `Paddle.Checkout.open()` overlay instead of displaying raw JSON in the browser - **Credit balance OOB updates** — sidebar and lead feed header credits now update instantly via HTMX OOB swaps after unlocking a lead (no page refresh) - **Boosts page layout** — capped main content column at 720px on wide screens; credit card grid uses `auto-fill` for graceful responsive adaptation ### Added - **Listing preview live update** — form fields (name, tagline, description, website) trigger HTMX `hx-get` with 500ms debounce to update the directory card preview in real time; new `/dashboard/listing/preview` endpoint and extracted `dashboard_listing_preview.html` partial - **Lead cards: full qualification data** — unlocked cards now show decision process, prior supplier contact, financing help preference, with clear section headers (Project, Location & Timeline, Readiness, Contact) and human-readable enum labels; includes "View their plan" link to linked scenario - **Lead feed search bar** — text input with 300ms debounced HTMX filtering on country, facility type, and additional info; mirrors directory search pattern - **Phone number mandatory in quote form** — step 9 now requires phone with HTML `required` attribute and server-side validation - **Supplier-aware dashboard redirect** — `/dashboard/` checks if user has a claimed supplier with paid tier and redirects to supplier dashboard - **Inline SVG logo** — replaced PNG logo with inline SVG padel racket icon + Bricolage Grotesque wordmark in navbar and footer ### Changed - **Planner fonts** — replaced Inter with DM Sans for body text, JetBrains Mono with Commit Mono for numeric values, added Bricolage Grotesque for planner header and wizard step titles; loaded Commit Mono via fontsource CDN - **Migration system: single source of truth** — 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 `schema.sql`, `_is_fresh_db()`, and the fresh-DB fast-path that skipped migration execution; `migrate()` accepts an optional `db_path` parameter for direct use in tests; test fixtures use cached migration replay instead of loading `schema.sql` directly; removed fragile `_old_schema_sql()` test helper and `TestMigration0001` class; template repo updated to match (deleted `0001_roles_and_billing_customers.py`, projects own their migrations) - **Design system: Bricolage Grotesque + DM Sans** — replaced Inter with Bricolage Grotesque (display headings) and DM Sans (body text); added `--font-display` theme variable; headings use display font via `font-family: var(--font-display)` in base layer; added `--color-forest` (#064E3B) to theme palette - **Glass navbar** — replaced opaque white navbar with semi-transparent `backdrop-filter: blur(14px)` frosted glass effect - **Landing page: dark hero** — navy background with radial blue glow, white text, green badge on dark, white ROI calculator card with stronger shadow; hero section is now full-width outside the container - **Landing page: journey timeline** — replaced 5 left-border cards with numbered step track (01-05) with connecting line, active/upcoming states; CSS grid 5-col desktop, stacks to horizontal layout on mobile - **Landing page: dark CTA card** — replaced plain white CTA section with rounded navy card with noise texture and white inverted button - **Directory card tiers** — pro cards get stronger green left border + subtle box-shadow glow and 48px logo; featured badges more prominent with box-shadow; free/unclaimed cards more visibly muted (lower opacity, lighter border) - **Supplier dashboard sidebar icons** — added inline SVG icons (chart, inbox, building, rocket) to sidebar navigation links - **Supplier dashboard lead cards** — added heat-color left borders (red/amber/gray by heat score) on `.lf-card` ### Added - **Admin sidebar navigation** — new `base_admin.html` template with persistent sidebar (Overview, Leads, Suppliers, Users, Content, System sections); Heroicons inline SVGs for each nav item; active state via `{% set admin_page %}` in child templates; mobile: horizontal scroll nav; all 20 admin templates now extend `base_admin.html` - **Admin dashboard section labels** — stat card groups labeled "Lead Funnel" and "Supplier Funnel" with color-coded left borders (blue for leads, green for suppliers) ### Fixed - **Hardcoded Inter on supplier unlock button** — `.lf-unlock-btn` used `font-family: 'Inter'`; changed to `inherit` so it picks up DM Sans ### Changed - **Admin auth: password → RBAC** — replaced `ADMIN_PASSWORD` env var and session-based password login with role-based access control; admin access is now granted via `ADMIN_EMAILS` env var (comma-separated); on login/dev-login, matching emails auto-receive the `admin` role; removed `/admin/login` and `/admin/logout` routes, `admin_required` decorator, and `login.html` template; all admin routes now use `@role_required("admin")` from `auth/routes.py` - **Billing: separated billing identity from subscriptions** — new `billing_customers` table stores `provider_customer_id` (was on `subscriptions.paddle_customer_id`); subscriptions table renamed `paddle_subscription_id` → `provider_subscription_id` and dropped `UNIQUE` constraint on `user_id` (allows multiple subscriptions per user); `upsert_subscription` now finds existing rows by `provider_subscription_id` instead of `user_id`; webhook handler calls `upsert_billing_customer()` for all subscription events - **Eager-loaded user context** — `load_user()` now JOINs `billing_customers`, `user_roles`, and latest subscription in a single query; adds `g.subscription` and `is_admin` template context variable (replaces `session.get('is_admin')`) ### Added - **RBAC decorators** — `role_required(*roles)`, `subscription_required(plans, allowed)`, `grant_role()`, `revoke_role()`, `ensure_admin_role()` in `auth/routes.py` - **`user_roles` table** — stores user-role pairs with `UNIQUE(user_id, role)` - **`billing_customers` table** — stores provider customer ID per user - **`ADMIN_EMAILS` config** — parsed from comma-separated env var in `core.py` - **Migration 0011** — adds `user_roles` and `billing_customers` tables, migrates `paddle_customer_id` data, recreates subscriptions table with `provider_subscription_id` column and no `UNIQUE` on `user_id` ### Removed - `ADMIN_PASSWORD` env var and password-based admin authentication - `/admin/login` and `/admin/logout` routes - `admin/templates/admin/login.html` template - `admin_required` decorator (replaced by `role_required("admin")`) - `subscription_required` from `billing/routes.py` (replaced by version in `auth/routes.py` that reads from `g.subscription`) ### Fixed - **Webhook crash on null `custom_data`** — Paddle sends `"custom_data": null` on lifecycle events (e.g. `subscription.updated`); `.get("custom_data", {})` returns `None` when the key exists with a null value, causing `AttributeError` on the next `.get()` call; switched to `or {}` fallback; also guarded `subscription.activated` to skip when `user_id` is missing (was inserting `user_id=0` causing FK violation), and applied same `or {}` to `current_billing_period` - **Webhook signature verification uses SDK Verifier** — replaced manual HMAC implementation with `paddle_billing.Notifications.Verifier` via a lightweight request wrapper; same algorithm, fewer moving parts ### Added - **Credit system test suite** (`tests/test_credits.py` — 24 tests) — covers `get_balance`, `add_credits`, `spend_credits`, `compute_credit_cost`, `already_unlocked`, `unlock_lead`, `monthly_credit_refill`, `get_ledger`; tests run against real in-memory SQLite with no mocking - **Supplier webhook test suite** (`tests/test_supplier_webhooks.py` — 10 tests) — integration tests POSTing signed payloads to `/billing/webhook/paddle` and verifying DB state for credit pack purchases, sticky boosts, supplier subscription activation (growth/pro tiers, boost items, noop on missing supplier_id), and business plan PDF export ### Removed - **Dead test** `test_mobile_nav_no_overflow` from `test_visual.py` — evaluated JS but never asserted the result; always passed regardless of overflow state ### Fixed - **Supplier logo missing on public directory** — directory cards and supplier detail page only checked `logo_url` (external URL); now checks `logo_file` first (uploaded via dashboard), falling back to `logo_url` - **Admin forward history links to wrong page** — supplier name in lead forward history linked to `/directory/` (public index) instead of `/admin/suppliers/` (admin detail page) - **Dashboard lead country filter drops heat/timeline state** — hidden inputs used mismatched names (`heat_hidden`/`timeline_hidden` vs `heat`/`timeline`), silently resetting other filters when country changed - **Directory pagination drops region filter** — paginating after filtering by region lost the `region` parameter; added to all pagination link templates - **`boost_card_color` missing from webhook handler** — `BOOST_PRICE_KEYS` omitted `boost_card_color`, so purchasing the card color boost via Paddle would silently fail to create the `supplier_boosts` record; also removed stale `boost_newsletter` entry (replaced by card color boost) - **Sticky boost DB operations not atomic** — `boost_sticky_week` and `boost_sticky_month` handlers issued two bare `execute()` calls (INSERT + UPDATE) without a transaction; wrapped in `db_transaction()` to prevent partial writes ### Added - **Clickable admin list rows** — entire row is clickable on leads, suppliers, and users admin list pages (`data-href` + JS click handler that skips links/buttons/forms); pointer cursor and hover highlight via CSS - **Supplier impersonation** — "Impersonate Owner" button on admin supplier detail page; reuses existing `admin.impersonate` route, shown only when the supplier has a `claimed_by` user - **Seed data: supplier owner accounts** — each claimed supplier now gets its own user + active subscription for realistic impersonation testing ### Fixed - **Insufficient credits UX** — unlock error now shows credit balance, cost, and a "Buy Credits" CTA linking to Boost & Upsells tab (was a collapsed red error message with no action) - **Supplier dashboard sidenav** — active tab highlight now updates on click (was only correct on initial page load) - **Boost/credit buy buttons used wrong price IDs** — dashboard boosts tab passed internal keys (`boost_logo`, `credits_25`) as Paddle `priceId` instead of resolved Paddle price IDs; buttons now show "Not configured" when Paddle products aren't set up - **Listing preview card stretched full width** — constrained to 420px to match actual directory card proportions - **Impersonate redirects to supplier dashboard** — impersonating a user who owns a supplier now lands on `/suppliers/dashboard` instead of the generic demand-side dashboard ### Added — Programmatic SEO: Content Generation Engine - **Database migration 0010** — `published_scenarios`, `article_templates`, `template_data`, `articles` tables with FTS5 full-text search on articles; content-sync triggers for INSERT/UPDATE/DELETE - **Article template system** — parameterized content recipes with `input_schema` (JSON field definitions), `url_pattern`, `title_pattern`, `body_template` (Markdown + Jinja2); supports multiple content types (only `calculator` built now) - **Template data rows** — per-city/region input data that feeds the generation pipeline; manual add or bulk CSV upload - **Bulk generation pipeline** — `POST /admin/templates//generate` computes financial scenarios from data rows via `validate_state()` + `calc()`, renders Markdown with baked scenario cards, writes static HTML to `data/content/_build/`, creates articles with staggered publish dates - **Published scenarios** — standalone financial widgets with pre-computed `calc_json`; embeddable in articles via `[scenario:slug]` markers with section variants (`:capex`, `:operating`, `:cashflow`, `:returns`, `:full`) - **Scenario widget templates** — 6 partial templates (summary, CAPEX breakdown, revenue & OPEX, 5-year projection, returns & financing, full combined) with dark navy header bar, Commit Mono numbers, responsive metric grids, and "Try with your own numbers" CTA - **Static file rendering** — articles rendered to `data/content/_build/` as pre-baked HTML; DB stores metadata only, no body content in DB - **Content blueprint** — catch-all route serves published articles by `url_path`; registered last to avoid path collisions - **Markets hub** (`/markets`) — public search + country/region filter page for discovering articles; HTMX live filtering via FTS5 queries - **Admin template CRUD** — list, create, edit, delete article templates; view/add/upload/delete data rows; bulk generation form with start date and articles-per-day controls - **Admin scenario CRUD** — list, create (with curated calculator form), edit + recalculate, delete, preview all widget sections - **Admin article management** — list with status badges (draft/published/ scheduled), create manual articles (Markdown textarea), edit, delete, publish/unpublish toggle, rebuild single or all articles - **Rebuild system** — `POST /admin/articles//rebuild` and `/admin/rebuild-all` re-render article HTML from source (template+data or markdown file) with fresh scenario `calc_json` - **Article detail page** — SEO meta tags (description, og:title, og:description, og:type=article, og:image, canonical), article typography styles, bottom CTA linking to planner - **Scenario widget CSS** — `.scenario-widget` component styles in `input.css` with responsive table scroll and metric grid collapse; `.article-body` typography for headings, paragraphs, lists, blockquotes, tables, code blocks - **`slugify()` utility** — added to `core.py` for URL-safe slug generation - **`mistune` dependency** — Markdown → HTML rendering for articles - **Sitemap** — `/markets` and all published articles added to `/sitemap.xml` (only articles with `published_at <= now`) - **Footer** — "Markets" link added to Product column - **Admin dashboard** — Templates, Scenarios, Articles quick-link buttons - **Path collision prevention** — `RESERVED_PREFIXES` validation rejects article `url_path` values that conflict with existing routes ### Added - **Dev setup script** (`scripts/dev_setup.sh`) — interactive bootstrap that checks prerequisites, installs deps, creates `.env` with auto-generated `SECRET_KEY`, runs migrations, seeds test data, optionally sets up Paddle sandbox products, and builds CSS - **Dev run script** (`scripts/dev_run.sh`) — resets DB, runs migrations, seeds data, builds CSS, then starts app + worker + CSS watcher in parallel with colored/labeled output and clean Ctrl-C shutdown; when Paddle is configured and ngrok is installed, starts a tunnel and updates the Paddle webhook destination automatically for end-to-end checkout testing - **Paddle webhook auto-setup** — `setup_paddle.py` now creates a notification destination in Paddle with the 5 event types the webhook handler processes, and writes `PADDLE_WEBHOOK_SECRET` to `.env` automatically - **Resend test email docs** — documented Resend test addresses (`delivered@resend.dev`, `bounced@resend.dev`, etc.) in `.env.example` and README for testing email flows without a verified domain ### Fixed - **Webhook signature verification broken** — `Verifier().verify()` was called with raw bytes instead of a request object, causing all signed webhooks to fail with 400; replaced with manual HMAC verification matching Paddle's `ts=;h1=` format; also added JSON parse error guard (400 instead of 500 on malformed payloads) - **Billing tests stale after SDK migration** — webhook tests used plain HMAC instead of Paddle's `ts=...;h1=...` signature format; checkout tests expected redirect instead of JSON overlay response; manage/cancel tests mocked httpx instead of Paddle SDK; removed stale `PADDLE_PRICES` config test (prices now in DB) - **Quote wizard state loss** — `_accumulated` hidden input used `"` attribute delimiters which broke on `tojson` output containing literal `"` characters; switched all 8 step templates to single-quote delimiters (`value='...'`) - **Admin dashboard crash** — `no such column: amount` in credit ledger query; corrected to `delta` (the actual column name) - **Admin supplier detail** — credit ledger table referenced `entry.entry_type` and `entry.amount` instead of `entry.event_type` and `entry.delta` - **Nav bar semi-transparent** — replaced `rgba(255,255,255,0.85)` + `backdrop-filter: blur()` with opaque `#ffffff` background - **Supplier pricing card buttons misaligned** — added flexbox to `.pricing-card` with `flex-grow: 1` on feature list and `margin-top: auto` on buttons ### Changed - **Newsletter boost removed** — replaced with "Custom Card Color" boost (€19/mo) across supplier pricing page, `setup_paddle.py`, and supplier dashboard; directory cards with active `card_color` boost render a custom border color from `supplier_boosts.metadata` JSON - Removed "Zillow-style" comments from `base.html` and `input.css` ### Added - **Migration 0009** — adds `metadata TEXT` column to `supplier_boosts` for card color boost configuration - **Admin supplier/lead CRUD** — `GET/POST /admin/suppliers/new` and `GET/POST /admin/leads/new` routes with form templates; "New Supplier" and "New Lead" buttons on admin list pages - **Dev seed data script** (`scripts/seed_dev_data.py`) — creates 5 suppliers (mix of tiers), 10 leads (mix of heat scores), 1 dev user, credit ledger entries, and lead forwards for local testing - **Playwright quote wizard tests** (`tests/test_quote_wizard.py`) — full 9-step flow, back-navigation data preservation, and validation error tests ### Added — Phase 2: Scale the Marketplace — Supplier Dashboard + Business Plan PDF - **Paddle.js overlay checkout** — migrated all checkout flows (billing, supplier signup, business plan) from server-side Paddle transaction creation + redirect to client-side `Paddle.Checkout.open()` overlay; `PADDLE_CLIENT_TOKEN` config; Paddle.js script in `base.html` with sandbox/production toggle - **Paddle products in DB** — new `paddle_products` table replaces 16 `PADDLE_PRICE_*` env vars; `get_paddle_price(key)` and `get_all_paddle_prices()` async helpers in `core.py`; `setup_paddle.py` rewritten to write product/price IDs directly to database - **Migration 0008** — `paddle_products`, `business_plan_exports`, `feedback` tables; `logo_file` and `tagline` columns on `suppliers` - **Umami analytics** — tracking script in `base.html`; config vars `UMAMI_API_URL`, `UMAMI_API_TOKEN`, `UMAMI_WEBSITE_ID`; directory click tracking redirect routes (`//website`, `//quote`) - **Supplier dashboard** (`/suppliers/dashboard`) — tab-based HTMX dashboard with sidebar nav (Overview, Lead Feed, My Listing, Boost & Upsells); each tab loads via `hx-get` with `hx-push-url` for deep-linking - **Overview tab** — 4 KPI stat cards (profile views, leads unlocked, credit balance, directory rank), new leads alert banner, recent activity feed - **Lead feed tab** — refactored `_get_lead_feed_data()` shared function with bidder count; heat/country/timeline filter pills; region matching badges; "No other suppliers yet — be first!" messaging - **My Listing tab** — preview card + inline edit form (company info, categories, service area, logo upload); `POST /suppliers/dashboard/listing` saves changes - **Boost & Upsells tab** — current plan, active boosts, available boosts with `Paddle.Checkout.open()` purchase buttons, credit packs grid, summary sidebar with visibility multiplier - **Business plan PDF export** — `businessplan.py` with WeasyPrint PDF engine; `plan.html` + `plan.css` A4 templates (executive summary, CAPEX, OPEX, revenue model, 5-year P&L, 12-month cash flow, sensitivity, key metrics); bilingual EN/DE; `generate_business_plan` worker task - **Business plan routes** — `GET /planner/export` (options page with scenario picker + Paddle checkout), `POST /planner/export/checkout`, `GET /planner/export/success`, `GET /planner/export/` (download); export CTA in planner sidebar - **Supplier landing page enhanced** — live stats from DB (business plans created, avg project value, suppliers listed, monthly leads); real anonymized lead preview cards (fallback to example data); credit explainer (hot=35, warm=20, cool=8); "Most Popular" badge on Growth plan; expanded FAQ (8 questions including credits, countries, cancellation); social proof section - **Admin supplier management** — `GET /admin/suppliers` with tier/country/name filters (HTMX search), `GET /admin/suppliers/` detail with profile info, credit balance + ledger, active boosts, lead forward history; `POST /admin/suppliers//credits` manual credit adjustment; `POST /admin/suppliers//tier` manual tier change; supplier stats on admin dashboard (claimed, growth, pro, credits spent, leads forwarded) - **Feedback widget** — compact "Feedback" button in navbar opens HTMX popover with textarea; `POST /feedback` rate-limited (5/hr per IP), inserts into `feedback` table; `GET /admin/feedback` paginated admin view with user email + page URL ### Added — Phase 1: Lead Operations + Builder Directory Monetization - **SDK migration** — replaced raw httpx calls with official `paddle-python-sdk` and `resend` SDKs for type safety, built-in webhook verification, and cleaner code; `send_email()` now accepts `from_addr` parameter; all Paddle API calls use SDK client - **EMAIL_ADDRESSES dict** — hardcoded `transactional`, `leads`, and `nurture` from-addresses sharing the `notification.padelnomics.io` Resend domain - **Paddle product setup script** — `scripts/setup_paddle.py` creates all 14 products/prices programmatically via SDK; outputs `.env` snippet for CI - **Claude Code skills** — `.claude/skills/paddle-integration/SKILL.md` and `.claude/skills/resend-emails/SKILL.md` for consistent SDK usage patterns - **Migration 0007** — `credit_ledger`, `lead_forwards`, `supplier_boosts` tables; 12 new columns on `suppliers` (profile, credits); `credit_cost` and `unlock_count` on `lead_requests` - **Credit system** (`credits.py`) — `get_balance`, `add_credits`, `spend_credits`, `unlock_lead`, `compute_credit_cost`, `monthly_credit_refill`, `get_ledger`; `InsufficientCredits` exception; heat-based pricing (hot=35, warm=20, cool=8 credits); `refill_monthly_credits` worker task + scheduler - **Admin lead management** — `GET /admin/leads` with status/heat/country filters (HTMX search), `GET /admin/leads/` detail with project brief + forward history, `POST /admin/leads//status` update, `POST /admin/leads//forward` manual forward (no credit cost); lead funnel stats on admin dashboard (planner users → leads → verified → unlocked) - **Lead forwarding emails** — `send_lead_forward_email` worker task sends full project brief + contact details to supplier; `send_lead_matched_notification` notifies entrepreneur when a supplier unlocks their lead - **Credit cost computed on submission** — `credit_cost` set from heat score both on verified-user submission and on email verification - **Supplier signup wizard** (`/suppliers/signup`) — 4-step HTMX wizard: plan selection (Growth €149/mo, Pro €399/mo), boost add-ons (logo, highlight, verified, newsletter), credit packs (25-250), account details + order summary; builds multi-item Paddle transaction; `_accumulated` hidden JSON pattern - **Supplier claim flow** — `GET /suppliers/claim/` verifies unclaimed and redirects to signup with pre-fill - **Webhook handlers** — `subscription.activated` with `supplier_*` plan creates supplier record with tier, credits, and boosts; `transaction.completed` handles credit pack purchases and sticky boost purchases with expiry - **Supplier profile page** (`/directory/`) — public profile with logo, verified badge, description, service categories as pills, service area, years in business, project count, website; "Request Quote" and "Claim This Listing" CTAs - **Directory card links** — all directory cards now link to supplier profile pages; paid-tier cards show "Request Quote" mini-CTA - **Supplier lead feed** (`/suppliers/leads`) — requires login + paid supplier tier; shows anonymized lead cards with heat badge, facility type, courts, country, timeline, budget range, credit cost, unlock count; `POST /suppliers/leads//unlock` spends credits, creates lead_forward, sends emails, returns full-details card via HTMX swap - **Email nurture via Resend Audiences** — on first scenario save, user is added to "Planner Users" audience (triggers 3-email automation); on quote submission, user is removed from audience (stops nurture) - **PADDLE_PRICES expanded** — 13 new price keys for supplier plans, boosts, credit packs; `PADDLE_ENVIRONMENT` config for sandbox/production switching ### Changed - Supplier marketing page CTAs link to `/suppliers/signup` instead of mailto - `httpx` removed from direct dependencies (transitive via paddle SDK) ### Added - **Double opt-in email verification for quote requests** — guest quote submissions now require email verification before the lead goes live; verification click also creates a user account and logs them in automatically (GDPR-friendly consent trail) - `GET /leads/verify` route — validates token, activates lead (`pending_verification` → `new`, sets `verified_at`), logs user in, sends admin notification and welcome email - `send_quote_verification` worker task — branded verification email with project details and "Verify & Activate Quote" CTA button (DEBUG mode prints link to console) - `quote_verify_sent.html` template — "Check your email" page shown after guest quote submission - Migration 0006 — adds `verified_at TEXT` column to `lead_requests` - 9 new tests in `TestQuoteVerification` class covering the full verification flow, expired tokens, duplicate verification, and user creation ### Changed - **Inline CTA full copy** — mobile/narrow-screen inline quote CTA now matches sidebar: "Next Step" label, full title, description, 4 checkmark benefits, "Get Supplier Quotes" button, and "Takes ~2 minutes" hint - **Signup bar simplified** — removed `×` close button from guest signup bar; now a non-dismissable nudge (still only shown on results tabs via JS) - **Investment tab narrower** — CAPEX tab content constrained to 800px max-width so 3-column card grid, table, and chart don't stretch across full 1100px on wide screens ### Changed - **Quote form → standalone 9-step HTMX wizard** — extracted "Get Quotes" from planner Step 5 into a standalone multi-step wizard at `/leads/quote` using server-rendered HTMX partials; each step validates server-side and swaps via `hx-post`/`hx-get` with OOB dot progress updates; accumulated state passed forward as hidden JSON field (no JS state management) - **Planner reduced to 4 steps** — removed embedded quote form (Step 5) from planner wizard; Step 4 "Get Quotes →" now navigates to `/leads/quote` with pre-filled params (venue, courts, glass, lighting, country, budget) - **Planner sidebar CTA** — "Get Supplier Quotes" button now links to standalone quote wizard instead of scrolling to embedded Step 5; sidebar now visible on all tabs including assumptions (was results-only) - **Sticky wizard nav** — planner preview bar (CAPEX/CF/IRR) and back/next buttons now stick to the bottom of the viewport so users don't have to scroll to navigate between steps - **Mobile quote CTA** — inline "Get Quotes" card shown below main content on screens narrower than 1400px (where the fixed sidebar is hidden) - **Step 4 → "Show Results"** — final planner wizard step now says "Show Results" instead of "Get Quotes" since quote flow is a separate standalone wizard - **Removed "2-5 suppliers" cap language** — replaced specific supplier count promises with "matched suppliers" across landing page, supplier FAQ, planner sidebar, and quote form privacy box ### Removed - Inline quote form from planner (Step 5 HTML, `#wizSuccess`, hidden inputs) - `populateWizAutoFill()`, `submitQuote()`, `COUNTRY_NAMES` from planner.js - `__PADELNOMICS_QUOTE_URL__` JS variable from planner template - Step 5 scoped CSS (~155 lines): `#wizQuoteForm`, `.wiz-autofill-summary`, `.wiz-input`, `.wiz-privacy-box`, `.consent-group`, `.wiz-success`, `.wiz-signup-nudge`, `.wiz-checkbox-label` ### Added - **Supplier tier system** — Migration 0005 adds `tier` (free/growth/pro), `logo_url`, `is_verified`, `highlight`, `sticky_until`, `sticky_country` columns to suppliers table for paid listing support - **HTMX live search** — directory search input and filters update results via `hx-get` with 300ms debounce; new `/directory/results` endpoint returns swappable partial - **Directory card tiers** — three-tier card design: Pro (green border, logo, verified badge, website), Growth (description, blue badge), Free (muted, unverified, "Is this your company?" CTA); sticky/featured suppliers pinned to top with blue border - **Supplier pricing page** — `/suppliers/` now shows Growth (€149/mo) and Pro (€399/mo) plan cards with feature lists, boost add-ons grid (Logo, Highlight, Verified Badge, Sticky Top, Newsletter Feature), updated FAQ - **Mandatory form fields** — country, timeline, and stakeholder_type now required on quote request form with server-side 422 validation - **Validation test** — `test_quote_validation_rejects_missing_fields` verifies server returns 422 JSON errors for missing mandatory fields ### Changed - **Nav redesign** — Zillow-style sticky nav with backdrop-blur: demand-side links (Planner, Directory) left, supply-side (For Suppliers, Help) after separator, Sign In right; removed "Get Started Free" button - **CTA text sweep** — "Get Matched" → "Get Quotes" across planner, landing, and lead forms; removed all "Free" qualifiers from CTAs and badges - **ROI calculator fix** — realistic cost model: €35K/court (was €25K), staff costs, €8/sqm rent (was €4); payback and ROI now based on total investment (was equity only); defaults: €40/hr rate, 35% utilization; shows ~3.9yr payback, ~26% ROI (was 0.1yr/1255%) - **Directory route refactor** — shared `_build_directory_query()` helper with tier-based SQL ordering (sticky → pro → growth → free → alphabetical) ### Added - **Supplier directory** — public searchable directory at `/directory/` with 279 padel court suppliers across 31 countries; FTS5 full-text search, country and category filters, pagination, category-colored badges, unclaimed listing model - **Supplier landing page** — `/suppliers/` marketing page for suppliers: hero, how-it-works steps, example lead preview, FAQ, "Claim Your Listing" CTAs - **Migration 0004** — creates `suppliers` table with FTS5 virtual table, content-sync triggers, and seeds 279 suppliers from PadelDirectory.md - **Quick ROI calculator** — landing page now features an interactive 3-slider calculator (courts, rate, utilization) showing investment, monthly cash flow, payback period, and annual ROI in real time - **Supplier matching section** — landing page "Find the Right Suppliers" section with 3-step flow and link to directory - **FAQ accordion** — landing page FAQ covering planner features, signup requirements, supplier matching, directory pricing, and projection accuracy ### Changed - **Visual refresh** — adopted React prototype color palette and aesthetic site-wide: royal blue primary (#1D4ED8), green (#16A34A), gold (#D97706); elevated cards with soft shadows, rounder corners (rounded-2xl cards, rounded-xl buttons/inputs), frosted-glass planner nav, highlighted CTA regions with blue-tinted backgrounds, pill-shaped toggle/filter controls, polished buttons with colored shadows, stronger hover lift on directory cards - **Landing hero redesigned** — two-column layout with headline + CTAs on left and interactive Quick ROI calculator on right (matching React prototype); green badge pill, feature check bullets, "Open Full Planner" CTA inside calculator card; responsive single-column on mobile - **Landing page redesigned** — replaced screenshot card with Quick ROI calculator; added supplier matching section, FAQ, and live supplier stats; CTAs renamed "Open the Planner — Free"; Build journey card updated with live supplier/country counts - **Navbar** — Planner and Directory links now visible for all users (not just logged-in); footer updated with Directory and For Suppliers links - **Planner CTAs** — removed sticky "Get Builder Quotes" footer bar; CAPEX and Returns tab CTAs now navigate to wizard step 5 (integrated lead qualifier) instead of redirecting to standalone `/leads/quote` form - **Sitemap** — added `/planner/`, `/directory/`, and `/suppliers/` URLs ### Changed - **Planner wizard** — Assumptions tab reorganized into 5 guided steps (Your Venue → Pricing → Costs → Finance → Get Matched) with live preview bar and step navigation; reduces cognitive load from 60 sliders to ~6-15 per step - **Integrated lead qualifier** — Step 5 "Get Matched" embeds the supplier quote form directly in the planner; auto-fills venue, courts, glass, lighting, country, budget from planner state; submits inline via fetch - **JSON quote endpoint** — `POST /leads/quote` now accepts `application/json` and returns `{"ok": true, "heat": "..."}` for inline planner submissions; standalone HTML form unchanged ### Added — Phase 0 Round 2: Polish & Country-Specific Calculator - **Country-specific calculator** — `country` selector (DE/ES/IT/FR/NL/SE/UK/Other) and `permitsCompliance` CAPEX item for Indoor Rent and Outdoor scenarios; country presets auto-adjust permit costs - **Permits & Compliance** — new CAPEX line item for building permits, noise studies, and regulatory compliance (default €12K for Germany); excluded from Indoor Buy where Planning + Permits already covers this - **Quote form redesign** — elevated white card on gradient background, green gradient CTA buttons, progress labels (Project/Details/Contact), privacy info box, mandatory consent checkbox - **Project phase** (replaces location_status options) — 7-stage progression: still searching → location found → converting existing → lease signed → permit not filed → permit pending → permit granted; updated heat scoring - **Stakeholder type** field — "You are..." selector (Entrepreneur, Tennis Club, Municipality, Developer, Operator, Architect) with `stakeholder_type` DB column (migration 0003) - **Build context** — added "Need Help Finding a Venue / Land" (`venue_search`) option - **Quote submitted page redesign** — "You're matched!" flow with next-steps timeline, email confirmation box, and signup CTA for guests - **Migration 0003** — adds `stakeholder_type TEXT` column to `lead_requests` ### Changed - **Landing page** — replaced teaser calculator with planner screenshot in browser-frame card + "Start Planning — Free" CTA; all CTAs now point to `/planner/` (no signup gate) - **Heat score** — updated `calculate_heat_score()` for new project phase values (`permit_granted` +4, `lease_signed`/`permit_pending` +3, `converting_existing`/`permit_not_filed` +2, `location_found` +1) - **Quote URL** — planner now passes `country` parameter to quote form prefill - **Admin email** — includes stakeholder type and updated field labels ### Added — Phase 0: Ungate & Validate - **Guest mode planner** — removed auth gate from `/planner/` and `/planner/calculate`; scenarios still require login - **New calculator variables** — `budgetTarget` (budget vs CAPEX comparison), `glassType` (standard/panoramic, 1.4x multiplier), `lightingType` (LED standard/competition/natural, 1.5x/0x multipliers) - **Pill select UI component** — reusable `pillSelect()` helper in planner.js with matching `.pill-btn` CSS for multi-option inputs - **Budget indicator card** — shows over/under budget with variance amount and percentage on the Investment tab - **3-step "Get Builder Quotes" flow** — `/leads/quote` with project specs, details, and contact steps; no login required - **Lead heat scoring** — `calculate_heat_score()` rates leads as hot/warm/cool based on timeline, financing, location readiness, and budget signals - **PDF export CTA** — "Export Business Plan (PDF) — €99" wired to Paddle checkout (`business_plan` price in PADDLE_PRICES) - **SEO meta tags** — `` description, og:title, og:description, og:image on planner page - **Migration 0002** — expands `lead_requests` with 17 new columns for quote qualification flow; makes `user_id` nullable for guest leads - **Phase 0 test suite** (`tests/test_phase0.py`) — 47 tests covering guest mode, glass/lighting/budget variables, heat scoring, quote submission, schema validation - Updated Hypothesis strategy in `test_calculator.py` with `budgetTarget`, `glassType`, `lightingType` ### Changed - Planner CTA links now point to `/leads/quote` with pre-filled calculator state params (venue, courts, glass, lighting, budget) - Sticky footer bar updated: "Get Builder Quotes" + "Export Business Plan (PDF)" replace old supplier/financing links ### Changed - Landing page journey section: renamed "From Idea to Operating Hall" → "Your Journey", expanded from 4 cards to 5 (Explore → Plan → Finance → Build → Grow) with "Coming Soon" badges on unreleased stages - Added `.grid-5` CSS helper for 5-column grid layout ### Changed - **Pico CSS → Tailwind CSS v4** — full design system migration across all templates (except planner, which keeps its own CSS) - Standalone Tailwind CLI binary (no Node.js) with `make css-build` / `make css-watch` - Court Tech brand theme: navy/charcoal/electric/accent color palette - Component classes (`.btn`, `.card`, `.form-input`, `.table`, `.badge`, `.flash`, etc.) in `input.css` for consistent styling - Self-hosted Commit Mono font (replaces JetBrains Mono) for monospace data display - Docker multi-stage build: CSS compiled in dedicated stage before Python build - Landing page teaser calculator restyled with Tailwind utilities and brand colors ### Removed - Pico CSS CDN dependency - `custom.css` (replaced by Tailwind `input.css` with `@layer components`) - JetBrains Mono font (replaced by self-hosted Commit Mono) ### Fixed - Empty env vars (e.g. `SECRET_KEY=`) now fall back to defaults instead of silently using `""` — fixes 500 on every request when `.env` has blank values ### Added - Comprehensive migration test suite (`tests/test_migrations.py` — 20 tests) covering fresh DB, existing DB, up-to-date DB, idempotent migration, version discovery, `_is_fresh_db`, migration 0001 correctness, and ordering - Expanded `migrate.py` module docstring documenting the 8-step algorithm, protocol for adding migrations, and design decisions - Sequential migration system (`migrations/migrate.py`) — tracks applied versions in `_migrations` table, auto-detects fresh vs existing DBs, runs pending migrations in order - `migrations/versions/0001_rename_ls_to_paddle.py` — first versioned migration (absorbed from `scripts/migrate_to_paddle.py`) - Server-side financial calculator (`planner/calculator.py`) — ported JS `calc()`, `pmt()`, `calcIRR()` to Python so the full financial model is no longer exposed in client-side JavaScript - `POST /planner/calculate` endpoint for server-side computation - Pre-computed initial data (`window.__PADELNOMICS_INITIAL_D__`) injected on page load for instant first render - Debounced API fetch pattern in `planner.js` with `AbortController` for in-flight request cancellation - Computing indicator CSS (`.planner-app--computing`) with subtle "computing..." text - Comprehensive test suite for calculator (`tests/test_calculator.py` — 227 tests) covering all 4 venue/ownership combos, edge cases, and Hypothesis property-based fuzzing - Comprehensive billing test suite (371 tests total): - `tests/conftest.py` — shared fixtures (DB, app, clients, subscriptions, webhook helpers) - `tests/test_billing_helpers.py` — unit tests for SQL helpers, feature/limit access, plan determination (60+ tests + parameterized + Hypothesis) - `tests/test_billing_webhooks.py` — integration tests for LemonSqueezy webhooks (signature verification, all lifecycle events, Hypothesis fuzzing) - `tests/test_billing_routes.py` — route tests (pricing, checkout, manage, cancel, resume, subscription_required decorator) - Added `hypothesis>=6.100.0` and `respx>=0.22.0` to dev dependencies for property-based testing and httpx mocking - **Factored into Copier template** — all billing tests now generate as `.jinja` templates with provider-specific conditionals for Stripe, Paddle, and LemonSqueezy - GitLab CI/CD pipeline (`.gitlab-ci.yml`) — runs pytest + ruff on master/MRs, auto-deploys on master - Blue-green deployment with Docker Compose profiles (`docker-compose.prod.yml`, `deploy.sh`) - nginx router on port 5000 proxies to active blue/green slot - Zero-downtime: new slot health-checked before traffic switch - Automatic rollback on failed health check ### Removed - `scripts/migrate_to_paddle.py` — superseded by `versions/0001_rename_ls_to_paddle.py` ### Changed - `planner.js` no longer contains `calc()`, `pmt()`, or `calcIRR()` functions — computation moved server-side - `render()` split into `render()` (tab switching + schedule calc) and `renderWith(d)` (DOM updates from data) - Tab switching now renders from `_lastD` cache (instant, no API call) - Slider input triggers 200ms debounced server call instead of synchronous client-side calc