Two defects, both found by driving the real editor in Firefox:
1. The editor drew grab handles with a grab cursor on sets it refuses to
reorder, so the live Food menu looked draggable and did nothing. Handles and
the move buttons are now rendered only when reordering is possible, and the
notice says what can still be done.
2. Reordering was saved only from dragend. Browsers suppress mouse events during
a drag and dragend is not guaranteed to arrive, which left the rows visibly
reordered while the server kept the old order — the drag looked like it
worked and then reverted on reload. The order now commits from dragover after
a pause in the drag, with drop/dragend/mouse events as faster paths, deduped
against the last confirmed order and reported in the page.
To make (1) coherent the Current set may now be reordered while its Pages stay
fixed: order is presentation, media URLs are content-hashed and pages are
rendered per request, so nothing stale can be served (ADR 0006). Archives stay
frozen. Admin pages are no-store and assets cache for 5 minutes, so a fix like
this one is not masked by a stale admin.js.
scripts/check-dnd.mjs drives the real editor and asserts the drag persists; it
is the only automated check that reaches this path.
A file past the size limit makes the multipart stream itself throw, which
escaped the per-file catch and reached the error handler: the phone that just
sent a 35 MB photo got 'Something went wrong'. Catch the limit errors around the
part loop, say which limit was hit, and report it once rather than twice. Also
asserts that derivatives carry no EXIF while the Original keeps the uploaded
bytes.
Every mutating form now carries the CSRF token as a hidden field, and the
upload route accepts it as a multipart field as well as a header. Driving the
real UI in Firefox found what the header-only tests could not: Create Draft,
Save, Publish, Delete and the up/down buttons all posted without a token and
were rejected. Adds npm run check-site, which verifies a running Site from the
outside and exits non-zero so it can gate a cutover.
The admin signs in with the shared password, creates a Menu Set, drops photos
into it (shrunk in the browser first), reorders Pages by dragging or with
up/down buttons, previews the Draft exactly as customers would see it, and
publishes it over the previous set. Restore and delete close the loop. Mutations
need a session, a same-origin request and a CSRF token derived from the session;
a Draft's media is served only to a session holder.