Skip to content

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/:id and 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 from CampaignCard, CharacterCard and a NewCharacterFlow extracted 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/new is 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 layerId is omitted or default:<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 .cat imports, 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-chrome while 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 ExpandCaret for 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 is users.username in 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 seen systemTraits.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:deleted carrying its mapId, and a guard on any other io.emit in a route.