Skip to content

v0.88.0 — UX and playtest bugs

Shipped 2026-09-16, with v0.88.1 behind it for the red CI run that stopped v0.88.0 deploying. v0.89.0 was cut from it rather than sharing its version number, because a release-sized project should not arrive as a footnote to a playtest; its notes stayed inline in CHANGELOG.md until v0.90.0 opened, which is why this page exists.

UX

  • [x] Ability scores are clickable in the character sheet (5e/s8) but have no hover state to confirm this. (StatTile: theme hover + focus ring on interactive tiles; "Roll a STR check" tooltip)

  • [x] Character sheet: the sectional info icon for validation issues should change to info-octagon (the info severity, in the section marker, the panel alert and the header button alike)

  • [x] S8 Character sheet: the button to see feat/power detail modal should change to info-circle (trait row and per-action (i) alike; v0.77.0's document icon retired)

  • [x] Need a measure tool on the palette (press-drag or click-move-click; Space anchors a waypoint; measures the way a token moves, in squares, under the campaign diagonal rule; mouse + touch)

  • [x] Select monster from system modal is behind character context in terms of z-index; all modals should be above all sidebars (theme default: Modal/Drawer → MODAL, Combobox/Popover → MODAL_DROPDOWN, both skins; 23 modals had no band of their own)

  • [x] S8 power actions should link some sort of button in the chat output to see the power action's detail modal (the action doc header carries actionId; the chat frame offers the sheet's (i) when the actor token on this map still has the action)

  • [x] Create Token > Background Image: change the flow so that this immediately creates a blank token on the background and auto-opens the Edit Artwork Modal, rather than invoking the old Edit Token modal. (store-driven Token Artwork modal; the blank token is sized to its first image — in the modal or the settings drawer)

  • [x] Edge case: while non-GMs don't have enough Palette tools to trigger this, in mobile for GM users, in portrait mode, the Paletta doesn't have enough room for all tools. Collapse the tools into an overflow menu or similar UI element, with the following order determining which tools are last to be rolled up, from most to least important:

    • Create, Drawing, Current Layer, Undo/Redo (as a pair), all the rest
    • (the row is planned against the viewport width — utils/paletteOverflow.ts, pure and ranked as above, the rest going rightmost first — and whatever doesn't fit becomes a ⋯ menu at the right end whose items do what the buttons did; the token-search popover re-anchors on ⋯ so / still works; on a 390px phone the GM keeps Create/Delete/Draw/Measure/Fog + layer + undo/redo, with VBL, Edit Grid and Search rolled up)
  • [x] Power actions and 5e spells: the ⋯ menu gets Duplicate below View Action — a fully editable custom action whose initial values match, filed under Other Actions (for diagnosing a derived action by changing it and running it again)

Bugs

  • [x] Load /play for s8 character > impersonate a character with edit access > observe character sheet looks normal > enter build mode > add new power (Ignis) > expected: Ignis appears with an expando to show the actions in Ignis. observed: Ignis appears as an actionless power (despite the info popup showing its actions), requires hard reload to see power actions. (the POST answers before derivation writes powers[].actions, the fan-out told the linked tokens and nothing told the sheet, which reads the character; the server now announces the late write — character:changed to the owner's, grantees' and linked campaigns' rooms — and the client refetches the character query)
  • [x] When the message "you have no tokens on this map. Add one of your characters:" appears, there is an Insert button. However, it doesn't do anything, and gives no sign of being clickable. There is also a View button that does nothing. (the desktop bar wrapper is pointer-events: none so the map stays clickable through it, and the seating panel never took them back — the same trap v0.29.2 removed a selector over; it and the "Select your character" paper do now)
  • [x] When impersonating character X, and you delete the last token of character X on the map, un-impersonate that character. (every reader fell back to the store's snapshot when the row was gone, so you kept speaking as a ghost; useImpersonationFollowsTokens watches the loaded list — the token gone but a sibling of the same character still here switches to it, the last one gone un-impersonates; also covers a delete by someone else and a restored impersonation onto a map that no longer has the token)
  • [x] When you have View access to a token, you should be able to impersonate that token, even if you can't execute their actions or change anything. Instead, it says 'you do not have access to impersonate X'.
    • Read-only: you can select the character in the character context, but cannot execute actions (including things like clicking ability scores to roll them, using the quick actions in the hover UI, or chatting as the character in the chat log).
    • Play: you can select them and use their actions / chat as them, etc.
    • Edit: you can enter Build Mode.
    • (GET /tokens/impersonatable admits read grants and reports a levels map beside the ids — owner/GM/admin edit, token allow control, a character grant its own level, highest wins; useImpersonatedTokenLevel reads it off the same set the guard enforces from; a read impersonation is refused with one sentence in the impersonated executor (every action surface funnels through it), in both contexts' roll and chat handlers, and the chat-log roll table posts as the user; the sheet's writes and Build mode were already withheld by _level)
  • [x] target.requestRoll() never prompted the requester when they were the GM (or a character editor) — the relay left the requester out of the co-recipients "so they don't re-prompt themselves", so a GM running an action as one token against a Kazimir whose token row a player had placed sat at "waiting for dice…" for the whole five-minute timeout. Whoever may answer is now a recipient, requester included (the first answer closes every copy); and the bound character's OWNER is prompted unconditionally, like the token's owner, whoever placed the row.
  • [x] In the system 8 character sheet, in the Equipment modal, Filters button doesn't do anything. Likely the filters popup is behind the modal in z-index terms. (Also: this should migrate to a slide-over treatment similar to the feat/power modals) — confirmed (popover at Mantine's 300 behind the modal at 2000); migrated to FilterSlideOver + FilterFacet + ActiveFilterTags like the feat/power pickers
  • [x] No way to add custom feats or powers in system 8. (the pickers' "Create Custom Feat / Power" buttons now wired to a name + brief form; source: 'custom')
  • [x] Can't see timestamps in chat log anymore. On mouseover (of a part of the chat message that doesn't have its own tooltip), the timestamp should appear as a tooltip. (the action-doc frame — most messages since v0.73.0 — never had the bubble's timestamp wrapper; it does now, yielding over dice, buttons, links and data-own-tooltip)
  • [x] Users 1 and 2 go to map X. User 1 makes new map Y. User 2 clicks map selector in top nav, doesn't see map Y. The caret dropdown selector should open the current cached list of maps but also asynchronously check for new maps. (a created map is broadcast by no socket event; both map dropdowns now invalidate the maps query on open — cached list at once, refetch behind it)
  • [x] Create Token > Existing Character > doesn't show non-owned tokens even if you have play/edit access. This modal should show all tokens in the campaign that the current user has play or edit access to, or owns. (my-characters now adds characters linked to the campaign held at control/edit, and every linked character for the GM; soft-deleted excluded; rows say "Shared with you")
  • [x] Create Token > New Character > change name to Sloan. Name in character sheet is Sloan. But the token on the map and in initiative is still New Character. Changing the token property 'name' does work, but changing the character name should auto-propagate to the token name. (the server fan-out now writes the character's name onto linked tokens — R-b's closed list never held it; the initiative store re-syncs its snapshotted names on any token rename)
  • [x] Targeting Mode on mobile doesn't work due to failure to capture touch events. Moving a finger around the screen should have the same behavior as mousing around on desktop. (the finger now feeds the targeting cursor and hovered token from touchstart and every touchmove; a tap still confirms)
  • [x] System bug (to be moved): Wall of Thorns erroneously labeled single target, not wall — not platform: already fixed in the system8 repo's working tree on version/8.18.0 (target: none, area: wall 1/level; HEAD b34984a still says target: single); reaches players with the next system8 docs + module publish
  • [x] S8 character sheet: "94 proficiency points spent of 16 available" completely inaccurate. (root cause: two creation flows wrote the base component as Base while the schema declares base, so the ten counted as proficiency and the wizard's base: 16 landed beside it; both flows use the template now, component identity is case-insensitive, and every write heals the stored spelling — a damaged character reads the score it chose and is fixed by its next edit or a wizard re-run)
    • Test 1: new character, no stats assigned. Thus, all stats are 10. It says '60 points spent', when in fact 0 have been spent. Proficiency points are measured as points added on top of the base ability score.
    • Test 2: new character, ran stat wizard. Assigned stats in order: 16 14 12 12 10 10. Expected stats: 16 14 12 12 10 10 with 0 points spent. Observed: 26 24 22 22 20 20 with 60 points spent.
      • Correct behavior: setting a 16, 14, etc should not add 16 to the base of 10, but replace it. Also, if an ability score is assigned 16 at character creation (for instance), it should be stored as { base: 16, proficiency: 0 }. If the user adds 5 proficiency points, it would be { base: 16, proficiency: 5 }. The +/- buttons modify the proficiency bonus, and the ability score wizard changes the base number.
  • [x] User X has play access to character 1. User X has character 1 open in the character context / detail page. User Y, owner of character 1, upgrades user X's access to Edit. User X has to reload the app to see the change. Change this flow to use a websocket push to update User X's access in real-time. (every grant write announces from utils/characterPermissions; index.ts pushes characterAccess:changed to the grantee's room; the client refetches the character + lists)
  • [x] User added a custom action to their character in the context. The action worked as expected. When clicking 'edit' on that action afterward, the Edit Action modal shows no code in the action editor, despite the action working as expected. Hard reload doesn't fix. (the map-paint token list carries action STUBS without code; execution hydrated its own, the Character Context's editor never did — useFullTokenActions hydrates it, and the sheet's trait action pool too)
  • [x] RequestRoll() seems to be doing math wrong (e.g. 1d20+dex is adding the dex score to 1d20 instead of the mod). To do: confirm this, as other bugs prevent confirmation since I can't see action code. — CONFIRMED, and wider: DiceRoller has taken prop types since v0.45.0 and no roll path outside the sheet's tile passed any (action roll(), requestRoll answers, the GM's overdue ask all added the score); a roll prop-type registry published from the app shell fixes every path at once
  • [x] In streaming chat message it says 'character rolled 1d20+dex=result' from a requestRoll() but there's no way to introspect the roll. When it says 'characterName rolled 1d20+dex=22', there should be a tooltip showing the roll and the math. (the answer now carries the dice — ActionDocAskStep.breakdown — and the answered line renders the same rich roll row a roll() gets, hover included; older answers keep the bare total)
  • [x] Tons of issues with Roll Initiative--duplicating some tokens, skipping others, multiple tokens marked as current turn when it can only be one token's turn at a time. Simple: one initiative roll per token on the map. (Another bug: End Turn button doesn't update current token in mini initiative) — every one was a second entry for one tokenId (the strip keys chips by tokenId), fed by other maps' tokens leaking into the list; the store now enforces one entry per token and a clamped turn index everywhere entries are written, Roll Initiative de-dupes, and a player's End Turn is relayed to the GM (initiative:endTurn) instead of being overwritten by the GM's next broadcast
  • [x] Initiative shouldn't cross maps; when changing to a new map, initiative shouldn't be active anymore. Switching back to the old map would restore it. Store initiative status and number on a per-map basis. (the store was per map already; the WIRE was not — every payload now carries the GM's mapId, a receiver files another map's payload under mapStates instead of showing it, and a late joiner asks for the map it is on)
  • [x] To do: reproduce this. User 1 makes map X, user 2 joins. Tokens created on map X. User 2 makes map Y. User sees map Y without invitation, and tokens from both maps combined. — reproduced by reading: the server emits every token event to every socket and the client appended whatever arrived to the current map's cache, so a token created on map Y joined map X's list (and its initiative); handlers now take only this map's tokens, add on a move in, drop on a move out. "Sees map Y without invitation" is by design — campaign members see the campaign's maps; map:invite is a summons to a map, not a gate on its existence

Viewstate Invalidation

Currently, there are a large number of interactions that can update the viewstate in one browser session (either a different user or the same user in another tab) when triggered from another. Most of these rely on explicit WebSocket pushes that force-update the connected clients, but there are plenty of edge cases where no such update exists, and other tabs/users must hard reload.

Claude: go through the app and create an inventory of such use cases, along with whether that use case has a WebSocket push or not. My recommended refactor is as follows:

  • Whenever an action might change the view state for other campaign users (e.g. character change, token change, chat message, permissions change, etc):
    • Send a message with the changed data (equivalent of a PATCH)
    • In that message, include a tree of invalidated data

Example:

  • Chat message: as today, contains chatting user ID, chat message, timestamp, campaign (i.e. channel)
    • Also contains the equivalent of "the chat log for campaign #123 may be invalid...here is the most recent chat message hash" (implying that a chat log for campaign X has a hash uniquely identifying a complete state; thus, if user A has messages 1, 2, and 5, and user B had messages 1-5, 5 being the one that was just posted over WS, the user B wouldn't match the user A hash because of 3/4 being missing so they'd know to refresh via REST API)
  • Permissions change:
    • Main message: user X granted permission Y to user Z at timestamp A for asset B
    • Invalidation message: permissions for user Z and asset B may be invalid

Clients can have a rolling refresh window to check the REST API for fresher viewstate based on their in-memory viewstate cache, which retains hashes and timestamps. Hashes definitely invalidate parts of the viewstate, timestamps suggest when a refresh would be a good idea (e.g. ChatLog is 5 minutes old, let's check it anyway).

Please examine and make a recommendation for this implementation.

  • [x] Inventory and recommendation written up as a roadmap page: Viewstate invalidation (proposed). Short version: the shape is right; use per-collection revisions rather than content hashes, add one generic viewstate:changed hint beneath the typed events rather than instead of them, and scope the audience by room (the token/drawing/chat events today go to every socket). Group D of the inventory — ~40 write routes with no push at all — is what the release would close.