Unsaved work: a form sent with htmx stops being guarded the moment it is sent, so a failed save can lose what was typed #1156
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#1156
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What exists today
static/js/app.js:1925-1993keeps a per-form dirty mark (form.dataset.dirty) thatbeforeunload(2023-2032) and the in-page leave dialog (2200-2295) read. Two listeners clear it for an htmx form, and neither looks at the outcome:app.js:1987-1993, thesubmitlistener, deletes the mark for every submitted form. For a form withhx-post, htmx cancels the native submission but the event still bubbles to the document, so the mark is gone as the request leaves, before any answer.app.js:1996-2001,htmx:afterRequest, deletes it again forevent.target.closest("form"). In the vendored htmx 2.0.10 (node_modules/htmx.org/dist/htmx.js:4581, 4608, 4615, 4622)afterRequestis raised after a 4xx or 5xx (whichresponseHandlingdoes not swap, lines 264-268), after a network error, an abort and a timeout, as well as after success.So when the server refuses or the network drops, the form stays on the page with the person's text in it, the alert says the request failed, and the next link click or tab close leaves without asking. (In passing: the comment at
app.js:221-223saysafterRequestis not raised when the request never reached the server; in htmx 2.0.10 it is. Harmless there, since the code listens to all four events, but the comment should be corrected.)Guarded forms (
method="post", nodata-no-guard, not an autosubmit control) that post through htmx, found by scanning every template for a<form>withmethod="post"and anhx-post:applications/partials/event_form.html:11hx-target="#event-form", outerHTML)applications/partials/status_card.html:47cotton/dialog.html:40<c-dialog action=... live>,hx-swap="none"(Your details row removals)cotton/reminder_actions.html:37, 61, 70targetis setjobs/company_form.html:41core/partials/theme_switch.html:9is excluded bydata-theme-switch;partials/table/cell.html:27, the cell editor, has nomethodand so is never guarded (markDirty, 1950-1952), which is outside this issue. The first two rows are where typed text is at risk.What would change
submitlistener skips forms htmx sends (hx-post/hx-geton the form, orevent.defaultPreventedafter htmx's own listener), leaving their mark to the answer.afterRequestlistener clears the mark only whenevent.detail.successfulis true and the request was the form's own submission (event.detail.eltis the form, or its submitter). On a failure the mark stays, so leaving asks.hx-swap="none"(the live dialog) is cleared by the success branch. A refused save that the server redraws (a 2xx with errors, asevent_formdoes) arrives as a new form element holding the typed values, unmarked, which matches the full-page behaviour where a refused save is a new page.What it saves or protects
reliability: text typed into the timeline form (summary and notes), a status note or a dialog is no longer lost without a question after a request that failed, timed out or was refused
Accessibility
must be checked: the leave question must still come in Postulo's own dialog with focus handed back on Cancel (#657), the failure must still be announced by the page's
role="alert"region (data-htmx-alert,static/js/app.js:235-257), and a refused save that the server redraws must not ask twice. Guarded today bytests/e2e/test_unsaved_work.py(full-page forms only, no htmx-posted one) andtests/test_page_script.py:85-87(the failure listeners exist); a new e2e case is needed for an htmx form whose request failsDecisions
Found by the efficiency and reliability audit of 2026-10-30 (finding A11; index postulo/postulo#1108). Checked against
mainatdaee30b15(2026-10-30).