- Add customer.subscription.created → subscription.activated mapping in
stripe.parse_webhook so direct API subscription creation also creates DB rows
- Add customer.subscription.created to setup_stripe.py enabled_events
- Pin PAYMENT_PROVIDER=paddle and STRIPE_WEBHOOK_SECRET="" in test conftest
so billing tests don't hit real Stripe API when env has Stripe keys
- Add 8 unit tests for stripe.parse_webhook covering all event types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stripe Python SDK doesn't accept request_options as a kwarg to create/retrieve/modify.
Timeouts are handled by the global max_network_retries setting.
Also gracefully handle webhook endpoint creation failure for localhost URLs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mirrors setup_paddle.py structure:
- Creates 17 products + prices in Stripe (same keys, same prices)
- Writes to payment_products table with provider='stripe'
- Registers webhook endpoint at /billing/webhook/stripe
- tax_behavior='exclusive' (price + VAT on top, EU standard)
- Supports --sync flag to re-populate from existing Stripe products
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New _payment_js.html: conditionally loads Paddle.js or nothing (Stripe
uses server-side Checkout Session). Provides startCheckout() helper.
- All checkout templates use _payment_js.html instead of _paddle.html
- export.html, signup_step_4.html: Paddle.Checkout.open() → startCheckout()
- dashboard_boosts.html: inline onclick → buyItem() with server round-trip
- New /billing/checkout/item endpoint for single-item purchases (boosts, credits)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Curly quotes (U+201C/U+201D) were used as JSON key/value delimiters
on line 894 of both locale files, making them invalid JSON.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Task 1: Hero, features, FAQ, final CTA, supplier matching, meta/SEO
strings all rewritten. New keys added for proof strip, struggling-
moments section, and "Why Padelnomics" comparison section.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the auto-escaped `{{ body_html }}` div (showed raw HTML tags)
with a sandboxed `<iframe srcdoc>` pattern matching the email preview.
Both the initial page load and the HTMX live-update endpoint now build
a full `preview_doc` document embedding the public CSS and wrapping
content in `<div class="article-body">` — pixel-perfect against the
live article, admin styles fully isolated.
Also:
- Delete ~65 lines of redundant `.preview-body` custom CSS
- Add "Meta ▾" toolbar toggle to collapse/expand metadata strip
- Add word count footer in the editor pane (updates on input)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes:
- _find_article_md() scans _ARTICLES_DIR for files whose frontmatter
slug matches, so padel-halle-bauen-de.md is found for slug
'padel-halle-bauen'. The previous exact-name lookup missed any file
where the filename ≠ slug (e.g. {slug}-{lang}.md naming convention).
- Editor pane: replace dark navy background with warm off-white (#FEFDFB)
and dark text so it reads like a document, not a code editor.
Bug: article_edit GET was passing raw .md file content (including YAML
frontmatter) to the body textarea. Articles synced from disk via
_sync_static_articles() had their frontmatter bled into the editor,
making it look like content was missing or garbled.
Fix: strip frontmatter (using existing _FRONTMATTER_RE) before setting
body, consistent with how _rebuild_article() already does it.
Also switch to _ARTICLES_DIR (absolute) instead of relative path.
New: split editor layout — compact metadata strip at top, dark
monospace textarea on the left, live rendered preview on the right
(HTMX, 500ms debounce). Initial preview server-rendered on page load.
New POST /admin/articles/preview endpoint returns the preview partial.
Lift admin_client fixture from 7 duplicate definitions into conftest.py.
Add mock_send_email fixture, replacing 60 inline patch() blocks across
test_emails.py, test_waitlist.py, and test_businessplan.py. Net -197 lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eliminates confirmAction() entirely. One code path: all confirmations
go through showConfirm() called by the htmx:confirm interceptor.
14 template files converted to hx-boost + hx-confirm pattern.
Pipeline endpoints updated to exclude HX-Boosted requests from the
HTMX partial path.
# Conflicts:
# web/src/padelnomics/admin/templates/admin/affiliate_form.html
# web/src/padelnomics/admin/templates/admin/affiliate_program_form.html
# web/src/padelnomics/admin/templates/admin/base_admin.html
# web/src/padelnomics/admin/templates/admin/partials/affiliate_program_results.html
# web/src/padelnomics/admin/templates/admin/partials/affiliate_row.html
Task 3/6: lead_forward and lead_forward_htmx shared ~20 lines of
identical DB logic. Extracted into _forward_lead() that returns an
error string or None. Both routes now call the helper and differ
only in response format (redirect vs HTMX partial).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Task 2/6: Adds count_where(table_where, params) to core.py that
compresses the fetch_one + null-check COUNT(*) pattern. Applied
across admin/routes.py — dashboard stats shrinks from ~75 to ~25
lines, plus 10 more call sites compressed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eliminate `confirmAction()` and the duplicate `cloneNode` hack entirely.
One code path: everything goes through `showConfirm()` called by the
`htmx:confirm` interceptor.
Dialog HTML:
- `<form method="dialog">` for native close semantics; button `value`
becomes `dialog.returnValue` — no manual event listener reassignment.
JS:
- `showConfirm(message)` — Promise-based, listens for `close` once.
- `htmx:confirm` handler calls `showConfirm()` and calls `issueRequest`
if confirmed. Replaces both the old HTMX handler and `confirmAction()`.
Templates (Padelnomics, 14 files):
- All `onclick=confirmAction(...)` and `onclick=confirm()` removed.
- Form-submit buttons: added `hx-boost="true"` to form + `hx-confirm`
on the submit button.
- Pure HTMX buttons (pipeline_transform, pipeline_overview): `hx-confirm`
replaces `onclick=if(!confirm(...))return false;`.
Pipeline routes (pipeline_trigger_extract, pipeline_trigger_transform):
- `is_htmx` now excludes `HX-Boosted: true` requests — boosted form
POSTs get the normal redirect instead of the inline partial.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The handler called evt.preventDefault() unconditionally, so auto-poll
requests (hx-trigger="every 5s", no hx-confirm) caused an empty dialog
to pop up every 5 seconds. Add an early return when evt.detail.question
is falsy so only actual hx-confirm interactions are intercepted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add global htmx:confirm handler in base_admin.html that intercepts
hx-confirm attributes and shows #confirm-dialog instead of window.confirm()
- Convert 4 pipeline HTMX buttons (Run Transform, Run Export, Run Full
Pipeline, Run extractor) from onclick+confirm() to hx-confirm
- Convert 4 affiliate form/list delete buttons from onclick+confirm()
to confirmAction() via event.preventDefault()
- Add scrollbar-width:none + ::-webkit-scrollbar{display:none} to
.pipeline-tabs to suppress spurious horizontal scrollbar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add proxy_failure_limit param to make_tiered_cycler (default 3).
Individual proxies hitting the limit are marked dead and permanently
skipped. next_proxy() auto-escalates when all proxies in the active
tier are dead. Both mechanisms coexist: per-proxy dead tracking removes
broken individuals; tier-level threshold catches systemic failure.
- proxy.py: dead_proxies set + proxy_failure_counts dict in state;
next_proxy skips dead proxies with bounded loop; record_failure/
record_success accept optional proxy_url; dead_proxy_count() added
- playtomic_tenants.py: pass proxy_url to record_success/record_failure
- playtomic_availability.py: _worker returns (proxy_url, result);
serial loops in extract + extract_recheck capture proxy_url
- test_supervisor.py: 11 new tests in TestTieredCyclerDeadProxyTracking
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docker-compose.prod.yml: fix volume mount for all 6 web containers
from /opt/padelnomics/data (stale) → /data/padelnomics (live supervisor output);
add LANDING_DIR=/app/data/pipeline/landing so extraction/landing stats work
- pipeline_routes.py: fix _REPO_ROOT parents[5] → parents[4] so workflows.toml
is found in dev and pipeline overview shows workflow schedules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Centralises retailer config in affiliate_programs table (URL template,
tracking tag, commission %). Products now use program dropdown + product
identifier instead of manual URL baking. URL assembled at redirect time
via build_affiliate_url() — changing a tag propagates to all products
instantly. Backward compatible: legacy baked-URL products fall through
unchanged. Amazon OneLink (configured in Associates dashboard) handles
geo-redirect to local marketplaces with no additional programs needed.
Also fixes _rebuild_article() frontmatter rendering bug.
Commits: fix frontmatter, migration 0027, program CRUD functions,
redirect update, admin CRUD + templates, product form update, tests.
41 tests, all passing. Ruff clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the manual affiliate URL field with a program selector and
product identifier input. JS toggles visibility between program mode and
manual (custom URL) mode. retailer field is auto-populated from the
program name on save. INSERT/UPDATE statements include new program_id
and product_identifier columns. Validation accepts program+ID or manual
URL as the URL source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds program list, create, edit, delete routes with appropriate guards
(delete blocked if products reference the program). Adds "Programs" tab
to the affiliate subnav. New templates: affiliate_programs.html,
affiliate_program_form.html, partials/affiliate_program_results.html.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Program-based products now get URLs assembled from the template at
redirect time. Changing a program's tracking_tag propagates instantly
to all its products without rebuilding. Legacy products (no program_id)
still use their baked affiliate_url via fallback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds get_all_programs(), get_program(), get_program_by_slug() for admin
CRUD. Adds build_affiliate_url() that assembles URLs from program template
+ product identifier, with fallback to baked affiliate_url for legacy
products. Updates get_product() to JOIN affiliate_programs so _program
dict is available at redirect time. _parse_product() extracts program
fields into nested _program key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>