Skip to content

v0.101.0 — Time Machine: history UI + Error messaging: productive actions

Shipped 2026-09-20 — built on version/0.101.0, twenty-seven commits, merged to main as 6453cfa0, tagged v0.101.0. The eleventh of the releases the near-term roadmap sequences from the August 2026 heuristic evaluation. Two roadmap pages ride together — Time Machine — history UI (part two of the Time Machine, on v0.100.0's /api/history) and Error messaging — productive actions (part two of error messaging) — with a third section of play-chrome and navigation items queued against the release. The help is History and Restore.

The History panel

One surface for every versioned entity, reached through one icon (the tabler restore; the tooltip says History: see and restore previous states — "Time Machine" is the internal name and appears nowhere a user reads):

  • Where (V.1) — the sheet header left of Share, on both routes; the campaign edit page as a secondary button left of Done; the Edit Campaign sidebar in /play, which gained a header row with History and an icon form of Share; the map palette right of Undo/Redo in all three renders; the token drawer, which opens its map's. useEntityHistory is one hook for the panel and for the map's Undo/Redo, so they cannot disagree about the head; history:commit pushes keep it live, and now carry the author's username.
  • The list — each commit's one-line summary, who (a username), when (relativeTime), the reason as a person reads it, a current badge on the state commit, a timeline badge when off main; paged.
  • The diff (V.2) — select one commit for the difference from it to the current state (older → newer whichever side the state is on, since after an Undo the state can be older than a listed commit); select two for the pair. Grouped by the entity's sections (a character's by the sheet's; a map's by Layers / Tokens / Drawings / Walls / Fog / Objects / Permissions), one item per row, YAML-style before/after with the unchanged fields folded to a count; a token that only moved is moved (2, 0) → (3, 1).
  • Blame (V.3) — GET …/blame walks back from the state commit through each commit's stored change list, first writer wins. On the sheet, every section title; in the token drawer, every label: hover on desktop, long-press on touch — Last changed by alice · 2 hours ago · 1 token moved.
  • Restore (V.4) — with one commit selected, Restore this version for whoever holds Edit, through the ConfirmDestructive pattern with the diff from the current state to the target under What changes; the server announces and every client's view moves.
  • Cascade (V.5) — GET …/cascade?commit=: the linked entities (a campaign's maps and characters; a map's the characters its tokens project; a character's the maps its tokens stand on) whose state at the target's time differs from now, each with its own summary and a checkbox in the confirm — unchecked by default, disabled without Edit; the checked set travels as cascade, each to the commit it was at then.
  • Timelines (V.6) — the switcher above the list: every timeline with its head, the current one marked, a filter to one, rename for an Editor (PATCH …/timelines/:id, no commit — a name is not a state). Restore timelines are named "restore N", not by commit id. No merge.
  • Versions (V.7) — a character's tags at the top of its panel: Tag this version names the current state (or the selected commit), a tag's row selects its commit so the ordinary Restore activates it, × removes the name. CharacterPage's old Versions control is deleted.
  • Undelete (V.8) — /undelete lists the deletion commits for campaigns, maps and characters (GET /api/history/deleted, now carrying each entity's name, deletion time, purge countdown and restorability), with who deleted each and a History icon that opens the entity's panel while deleted; Restore is the undelete, itself a commit. Tokens, which have no history of their own, still come from the soft-delete index.
  • Help (V.9) — History and Restore, with the evaluation's map/token example, linked from the index, Maps, Character Sheets, Keyboard Shortcuts and Your Data.

Two things found live and fixed here: a map's thumbnail re-capture (snapshotUrl, a cache-busted URL) had been a commit between every real change — it is now volatile to history, and writeSnapshot never writes a volatile column back whatever the snapshot's age; and a pushed commit row read "the platform" until refetch, because the push carried no username.

The productive actions

The error panel's second half, on v0.92.0's standard and v0.96.0's permissions UI. The help is Error Messages.

  • Request access (P.1) — a request is a row against (entity_type, entity_id) for every type the Gatekeeper guards (P-D1): access_requests (pg 0039), POST /api/permissions/:type/:id/request { role }, the caller's own GET …/request, the pending list GET …/requests for whoever may share, and …/requests/:id/grant (through setGrant, up to the granter's own level) or /deny. Two message kinds: access-request to everyone who can share (loud — an answer is owed), access-request-answered to the requester. The Gatekeeper's 403 now names the entity it refused, the client reads it off the wire, and a forbidden error with an entity carries Request access — one modal, mounted in App, where the requester picks Viewer / Collaborator / Editor. The character page, the sheet in /play and the sheet modal render their 403 through UserErrorPanel, so the button is there. Answered in the one sharing UI: pending requests at the top of the permissions editor's Grants tab.
  • Who can help (P.2) — GET …/helpers?role=: a member of the campaign the entity belongs to gets the usernames of members who hold share and may grant the level; anyone else gets nothing but the interim sentence. The route decides membership itself, since the caller cannot read the entity. A test holds that the non-member branch reads no helper.
  • The pending state (P.3) — Access requested 2 hours ago — waiting for an answer instead of the button; a declined request says so and offers to ask again; the answer over the socket invalidates the entity, so a grant reloads it and the panel goes with the error.
  • Roll back (P.4) — a system fault against a campaign, map or character the viewer can edit offers the History panel with the previous version pre-selected. The three boundaries in MainContent name what they render, so a render that throws is a fault against that entity.
  • Preview previous version (P.5) — the prior snapshot rendered without writing (a character as the real sheet, read-only; a map or campaign as its contents and the diff), inside its own boundary; a successful render offers the one-click rollback and files reportProblem() with the offending commit's diff.

Found and fixed on the way: the modal mounted outside the auth provider crashed the app at boot (moved inside); a draft of the 0039 migration had run on the dev database, so the migration is idempotent.

Play chrome and navigation

Seven items queued against the release, not roadmap tasks:

  • The play bar's columns (N.1) — each side column is the minimum its icons need (198 px left, 162 px right), never the sidebar under it; the centre takes the rest, and the palette neither contracts nor moves when a sidebar opens.
  • The palette in the bar (N.2) — no shadow, no border: a row of the bar, not a card floating on it (the theme's Paper default had been winning over the stylesheet).
  • The nub's popup (N.3) — Edit Current Map opens the map editor in the sidebar; Invite Players wears the Share icon (share-2, the verb).
  • Connected (N.4) — gone from the global nav; the nub on /play is its home.
  • Help, Notifications and Profile (N.5) — routes, not popups: each rail row goes to its page (on /play, the sidebar page as before), and the active one shows its entries beneath it as the second-level nav — the same options the menus held. The bell keeps its badge, its ring and its hover peek. ProfileMenu is now the avatar plus the entries.
  • Edit Campaign's side nav (N.6, N.7) — sixteen sections in four groups (Campaign — with a new Permissions section —, Tokens, Maps, Characters), and the column scrolls on its own in a short sidebar.

Guards and verification

Harness tests under services/history/ for the blame, cascade, tags, soft-delete and history APIs and for the access-request routes; contract tests under UI/HistoryPanel/, UI/RequestAccess/, SystemFault/, SectionNav/ and the rail; the versions, undelete, PlayBar, rail, profile and bell guards re-anchored. Verified live in Chrome against the dev server: every History entry point and the panel's list, diff, restore, cascade, timelines, tags and undelete; the forbidden page's Request access, the modal, the pending state and the non-member wording; the play bar's fixed palette, the bare palette, the nub's two buttons, the rail's three routes with their entries, and the grouped, scrolling section column.