From a219df9545b2b6cb4c3c45080a74fce20c7f3dd6 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Wed, 2 Sep 2026 12:45:04 -0500 Subject: [PATCH] appview/js: fix webhook test result, 400 toasts, and Layers tab init Three unrelated client-side defects found while baselining production. The webhook Test button always reported success. renderAlert writes no status code, so both outcomes are HTTP 200 and the result lives in the markup, which partials/alert.html emits as "alert alert-error". testWebhook looked for class="error", which that string does not contain, and resp.ok is always true, so the failure branch was unreachable. A webhook pointed at a dead URL was reported as delivered. Match alert-error instead. Avatar upload rejections lost the server's reason. The htmx:responseError handler maps status codes to fixed strings and had no 400 case, so "File too large (max 3MB)" and "Invalid file type" both surfaced as "Something went wrong". Surface the body when it is short plain text; a rendered error page or a long trace is not toast material. The repository page's Layers tab was never initialised. initLayersTables runs from DOMContentLoaded, when the panel is still a spinner, and from htmx:afterSettle, which htmx.process() does not emit. So empty-layer hiding and no-history run collapsing never ran there, and the checkbox claimed rows were hidden while all of them were on screen. Export it and call it from the tab controller. It now also re-seeds checkboxes within the loaded scope, which fixes the digest page contradicting itself: the stored preference was honoured for the rows and ignored for the control. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UAqi2hS2dhZoatqcWoYZQk --- pkg/appview/src/js/app.js | 22 +++++++++++++++++++--- pkg/appview/src/js/layers.js | 18 +++++++++++++----- pkg/appview/src/js/repository.js | 5 +++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/pkg/appview/src/js/app.js b/pkg/appview/src/js/app.js index 6eecd1e..a97b61d 100644 --- a/pkg/appview/src/js/app.js +++ b/pkg/appview/src/js/app.js @@ -893,6 +893,16 @@ document.addEventListener('DOMContentLoaded', () => { // is the fallback for handlers that didn't set the header. // Opt-out: any ancestor with data-suppress-htmx-toast skips the toast (use // for components that render their own inline error state). +// A 400 usually carries a reason the server already wrote for the user +// ("File too large (max 3MB)", say). Prefer it over the generic fallback, but +// only when it is short plain text — a rendered error page or a long trace is +// not toast material. +function plainTextReason(xhr) { + const body = xhr && typeof xhr.responseText === 'string' ? xhr.responseText.trim() : ''; + if (!body || body.length > 200 || body[0] === '<') return ''; + return body; +} + document.body.addEventListener('htmx:responseError', (evt) => { const elt = evt.detail && evt.detail.elt; if (elt && elt.closest && elt.closest('[data-suppress-htmx-toast]')) return; @@ -906,6 +916,7 @@ document.body.addEventListener('htmx:responseError', (evt) => { : status === 404 ? 'Not found' : status === 429 ? 'Too many requests \u2014 please slow down' : status >= 500 ? 'Server error \u2014 please try again' + : status === 400 ? (plainTextReason(xhr) || 'Something went wrong') : 'Something went wrong'; showToast(msg, 'error'); }); @@ -1021,10 +1032,15 @@ async function testWebhook(id) { credentials: 'include', }); const text = await resp.text(); - if (text.includes('class="success"') || (resp.ok && !text.includes('class="error"'))) { - showToast('Test webhook delivered successfully!', 'success'); + // The handler answers 200 either way and carries the outcome in the + // markup, which partials/alert.html emits as "alert alert-error" \u2014 + // not the bare class="error" an earlier version of this check looked + // for. Matching the wrong string made the failure branch unreachable, + // so a webhook pointed at a dead URL reported success. + if (text.includes('alert-error') || !resp.ok) { + showToast('Test delivery failed, check the webhook URL', 'error'); } else { - showToast('Test delivery failed \u2014 check the webhook URL', 'error'); + showToast('Test webhook delivered successfully!', 'success'); } } catch { showToast('Failed to reach server', 'error'); diff --git a/pkg/appview/src/js/layers.js b/pkg/appview/src/js/layers.js index eb23956..a4e7cfa 100644 --- a/pkg/appview/src/js/layers.js +++ b/pkg/appview/src/js/layers.js @@ -65,12 +65,20 @@ function applyLayerVisibility(table) { }); } -function initLayersTables(root) { +// Exported so the repository page's tab controller can run it on content it +// loaded itself. That panel is filled with a plain fetch + innerHTML and +// htmx.process(), which emits no htmx:afterSettle, so neither hook below +// reaches it and the table would otherwise stay unprocessed. +export function initLayersTables(root) { const scope = root || document; - const tables = scope.querySelectorAll ? - scope.querySelectorAll('.layers-table:not([data-layers-processed])') : - []; - tables.forEach(table => { + if (!scope.querySelectorAll) return; + // Re-seed controls that arrived with this content: a checkbox created + // after DOMContentLoaded has no idea what the stored preference is, and + // leaving it unchecked while the rows it governs are visible makes the UI + // contradict itself. + const show = localStorage.getItem(STORAGE_KEY) === 'true'; + scope.querySelectorAll('.show-empty-layers-cb').forEach(cb => { cb.checked = show; }); + scope.querySelectorAll('.layers-table:not([data-layers-processed])').forEach(table => { table.setAttribute('data-layers-processed', '1'); collapseNoHistoryLayers(table); applyLayerVisibility(table); diff --git a/pkg/appview/src/js/repository.js b/pkg/appview/src/js/repository.js index 13ed39e..23f9548 100644 --- a/pkg/appview/src/js/repository.js +++ b/pkg/appview/src/js/repository.js @@ -1,6 +1,8 @@ // Repository page: owner markdown editor, tag sort/filter, tab controller. // Extracted from repository.html to keep the page shell light. +import { initLayersTables } from './layers.js'; + // ---------------------------------------- // Owner markdown editor (no-op if not owner / not on page) // ---------------------------------------- @@ -236,6 +238,9 @@ function initTabController() { old.parentNode.replaceChild(s, old); }); if (typeof window.htmx !== 'undefined') window.htmx.process(target); + // htmx.process() does not emit htmx:afterSettle, so the layer + // table enhancements have to be kicked off by hand here. + initLayersTables(target); }) .catch(err => { // Clear state immediately so the retry button (or another