From d3626193c56a64a7e6b63b2232cdc0d3f7e48f85 Mon Sep 17 00:00:00 2001 From: Deeman Date: Mon, 2 Mar 2026 07:35:32 +0100 Subject: [PATCH] =?UTF-8?q?refactor(admin):=20unify=20confirm=20dialog=20?= =?UTF-8?q?=E2=80=94=20pure=20hx-confirm=20+=20form[method=3Ddialog]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminate `confirmAction()` and the duplicate `cloneNode` hack entirely. One code path: everything goes through `showConfirm()` called by the `htmx:confirm` interceptor. Dialog HTML: - `
` 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 --- CHANGELOG.md | 6 ++++ web/src/padelnomics/admin/pipeline_routes.py | 9 ++++-- .../admin/templates/admin/affiliate_form.html | 4 +-- .../admin/affiliate_program_form.html | 4 +-- .../admin/templates/admin/articles.html | 5 ++-- .../templates/admin/audience_contacts.html | 5 ++-- .../admin/templates/admin/base_admin.html | 28 ++++++++++++------- .../admin/templates/admin/generate_form.html | 5 ++-- .../partials/affiliate_program_results.html | 4 +-- .../admin/partials/affiliate_row.html | 4 +-- .../admin/partials/pipeline_extractions.html | 12 ++++---- .../admin/partials/pipeline_overview.html | 2 +- .../admin/partials/pipeline_transform.html | 6 ++-- .../admin/partials/scenario_results.html | 5 ++-- .../admin/templates/admin/pipeline.html | 6 ++-- .../templates/admin/template_detail.html | 5 ++-- 16 files changed, 66 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd7eb89..f2c54e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Changed +- **Unified confirm dialog — pure HTMX `hx-confirm` + ``** — eliminated the `confirmAction()` JS function and the duplicate `cloneNode` hack. All confirmation prompts now go through a single `showConfirm()` Promise-based function called by the `htmx:confirm` interceptor. The dialog HTML uses `` for native close semantics (`returnValue` is `"ok"` or `"cancel"`), removing the need to clone and replace buttons on every invocation. All 12 Padelnomics call sites converted from `onclick=confirmAction(...)` to `hx-boost="true"` + `hx-confirm="..."` on the submit button. Pipeline trigger endpoints updated to treat `HX-Boosted: true` requests as non-HTMX (returning a redirect rather than an inline partial) so boosted form submissions flow through the normal redirect cycle. Same changes applied to BeanFlows and the quart-saas-boilerplate template. + - `web/src/padelnomics/admin/templates/admin/base_admin.html`: replaced dialog `
` with ``, replaced `confirmAction()` + inline `htmx:confirm` handler with unified `showConfirm()` + single `htmx:confirm` listener + - `web/src/padelnomics/admin/pipeline_routes.py`: `pipeline_trigger_extract` and `pipeline_trigger_transform` now exclude `HX-Boosted: true` from the HTMX partial path + - 12 templates updated: `pipeline.html`, `partials/pipeline_extractions.html`, `affiliate_form.html`, `affiliate_program_form.html`, `partials/affiliate_program_results.html`, `partials/affiliate_row.html`, `generate_form.html`, `articles.html`, `audience_contacts.html`, `template_detail.html`, `partials/scenario_results.html` + - Same changes mirrored to BeanFlows and quart-saas-boilerplate template + - **Per-proxy dead tracking in tiered cycler** — `make_tiered_cycler` now accepts a `proxy_failure_limit` parameter (default 3). Individual proxies that hit the limit are marked dead and permanently skipped by `next_proxy()`. If all proxies in the active tier are dead, `next_proxy()` auto-escalates to the next tier without needing the tier-level threshold. `record_failure(proxy_url)` and `record_success(proxy_url)` accept an optional `proxy_url` argument for per-proxy tracking; callers without `proxy_url` are fully backward-compatible. New `dead_proxy_count()` callable exposed for monitoring. - `extract/padelnomics_extract/src/padelnomics_extract/proxy.py`: added per-proxy state (`proxy_failure_counts`, `dead_proxies`), updated `next_proxy`/`record_failure`/`record_success`, added `dead_proxy_count` - `extract/padelnomics_extract/src/padelnomics_extract/playtomic_tenants.py`: `_fetch_page_via_cycler` passes `proxy_url` to `record_success`/`record_failure` diff --git a/web/src/padelnomics/admin/pipeline_routes.py b/web/src/padelnomics/admin/pipeline_routes.py index 3bc926f..623b649 100644 --- a/web/src/padelnomics/admin/pipeline_routes.py +++ b/web/src/padelnomics/admin/pipeline_routes.py @@ -780,7 +780,8 @@ async def pipeline_trigger_extract(): else: await enqueue("run_extraction") - is_htmx = request.headers.get("HX-Request") == "true" + is_htmx = (request.headers.get("HX-Request") == "true" + and request.headers.get("HX-Boosted") != "true") if is_htmx: return await _render_overview_partial() @@ -1005,7 +1006,8 @@ async def pipeline_trigger_transform(): (task_name,), ) if existing: - is_htmx = request.headers.get("HX-Request") == "true" + is_htmx = (request.headers.get("HX-Request") == "true" + and request.headers.get("HX-Boosted") != "true") if is_htmx: return await _render_transform_partial() await flash(f"A '{step}' task is already queued (task #{existing['id']}).", "warning") @@ -1013,7 +1015,8 @@ async def pipeline_trigger_transform(): await enqueue(task_name) - is_htmx = request.headers.get("HX-Request") == "true" + is_htmx = (request.headers.get("HX-Request") == "true" + and request.headers.get("HX-Boosted") != "true") if is_htmx: return await _render_transform_partial() diff --git a/web/src/padelnomics/admin/templates/admin/affiliate_form.html b/web/src/padelnomics/admin/templates/admin/affiliate_form.html index 9acd571..9a158e3 100644 --- a/web/src/padelnomics/admin/templates/admin/affiliate_form.html +++ b/web/src/padelnomics/admin/templates/admin/affiliate_form.html @@ -226,10 +226,10 @@ document.addEventListener('DOMContentLoaded', function() { Cancel
{% if editing %} - + + hx-confirm="Delete this product? This cannot be undone.">Delete {% endif %} diff --git a/web/src/padelnomics/admin/templates/admin/affiliate_program_form.html b/web/src/padelnomics/admin/templates/admin/affiliate_program_form.html index 9c2949d..3ec2a03 100644 --- a/web/src/padelnomics/admin/templates/admin/affiliate_program_form.html +++ b/web/src/padelnomics/admin/templates/admin/affiliate_program_form.html @@ -120,10 +120,10 @@ document.addEventListener('DOMContentLoaded', function() { Cancel {% if editing %} -
+ + hx-confirm="Delete this program? Blocked if products reference it.">Delete
{% endif %} diff --git a/web/src/padelnomics/admin/templates/admin/articles.html b/web/src/padelnomics/admin/templates/admin/articles.html index c8133d4..80acc2a 100644 --- a/web/src/padelnomics/admin/templates/admin/articles.html +++ b/web/src/padelnomics/admin/templates/admin/articles.html @@ -11,9 +11,10 @@
New Article -
+ - +
diff --git a/web/src/padelnomics/admin/templates/admin/audience_contacts.html b/web/src/padelnomics/admin/templates/admin/audience_contacts.html index b48de55..e3e2187 100644 --- a/web/src/padelnomics/admin/templates/admin/audience_contacts.html +++ b/web/src/padelnomics/admin/templates/admin/audience_contacts.html @@ -27,10 +27,11 @@ {{ c.email if c.email is defined else (c.get('email', '-') if c is mapping else '-') }} {{ (c.created_at if c.created_at is defined else (c.get('created_at', '-') if c is mapping else '-'))[:16] if c else '-' }} -
+ - +
diff --git a/web/src/padelnomics/admin/templates/admin/base_admin.html b/web/src/padelnomics/admin/templates/admin/base_admin.html index 28323f6..6f8c0aa 100644 --- a/web/src/padelnomics/admin/templates/admin/base_admin.html +++ b/web/src/padelnomics/admin/templates/admin/base_admin.html @@ -228,21 +228,29 @@

-
- - -
+
+ + +
{% endblock %} diff --git a/web/src/padelnomics/admin/templates/admin/generate_form.html b/web/src/padelnomics/admin/templates/admin/generate_form.html index 1b7c137..33e3298 100644 --- a/web/src/padelnomics/admin/templates/admin/generate_form.html +++ b/web/src/padelnomics/admin/templates/admin/generate_form.html @@ -19,7 +19,7 @@

No data rows found. Run the data pipeline to populate {{ config_data.data_table }}.

{% else %} -
+
@@ -45,7 +45,8 @@

-
diff --git a/web/src/padelnomics/admin/templates/admin/partials/affiliate_program_results.html b/web/src/padelnomics/admin/templates/admin/partials/affiliate_program_results.html index 5d79cce..3de0bbd 100644 --- a/web/src/padelnomics/admin/templates/admin/partials/affiliate_program_results.html +++ b/web/src/padelnomics/admin/templates/admin/partials/affiliate_program_results.html @@ -21,10 +21,10 @@ Edit -
+ + hx-confirm="Delete {{ prog.name }}? This is blocked if products reference it.">Delete
diff --git a/web/src/padelnomics/admin/templates/admin/partials/affiliate_row.html b/web/src/padelnomics/admin/templates/admin/partials/affiliate_row.html index 5846ec7..494c45b 100644 --- a/web/src/padelnomics/admin/templates/admin/partials/affiliate_row.html +++ b/web/src/padelnomics/admin/templates/admin/partials/affiliate_row.html @@ -20,10 +20,10 @@ {{ product.click_count or 0 }} Edit -
+ + hx-confirm="Delete {{ product.name }}?">Delete
diff --git a/web/src/padelnomics/admin/templates/admin/partials/pipeline_extractions.html b/web/src/padelnomics/admin/templates/admin/partials/pipeline_extractions.html index de54678..40a6691 100644 --- a/web/src/padelnomics/admin/templates/admin/partials/pipeline_extractions.html +++ b/web/src/padelnomics/admin/templates/admin/partials/pipeline_extractions.html @@ -29,10 +29,10 @@ -
+ -
@@ -112,11 +112,11 @@ {% if run.status == 'running' %}
+ class="m-0" hx-boost="true"> -
diff --git a/web/src/padelnomics/admin/templates/admin/partials/pipeline_overview.html b/web/src/padelnomics/admin/templates/admin/partials/pipeline_overview.html index 43209c8..ce22a1d 100644 --- a/web/src/padelnomics/admin/templates/admin/partials/pipeline_overview.html +++ b/web/src/padelnomics/admin/templates/admin/partials/pipeline_overview.html @@ -40,7 +40,7 @@ hx-target="#pipeline-overview-content" hx-swap="outerHTML" hx-vals='{"extractor": "{{ wf.name }}", "csrf_token": "{{ csrf_token() }}"}' - onclick="if (!confirm('Run {{ wf.name }} extractor?')) return false;">Run + hx-confirm="Run {{ wf.name }} extractor?">Run

{{ wf.schedule_label }}

{% if run %} diff --git a/web/src/padelnomics/admin/templates/admin/partials/pipeline_transform.html b/web/src/padelnomics/admin/templates/admin/partials/pipeline_transform.html index 5f16034..2e5b786 100644 --- a/web/src/padelnomics/admin/templates/admin/partials/pipeline_transform.html +++ b/web/src/padelnomics/admin/templates/admin/partials/pipeline_transform.html @@ -53,7 +53,7 @@ hx-target="#pipeline-transform-content" hx-swap="outerHTML" hx-vals='{"step": "transform", "csrf_token": "{{ csrf_token() }}"}' - onclick="if (!confirm('Run SQLMesh transform (prod --auto-apply)?')) return false;"> + hx-confirm="Run SQLMesh transform (prod --auto-apply)?"> Run Transform @@ -107,7 +107,7 @@ hx-target="#pipeline-transform-content" hx-swap="outerHTML" hx-vals='{"step": "export", "csrf_token": "{{ csrf_token() }}"}' - onclick="if (!confirm('Export serving tables (lakehouse → analytics.duckdb)?')) return false;"> + hx-confirm="Export serving tables (lakehouse → analytics.duckdb)?"> Run Export @@ -138,7 +138,7 @@ hx-target="#pipeline-transform-content" hx-swap="outerHTML" hx-vals='{"step": "pipeline", "csrf_token": "{{ csrf_token() }}"}' - onclick="if (!confirm('Run full ELT pipeline (extract → transform → export)?')) return false;"> + hx-confirm="Run full ELT pipeline (extract → transform → export)?"> Run Full Pipeline diff --git a/web/src/padelnomics/admin/templates/admin/partials/scenario_results.html b/web/src/padelnomics/admin/templates/admin/partials/scenario_results.html index d742914..3ba3047 100644 --- a/web/src/padelnomics/admin/templates/admin/partials/scenario_results.html +++ b/web/src/padelnomics/admin/templates/admin/partials/scenario_results.html @@ -36,9 +36,10 @@ PDF EN PDF DE Edit -
+ - +
diff --git a/web/src/padelnomics/admin/templates/admin/pipeline.html b/web/src/padelnomics/admin/templates/admin/pipeline.html index 7f8d216..dca3454 100644 --- a/web/src/padelnomics/admin/templates/admin/pipeline.html +++ b/web/src/padelnomics/admin/templates/admin/pipeline.html @@ -56,11 +56,11 @@

Extraction status, data catalog, and ad-hoc query editor

-
+ -
diff --git a/web/src/padelnomics/admin/templates/admin/template_detail.html b/web/src/padelnomics/admin/templates/admin/template_detail.html index d0ca524..5395c02 100644 --- a/web/src/padelnomics/admin/templates/admin/template_detail.html +++ b/web/src/padelnomics/admin/templates/admin/template_detail.html @@ -13,9 +13,10 @@
Generate Articles -
+ -