Files
pl_mail/tests/e2e
Paul LütznerandClaude Opus 5 ab13ba242a fix(accounts,sidebar): ordering a list stops deciding who you are
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>
2026-08-13 13:01:40 +02:00
..