mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 10:04:18 +00:00
Eight defects from one manual pass. Three of them were the same line. **Order is not identity.** account.sort_order decided where a row sat, which account was primary, AND which of eight colours its dot was painted with — all in AccountCreator::resequence(). So dragging a row to tidy the settings list silently reassigned the address Compose sends from, and swapped two accounts' identity marks in the sidebar and on every message row at once. Nothing on screen said either would happen. The three are separated. sortOrder is display position. isPrimary is a stored choice with a "Make primary" button that says what it does, and an invariant (ensurePrimary) closing the two ways the flag can now be lost — deleting the account that held it, and the very first account, which has nothing to inherit from. colorIndex is handed out at creation from the lowest free slot and never moves, which keeps the "first eight accounts are guaranteed different" property sortOrder was chosen for without inheriting its habit of changing when the list does. The migration backfills colour from sort_order, so no existing install sees its dots move on deploy. The paths that create accounts without AccountCreator — OAuth linking, config restore, the e2e seeder — now state the primary too, or a user whose only account is an OAuth one would have none. Backups carry colorIndex, falling back to sortOrder so an older file restores the colours it actually had. **CSRF.** The reorder endpoint had none, with a comment explaining why: @stimulus-components/sortable builds its own multipart body and offers nowhere to put a token. The rules list had already dropped that wrapper for exactly this reason. Accounts now do the same — sortablejs in a controller of our own, the whole order as JSON with an X-CSRF-Token header, which also makes the write idempotent. The wrapper is unregistered and out of the importmap rather than left for the next list to reach for, and StateChangingPostsNeedCsrfTest covers both reorder routes and the new primary switch. **The dot.** It was never a status light — it is the account's identity colour, the same tone its rows wear in the unified lists. But it read sortOrder, and it had no title, aria-label or legend of any kind, so two coloured circles could not be interpreted at all and the obvious reading was status. It now derives from the account, and says out loud which account it marks; the one piece of state it carries, "switched off", is named too. Account health remains the place status is reported, and the two no longer appear to contradict each other because only one of them is claiming to answer that question. **Badges.** The rule was right and is unchanged: Trash and Drafts count the total, everything else counts unread. What was wrong is that saying so relied on colour alone — both were the same filled pill in the same place, one red and one a faint grey, and a bare number in a pill is a bare number. The two kinds now differ in shape as well as tone (filled pill vs outlined tally) and both carry an accessible name, since to a screen reader they were both simply the word "five". The names are patched alongside the numbers on every sync. **Duplicate ids.** The drawer and the desktop rail both render the sidebar into one document, so every account frame and expand toggle existed twice under one id — invalid HTML, and Turbo resolves frame targets by id. Each sidebar's ids carry its own suffix, and the frame id travels with the request so the reply wears the id of the frame that asked. **"ich, ich".** ThreadParticipants de-duplicated by address, and an account owns several — both resolved to "me". Collapsed by name, which also fixes a correspondent writing from two of their own addresses, and lets a self-written thread fall back to its recipients as it was meant to. **Untranslated German.** Settings → Labels printed the English creation name of system labels while the sidebar beside it printed translations — the two lists disagreeing on screen at the same time. One |label_name filter now answers for both, going to the sidebar's own key rather than adding a parallel table. And the German Updates tab is "Benachrichtigungen". **Theme preset contrast.** Each label was painted in the palette of the theme it names: the tile carried data-theme, and every theme block in app.css is selected by exactly that attribute, so the tile redeclared the whole palette onto itself. The tiles say data-theme-name now and the blocks are :root-scoped, so neither half can come back alone. Swatches still preview their own colours, which is their job. Measured after: worst label 7.58:1, against 1.22:1 for "Nord" in Solar before. phpunit 3454 tests / 8957 assertions green; composer stan [OK] with no baseline growth; 46 e2e across accounts, appearance, filters and labels green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>