- 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>
44 KiB
44 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
[Unreleased]
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_enquiriestable 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_requireddecorator now grants access to basic, growth, and pro tiers- New
_lead_tier_requireddecorator 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-fillfor graceful responsive adaptation
Added
- Listing preview live update — form fields (name, tagline, description,
website) trigger HTMX
hx-getwith 500ms debounce to update the directory card preview in real time; new/dashboard/listing/previewendpoint and extracteddashboard_listing_preview.htmlpartial - 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
requiredattribute 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 from0000_initial_schema.py; removedschema.sql,_is_fresh_db(), and the fresh-DB fast-path that skipped migration execution;migrate()accepts an optionaldb_pathparameter for direct use in tests; test fixtures use cached migration replay instead of loadingschema.sqldirectly; removed fragile_old_schema_sql()test helper andTestMigration0001class; template repo updated to match (deleted0001_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-displaytheme variable; headings use display font viafont-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.htmltemplate 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 extendbase_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-btnusedfont-family: 'Inter'; changed toinheritso it picks up DM Sans
Changed
- Admin auth: password → RBAC — replaced
ADMIN_PASSWORDenv var and session-based password login with role-based access control; admin access is now granted viaADMIN_EMAILSenv var (comma-separated); on login/dev-login, matching emails auto-receive theadminrole; removed/admin/loginand/admin/logoutroutes,admin_requireddecorator, andlogin.htmltemplate; all admin routes now use@role_required("admin")fromauth/routes.py - Billing: separated billing identity from subscriptions — new
billing_customerstable storesprovider_customer_id(was onsubscriptions.paddle_customer_id); subscriptions table renamedpaddle_subscription_id→provider_subscription_idand droppedUNIQUEconstraint onuser_id(allows multiple subscriptions per user);upsert_subscriptionnow finds existing rows byprovider_subscription_idinstead ofuser_id; webhook handler callsupsert_billing_customer()for all subscription events - Eager-loaded user context —
load_user()now JOINsbilling_customers,user_roles, and latest subscription in a single query; addsg.subscriptionandis_admintemplate context variable (replacessession.get('is_admin'))
Added
- RBAC decorators —
role_required(*roles),subscription_required(plans, allowed),grant_role(),revoke_role(),ensure_admin_role()inauth/routes.py user_rolestable — stores user-role pairs withUNIQUE(user_id, role)billing_customerstable — stores provider customer ID per userADMIN_EMAILSconfig — parsed from comma-separated env var incore.py- Migration 0011 — adds
user_rolesandbilling_customerstables, migratespaddle_customer_iddata, recreates subscriptions table withprovider_subscription_idcolumn and noUNIQUEonuser_id
Removed
ADMIN_PASSWORDenv var and password-based admin authentication/admin/loginand/admin/logoutroutesadmin/templates/admin/login.htmltemplateadmin_requireddecorator (replaced byrole_required("admin"))subscription_requiredfrombilling/routes.py(replaced by version inauth/routes.pythat reads fromg.subscription)
Fixed
- Webhook crash on null
custom_data— Paddle sends"custom_data": nullon lifecycle events (e.g.subscription.updated);.get("custom_data", {})returnsNonewhen the key exists with a null value, causingAttributeErroron the next.get()call; switched toor {}fallback; also guardedsubscription.activatedto skip whenuser_idis missing (was insertinguser_id=0causing FK violation), and applied sameor {}tocurrent_billing_period - Webhook signature verification uses SDK Verifier — replaced manual HMAC
implementation with
paddle_billing.Notifications.Verifiervia a lightweight request wrapper; same algorithm, fewer moving parts
Added
- Credit system test suite (
tests/test_credits.py— 24 tests) — coversget_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/paddleand 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_overflowfromtest_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 checkslogo_filefirst (uploaded via dashboard), falling back tologo_url - Admin forward history links to wrong page — supplier name in lead forward
history linked to
/directory/(public index) instead of/admin/suppliers/<id>(admin detail page) - Dashboard lead country filter drops heat/timeline state — hidden inputs
used mismatched names (
heat_hidden/timeline_hiddenvsheat/timeline), silently resetting other filters when country changed - Directory pagination drops region filter — paginating after filtering by
region lost the
regionparameter; added to all pagination link templates boost_card_colormissing from webhook handler —BOOST_PRICE_KEYSomittedboost_card_color, so purchasing the card color boost via Paddle would silently fail to create thesupplier_boostsrecord; also removed staleboost_newsletterentry (replaced by card color boost)- Sticky boost DB operations not atomic —
boost_sticky_weekandboost_sticky_monthhandlers issued two bareexecute()calls (INSERT + UPDATE) without a transaction; wrapped indb_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.impersonateroute, shown only when the supplier has aclaimed_byuser - 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 PaddlepriceIdinstead 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/dashboardinstead of the generic demand-side dashboard
Added — Programmatic SEO: Content Generation Engine
- Database migration 0010 —
published_scenarios,article_templates,template_data,articlestables 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 (onlycalculatorbuilt 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/<id>/generatecomputes financial scenarios from data rows viavalidate_state()+calc(), renders Markdown with baked scenario cards, writes static HTML todata/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/<id>/rebuildand/admin/rebuild-allre-render article HTML from source (template+data or markdown file) with fresh scenariocalc_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-widgetcomponent styles ininput.csswith responsive table scroll and metric grid collapse;.article-bodytypography for headings, paragraphs, lists, blockquotes, tables, code blocks slugify()utility — added tocore.pyfor URL-safe slug generationmistunedependency — Markdown → HTML rendering for articles- Sitemap —
/marketsand all published articles added to/sitemap.xml(only articles withpublished_at <= now) - Footer — "Markets" link added to Product column
- Admin dashboard — Templates, Scenarios, Articles quick-link buttons
- Path collision prevention —
RESERVED_PREFIXESvalidation rejects articleurl_pathvalues that conflict with existing routes
Added
- Dev setup script (
scripts/dev_setup.sh) — interactive bootstrap that checks prerequisites, installs deps, creates.envwith auto-generatedSECRET_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.pynow creates a notification destination in Paddle with the 5 event types the webhook handler processes, and writesPADDLE_WEBHOOK_SECRETto.envautomatically - Resend test email docs — documented Resend test addresses
(
delivered@resend.dev,bounced@resend.dev, etc.) in.env.exampleand 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'sts=<unix>;h1=<hmac>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 stalePADDLE_PRICESconfig test (prices now in DB) - Quote wizard state loss —
_accumulatedhidden input used"attribute delimiters which broke ontojsonoutput containing literal"characters; switched all 8 step templates to single-quote delimiters (value='...') - Admin dashboard crash —
no such column: amountin credit ledger query; corrected todelta(the actual column name) - Admin supplier detail — credit ledger table referenced
entry.entry_typeandentry.amountinstead ofentry.event_typeandentry.delta - Nav bar semi-transparent — replaced
rgba(255,255,255,0.85)+backdrop-filter: blur()with opaque#ffffffbackground - Supplier pricing card buttons misaligned — added flexbox to
.pricing-cardwithflex-grow: 1on feature list andmargin-top: autoon 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 activecard_colorboost render a custom border color fromsupplier_boosts.metadataJSON - Removed "Zillow-style" comments from
base.htmlandinput.css
Added
- Migration 0009 — adds
metadata TEXTcolumn tosupplier_boostsfor card color boost configuration - Admin supplier/lead CRUD —
GET/POST /admin/suppliers/newandGET/POST /admin/leads/newroutes 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_TOKENconfig; Paddle.js script inbase.htmlwith sandbox/production toggle - Paddle products in DB — new
paddle_productstable replaces 16PADDLE_PRICE_*env vars;get_paddle_price(key)andget_all_paddle_prices()async helpers incore.py;setup_paddle.pyrewritten to write product/price IDs directly to database - Migration 0008 —
paddle_products,business_plan_exports,feedbacktables;logo_fileandtaglinecolumns onsuppliers - Umami analytics — tracking script in
base.html; config varsUMAMI_API_URL,UMAMI_API_TOKEN,UMAMI_WEBSITE_ID; directory click tracking redirect routes (/<slug>/website,/<slug>/quote) - Supplier dashboard (
/suppliers/dashboard) — tab-based HTMX dashboard with sidebar nav (Overview, Lead Feed, My Listing, Boost & Upsells); each tab loads viahx-getwithhx-push-urlfor 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/listingsaves 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.pywith WeasyPrint PDF engine;plan.html+plan.cssA4 templates (executive summary, CAPEX, OPEX, revenue model, 5-year P&L, 12-month cash flow, sensitivity, key metrics); bilingual EN/DE;generate_business_planworker 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/<id>(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/supplierswith tier/country/name filters (HTMX search),GET /admin/suppliers/<id>detail with profile info, credit balance + ledger, active boosts, lead forward history;POST /admin/suppliers/<id>/creditsmanual credit adjustment;POST /admin/suppliers/<id>/tiermanual 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 /feedbackrate-limited (5/hr per IP), inserts intofeedbacktable;GET /admin/feedbackpaginated 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-sdkandresendSDKs for type safety, built-in webhook verification, and cleaner code;send_email()now acceptsfrom_addrparameter; all Paddle API calls use SDK client - EMAIL_ADDRESSES dict — hardcoded
transactional,leads, andnurturefrom-addresses sharing thenotification.padelnomics.ioResend domain - Paddle product setup script —
scripts/setup_paddle.pycreates all 14 products/prices programmatically via SDK; outputs.envsnippet for CI - Claude Code skills —
.claude/skills/paddle-integration/SKILL.mdand.claude/skills/resend-emails/SKILL.mdfor consistent SDK usage patterns - Migration 0007 —
credit_ledger,lead_forwards,supplier_booststables; 12 new columns onsuppliers(profile, credits);credit_costandunlock_countonlead_requests - Credit system (
credits.py) —get_balance,add_credits,spend_credits,unlock_lead,compute_credit_cost,monthly_credit_refill,get_ledger;InsufficientCreditsexception; heat-based pricing (hot=35, warm=20, cool=8 credits);refill_monthly_creditsworker task + scheduler - Admin lead management —
GET /admin/leadswith status/heat/country filters (HTMX search),GET /admin/leads/<id>detail with project brief + forward history,POST /admin/leads/<id>/statusupdate,POST /admin/leads/<id>/forwardmanual forward (no credit cost); lead funnel stats on admin dashboard (planner users → leads → verified → unlocked) - Lead forwarding emails —
send_lead_forward_emailworker task sends full project brief + contact details to supplier;send_lead_matched_notificationnotifies entrepreneur when a supplier unlocks their lead - Credit cost computed on submission —
credit_costset 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;_accumulatedhidden JSON pattern - Supplier claim flow —
GET /suppliers/claim/<slug>verifies unclaimed and redirects to signup with pre-fill - Webhook handlers —
subscription.activatedwithsupplier_*plan creates supplier record with tier, credits, and boosts;transaction.completedhandles credit pack purchases and sticky boost purchases with expiry - Supplier profile page (
/directory/<slug>) — 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/<id>/unlockspends 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_ENVIRONMENTconfig for sandbox/production switching
Changed
- Supplier marketing page CTAs link to
/suppliers/signupinstead of mailto httpxremoved 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/verifyroute — validates token, activates lead (pending_verification→new, setsverified_at), logs user in, sends admin notification and welcome emailsend_quote_verificationworker task — branded verification email with project details and "Verify & Activate Quote" CTA button (DEBUG mode prints link to console)quote_verify_sent.htmltemplate — "Check your email" page shown after guest quote submission- Migration 0006 — adds
verified_at TEXTcolumn tolead_requests - 9 new tests in
TestQuoteVerificationclass 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/quoteusing server-rendered HTMX partials; each step validates server-side and swaps viahx-post/hx-getwith 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/quotewith 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_NAMESfrom 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_countrycolumns to suppliers table for paid listing support - HTMX live search — directory search input and filters update results
via
hx-getwith 300ms debounce; new/directory/resultsendpoint 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_fieldsverifies 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
supplierstable 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/quoteform - 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/quotenow acceptsapplication/jsonand returns{"ok": true, "heat": "..."}for inline planner submissions; standalone HTML form unchanged
Added — Phase 0 Round 2: Polish & Country-Specific Calculator
- Country-specific calculator —
countryselector (DE/ES/IT/FR/NL/SE/UK/Other) andpermitsComplianceCAPEX 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_typeDB 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 TEXTcolumn tolead_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
countryparameter 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-btnCSS 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/quotewith 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_planprice in PADDLE_PRICES) - SEO meta tags —
<meta>description, og:title, og:description, og:image on planner page - Migration 0002 — expands
lead_requestswith 17 new columns for quote qualification flow; makesuser_idnullable 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.pywithbudgetTarget,glassType,lightingType
Changed
- Planner CTA links now point to
/leads/quotewith 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-5CSS 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.) ininput.cssfor 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
- Standalone Tailwind CLI binary (no Node.js) with
- Landing page teaser calculator restyled with Tailwind utilities and brand colors
Removed
- Pico CSS CDN dependency
custom.css(replaced by Tailwindinput.csswith@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.envhas 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.pymodule docstring documenting the 8-step algorithm, protocol for adding migrations, and design decisions - Sequential migration system (
migrations/migrate.py) — tracks applied versions in_migrationstable, auto-detects fresh vs existing DBs, runs pending migrations in order migrations/versions/0001_rename_ls_to_paddle.py— first versioned migration (absorbed fromscripts/migrate_to_paddle.py)- Server-side financial calculator (
planner/calculator.py) — ported JScalc(),pmt(),calcIRR()to Python so the full financial model is no longer exposed in client-side JavaScript POST /planner/calculateendpoint 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.jswithAbortControllerfor 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.0andrespx>=0.22.0to dev dependencies for property-based testing and httpx mocking - Factored into Copier template — all billing tests now generate as
.jinjatemplates 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 byversions/0001_rename_ls_to_paddle.py
Changed
planner.jsno longer containscalc(),pmt(), orcalcIRR()functions — computation moved server-siderender()split intorender()(tab switching + schedule calc) andrenderWith(d)(DOM updates from data)- Tab switching now renders from
_lastDcache (instant, no API call) - Slider input triggers 200ms debounced server call instead of synchronous client-side calc