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