Files
member-console/internal/embeds/static/error-handler.js
T
cgalo5758 8514df5b50 Remediate operator UX audit findings
Fix HTMX expired-session handling, CSP-blocked form behaviors, reorder
recovery, billing currency display, plan/checkout guards, FedWiki quota
edge cases, and operator/member empty/error states.

Add entitlement uniqueness migrations, canonical migration source
wiring,
and regression coverage for the remediated flows. Update status docs
with
the audit triage and model inventory.
2026-07-03 11:58:03 -05:00

104 lines
3.9 KiB
JavaScript

// Error handling for HTMX requests
// Shows toast notifications for failed requests and preserves user input
// Signal that a request's swap was suppressed (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();
}
// Handle HTMX response errors - this fires BEFORE swap
document.body.addEventListener('htmx:beforeSwap', function(evt) {
const xhr = evt.detail.xhr;
const status = xhr.status;
// Handle 403 Forbidden (CSRF errors)
if (status === 403) {
// Check if it's a CSRF error by looking at response
const responseText = xhr.responseText || '';
if (responseText.includes('CSRF') || responseText.includes('csrf')) {
// Prevent the swap so user input is preserved
evt.detail.shouldSwap = false;
evt.detail.isError = false; // Prevent htmx:responseError from firing
showErrorToast('Your session has expired. Please refresh the page to continue.');
signalReorderRecover();
return;
}
// Other 403 errors
evt.detail.shouldSwap = false;
showErrorToast('Access denied. Please refresh the page and try again.');
signalReorderRecover();
return;
}
// Handle 5xx server errors
if (status >= 500) {
evt.detail.shouldSwap = false;
showErrorToast('Server error. Please try again later.');
signalReorderRecover();
return;
}
// Handle network errors (status 0)
if (status === 0) {
evt.detail.shouldSwap = false;
showErrorToast('Network error. Please check your connection and try again.');
signalReorderRecover();
return;
}
// 422 Unprocessable Entity is the server-side validation contract per
// docs/operator-ux-conventions.md §6: the response body carries the form
// re-rendered with `is-invalid` + `.invalid-feedback` on the offending
// field(s). htmx 2.x's default responseHandling has `[45]..` set to
// `swap:false`, so we must explicitly enable the swap (and clear the
// error flag) for the validation body to land in the DOM. The inline
// message is strictly more useful than a generic "check your input"
// toast, so stay silent on the toast side.
if (status === 422) {
evt.detail.shouldSwap = true;
evt.detail.isError = false;
return;
}
// For other 4xx errors, allow the swap as the server may return helpful error HTML
// but show a toast as backup
if (status >= 400 && status < 500) {
evt.detail.shouldSwap = true;
showErrorToast('Request failed. Please check your input and try again.');
}
});
// Fallback handler for any errors that slip through
document.body.addEventListener('htmx:responseError', function(evt) {
console.error('HTMX Response Error:', evt.detail);
// Toast already shown by beforeSwap handler in most cases
});
// Handle send errors (network failures before response)
document.body.addEventListener('htmx:sendError', function(evt) {
showErrorToast('Failed to send request. Please check your connection.');
// No swap ever lands, so a drag-to-reorder started here stays frozen too.
signalReorderRecover();
});