- Pin Dockerfile to Go 1.23 to match go.mod - Record README front-door audit findings
91 lines
3.9 KiB
JavaScript
91 lines
3.9 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
// Error handling for HTMX requests
|
|
// Shows toast notifications for failed requests and preserves user input.
|
|
//
|
|
// htmx 4 swaps every response except 204/304 by default. Swap suppression is
|
|
// declarative, not scripted: the statuses whose bodies are not designed as
|
|
// swap content (403, 5xx) sit in the "noSwap" list of each page's
|
|
// htmx-config meta tag. 422 swaps natively — its body is the re-rendered
|
|
// form with inline field errors (docs/operator-ux-conventions.md §6), so no
|
|
// script steers that swap anymore. This script only reports:
|
|
// htmx:response:error HTTP >= 400 — toast by status; 422 stays silent
|
|
// htmx:error network failure or timeout — toast
|
|
|
|
// Signal that a request's swap was suppressed or never landed (CSRF expiry,
|
|
// 5xx, network error). sortable-reorder.js listens for this to recover a
|
|
// frozen drag-to-reorder: SortableJS has already moved the DOM row and
|
|
// disabled the instance, so a suppressed swap would leave the table showing
|
|
// the new order with stale rank badges and drag permanently dead. The
|
|
// listener re-enables the instance and re-fetches server order (audit
|
|
// finding #55). It is a no-op unless a reorder was actually in flight, so
|
|
// dispatching broadly is safe.
|
|
function signalReorderRecover() {
|
|
document.body.dispatchEvent(new CustomEvent('operator:reorder-recover'));
|
|
}
|
|
|
|
// Show error toast with a message
|
|
function showErrorToast(message) {
|
|
const toastBody = document.getElementById('errorToastBody');
|
|
const toastEl = document.getElementById('errorToast');
|
|
if (!toastBody || !toastEl) {
|
|
console.error('Error toast elements not found');
|
|
alert(message); // Fallback to alert
|
|
return;
|
|
}
|
|
toastBody.textContent = message;
|
|
const toast = new bootstrap.Toast(toastEl, { delay: 8000 });
|
|
toast.show();
|
|
}
|
|
|
|
// HTTP error responses (status >= 400). Whether the body swaps is decided by
|
|
// the noSwap config, not here.
|
|
document.body.addEventListener('htmx:response:error', function(evt) {
|
|
const ctx = evt.detail && evt.detail.ctx;
|
|
const status = ctx && ctx.response ? ctx.response.status : 0;
|
|
|
|
// 422 Unprocessable Entity is the server-side validation contract: the
|
|
// body carries the form re-rendered with `is-invalid` + `.invalid-feedback`
|
|
// on the offending field(s) and swaps inline by default behavior. The
|
|
// inline message is strictly more useful than a generic toast, so stay
|
|
// silent.
|
|
if (status === 422) {
|
|
return;
|
|
}
|
|
|
|
// 403 Forbidden (in noSwap: the swap is suppressed, user input preserved).
|
|
if (status === 403) {
|
|
const text = (ctx && ctx.text) || '';
|
|
if (text.includes('CSRF') || text.includes('csrf')) {
|
|
// A CSRF failure means this page's token no longer matches the
|
|
// browser's cookie (the page outlived the cookie, or another tab
|
|
// minted a new one). The session itself is fine; a session that has
|
|
// ended arrives as a 401 with HX-Redirect and never reaches here.
|
|
showErrorToast('This page is out of date. Refresh it and try again.');
|
|
} else {
|
|
showErrorToast('Access denied. Please refresh the page and try again.');
|
|
}
|
|
signalReorderRecover();
|
|
return;
|
|
}
|
|
|
|
// 5xx server errors (in noSwap: the swap is suppressed).
|
|
if (status >= 500) {
|
|
showErrorToast('Server error. Please try again later.');
|
|
signalReorderRecover();
|
|
return;
|
|
}
|
|
|
|
// Other 4xx swap by default — the server may return helpful error HTML —
|
|
// but show a toast as backup.
|
|
showErrorToast('Request failed. Please check your input and try again.');
|
|
});
|
|
|
|
// Request-level failures: network errors, timeouts (htmx 4 defaults to 60s).
|
|
document.body.addEventListener('htmx:error', function(evt) {
|
|
showErrorToast('Network error. Please check your connection and try again.');
|
|
// No swap ever lands, so a drag-to-reorder started here stays frozen too.
|
|
signalReorderRecover();
|
|
});
|