v0.90.0 — Heuristic evaluation quick fixes
Shipped 2026-09-19 — merged to main as f6f09b9f, tagged v0.90.0. Built on version/0.90.0, fifteen commits, one per task. The first of the
fifteen releases ROADMAP.md sequences from the August
2026 heuristic evaluation; the roadmap page is
heuristic-evaluation-quick-fixes
and the source document is
the evaluation itself.
The evaluation's miscellaneous findings that needed no new model behind them, plus the two items v0.89.0 deferred here by name, plus one item pulled forward from the permissions project because it was a live PII leak. Two of the fifteen reverse v0.42.0 decisions on purpose.
Routing and lists
- Character links go to
/characters/:id(Q.1). v0.42.0 put the sheet under/campaign/:id/characters/:idand the app linked one character two ways depending on which list you came from.characterPath(id)is the only author of a sheet URL and takes no campaign; the old form redirects, through a pure function rather than the two-list wait v0.42.0 needed. What the URL used to carry, the character supplies: the sheet and the top-nav breadcrumb read the character's home campaign (its first joined one) for reference data and the crumb. - The campaign strips always show prev / next / Show all (Q.2), as a control row under the strip — v0.75.0 removed the in-strip chevrons because they cost the cards a fifth of their width on a phone, and that reason still holds. Each chevron disables at its own extreme; Show all leads to the list even when the strip holds everything.
/is a home page (Q.3): campaigns and characters strips with New buttons, built fromCampaignCard,CharacterCardand aNewCharacterFlowextracted from the pages that already had them.- An empty list says "you have none" (Q.4) and hides the search it cannot use; a page search that found nothing is rendered as the filter result it is, toolbar included — the old inverse of the finding.
- One New Character modal (Q.5), from the campaign page too, pre-populated
with the campaign and its system; removing the campaign is allowed and named.
/campaign/:id/characters/newis the campaign page with the modal open, and the name-only page is gone.
Map layers
- Default token / object / drawing / background layers per map (Q.6), in a
Defaults section of Edit Layers. The server resolves them: a POST whose
layerIdis omitted ordefault:<kind>lands on the map's default of that kind; an explicit id — the palette's current layer — is honoured as given. Twelve client creation paths ask by reference, so.catimports, module routes and older clients behave too. - Drawing mode switches to the default drawing layer (Q.7), not the nearest drawable one, and says which layer is current — on the switch and on the switch back.
Mobile
- Notifications sit above the bar (Q.8), and above the footer everywhere —
the stack was under both. The bar publishes
--vtt-bottom-chromewhile mounted; its 56px is one constant now. - No hover UI while a token is dragged (Q.9) — a finger crossing other tokens no longer "mouseovers" them; the same flag quiets a mouse drag.
- "Your turn!" once, on turn start (Q.10), keyed on the turn's identity rather than on a token object that changes every square.
Character sheet
- Group by Category collapses like Group by Power (Q.11), through one
ExpandCaretfor both.
Privacy
- Never another user's email or full name (Q.12). One rule,
publicDisplayName— alias, username, given name,Player-xxxx— on both sides: the server's rosters and notifications, the client's presence, cursors, chat sender (which was the viewer's email), roll requests, dice bag and drag labels. Two guards allow-list the files that may still read those fields, with the reason beside each. The durable form isusers.usernamein v0.93.0.
Carried from v0.89.0
- Factions and system traits announce (Q.13): their hooks are React Query
caches now, three entities join the viewstate union, and a
system:<id>room carries the system editor's changes. The coverage guard's inventory had never seensystemTraits.ts— its routes are indented — and does now. - The group-A pushes go to the campaign room (Q.14), not every socket: a
synchronous map → campaign cache that over-broadcasts on a miss and warms
itself,
token:deletedcarrying itsmapId, and a guard on any otherio.emitin a route.