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.useEntityHistoryis one hook for the panel and for the map's Undo/Redo, so they cannot disagree about the head;history:commitpushes 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 offmain; 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 …/blamewalks 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
ConfirmDestructivepattern 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 ascascade, 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) —
/undeletelists 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 ownGET …/request, the pending listGET …/requestsfor whoever may share, and…/requests/:id/grant(throughsetGrant, up to the granter's own level) or/deny. Two message kinds:access-requestto everyone who can share (loud — an answer is owed),access-request-answeredto the requester. The Gatekeeper's 403 now names the entity it refused, the client reads it off the wire, and aforbiddenerror with an entity carries Request access — one modal, mounted in App, where the requester picks Viewer / Collaborator / Editor. The character page, the sheet in/playand the sheet modal render their 403 throughUserErrorPanel, 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 holdshareand 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
MainContentname 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
/playis 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.ProfileMenuis 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.