2c6062a53e
The /web/ prefix predated the /api/ split; today it just clutters every URL
without disambiguating anything. All 16 browser routes move to the apex
(/cases, /login, /magic, /events, /audio/...). The 6 /api/* routes are
unchanged. A new /->>/cases redirect closes the apex 404.
The open-redirect guard in magic.rs and case_actions.rs flips from a
positive whitelist (starts_with("/web/")) to a deny-list: same-origin path,
not protocol-relative, not under /api/, no \. The /api/ exclusion is now
load-bearing and covered by tests.
Pre-production: no transition redirects.
77 lines
2.6 KiB
JavaScript
77 lines
2.6 KiB
JavaScript
// Inline tap-to-edit for .oneliner-edit blocks. Click swaps the inner
|
|
// .oneliner-text for an <input>; Enter POSTs to /cases/{id}/oneliner;
|
|
// Escape (or blur) cancels. The SSE-driven reload picks up the persisted
|
|
// state, so no DOM patching is needed on success.
|
|
(() => {
|
|
const handleClick = (host) => {
|
|
if (host.classList.contains('editing')) return;
|
|
const span = host.querySelector('.oneliner-text');
|
|
if (!span) return;
|
|
const original = span.textContent.trim();
|
|
host.classList.add('editing');
|
|
const input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.value = original;
|
|
input.maxLength = 500;
|
|
input.className = 'oneliner-input';
|
|
span.replaceWith(input);
|
|
input.focus();
|
|
input.select();
|
|
|
|
const restore = () => {
|
|
const restoreSpan = document.createElement('span');
|
|
restoreSpan.className = 'oneliner-text';
|
|
restoreSpan.textContent = original;
|
|
input.replaceWith(restoreSpan);
|
|
host.classList.remove('editing', 'saving', 'error');
|
|
};
|
|
|
|
const submit = async () => {
|
|
const text = input.value.trim();
|
|
if (text === '' || text === original) { restore(); return; }
|
|
host.classList.remove('error');
|
|
host.classList.add('saving');
|
|
const body = new URLSearchParams({
|
|
text,
|
|
csrf_token: host.dataset.csrf,
|
|
});
|
|
try {
|
|
const r = await fetch(
|
|
`/cases/${host.dataset.caseId}/oneliner`,
|
|
{ method: 'POST', body, credentials: 'same-origin' }
|
|
);
|
|
if (!r.ok) {
|
|
host.classList.remove('saving');
|
|
host.classList.add('error');
|
|
input.focus();
|
|
return;
|
|
}
|
|
// SSE will trigger reload; keep input visible until then.
|
|
} catch (_) {
|
|
host.classList.remove('saving');
|
|
host.classList.add('error');
|
|
input.focus();
|
|
}
|
|
};
|
|
|
|
input.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') { e.preventDefault(); submit(); }
|
|
else if (e.key === 'Escape') { e.preventDefault(); restore(); }
|
|
});
|
|
input.addEventListener('blur', () => {
|
|
// Blur acts as cancel unless we're already saving (where blur
|
|
// happens implicitly when the SSE reload swaps the DOM) or in
|
|
// an error state (so the user can re-focus and retry).
|
|
if (host.classList.contains('saving') || host.classList.contains('error')) return;
|
|
restore();
|
|
});
|
|
};
|
|
|
|
document.addEventListener('click', (e) => {
|
|
const host = e.target.closest('.oneliner-edit');
|
|
if (!host) return;
|
|
if (e.target.closest('a, button, input')) return;
|
|
handleClick(host);
|
|
});
|
|
})();
|