v0.108.0 — Polish
Shipped 2026-09-30 — built on version/0.108.0; the merge hash and tag are
in the changelog. Followed by the hotfix v0.108.1.
Playtesting the System 8 campaign and character creation process.
Wiki
Design and the four releases: the wiki. This release carries parts one and two, foundations and in the app; hosting (v0.109.0) and across roots (v0.110.0) follow, ahead of PWA and the System 8 sheet.
- [x] WF.1 — the shared model: root kinds and ids, page paths, the cross-root link syntax
- [x] WF.2 — the
wiki_rootsandwiki_pagestables - [x] WF.3 — access: a root follows its campaign, character or user
- [x] WF.4 — the
/api/wikiroutes - [x] WF.5 — history: every save, delete and rename a Time Machine commit
- [x] WF.6 — materialize a root into a working copy (injected metadata, rewritten links,
.mdonly) - [x] WF.7 — build: ursa as a child process with a time limit
- [x] WF.8 — publish: ordered upload, deletions,
status.json,index.json,abstract.json - [x] WF.9 — the per-root coalescing queue and
wiki:buildstatus events - [x] WF.10 — the local wiki store and
{rootId}.wiki.localhostin the dev server
In the app:
- [x] WA.1 — the bridge in every page, and
embed.htmlfor document mode - [x] WA.2 — the abstract and a page at a commit, through the API
- [x] WA.3 —
WikiFrame, the sandboxed viewer - [x] WA.4 — build status:
useWikiRoot,wiki:build, the badge - [x] WA.5 — the editor: rich and raw, save against a base version, conflicts
- [x] WA.6 — the metadata panel
- [x] WA.7 — images in the editor
- [x] WA.8 — the campaign page section and
/campaigns/:slug/wiki - [x] WA.9 — the
/playWiki icon and sidebar on the current map's page - [x] WA.10 — the character abstract and Read more
- [x] WA.11 — history: versions, the text of one, restore
- [x] WA.12 — the guard: no wiki HTML outside
WikiFrame - [x] WA.13 — the full-page wiki's file tree: collapsible, New page / New folder, following the frame
- [x] WA.14 — new files are a document (.md), stylesheet (.css) or script (.js) — W-D9
The original requirements:
Since we already have a markdown editor, a storage medium for static files, a permissioning system, and a versioning system, we have all we need for a wiki.
- Wiki articles can be arbitrarily created by any user. With Editor access, an article can be associated with a Campaign, Map, or Character.
- The wiki is experienced either in a sidebar or full-page view.
- Ursa is used to render the wiki to HTML, storing the static files as MD or MDX.
- Wikiroots exist for:
- Each user
- Each campaign
- Each character
- Ursa's menuing system can be used as-is, to create auto-indexed and/or arbitrary menus.
- Views of the wiki:
- Campaign:
- On the campaign detail page, after the Maps and Characters sections, show a wiki view with a button to view the wiki on a standalone page.
- On the /play route, show an icon in the top left (amongst System, Library, Edit Map, etc) to launch the wiki sidebar. Default landing page is the current map's page.
- If that page doesn't exist, land on a page explaining the current map has no page, with a button to create that page (if user has Editor for the campaign or map), and a button to navigate to the campaign's default landing page within the sidebar.
- Character:
- In either the Character Context or full character sheet:
- Right after the Meta section (name, class, level, etc), show a section with the first paragraph of wiki content (typically a high-level abstract of the character), truncated to a short paragraph, with a "Read More" button to expand to the full page. This content should strip any images--it's likely to contain a portrait of the character, which already exists in this UI.
- The "Read More" button should open another section within the left sidebar showing the character's wiki page.
- Right after the Meta section (name, class, level, etc), show a section with the first paragraph of wiki content (typically a high-level abstract of the character), truncated to a short paragraph, with a "Read More" button to expand to the full page. This content should strip any images--it's likely to contain a portrait of the character, which already exists in this UI.
- In either the Character Context or full character sheet:
- Whether sidebar or full view, wiki panes have an Edit button to open the wiki editor. This should be a full featured in-browser Markdown editor, ideally with a way to swap between rich text and raw Markdown.
- Campaign:
- All edits to a wiki markdown page are versioned using the same versioning system as the characters, campaigns, etc; the user making the change should be noted as well as the timestamp.
- When showing the full wiki experience, include the top menu and widgets. In sidebars, show only the rendered document. (Either need an Ursa change or a stylesheet to hide all the stuff, but the former is preferable since there's also JS we don't want to run)
External Wiki
Allow users to specify external wiki URLs for their campaigns and characters. Rather than the default (built-in wiki), users can provide a custom URL to a wiki hosted elsewhere. Requirements:
- User can view the wiki in the same UI as the built-in wiki
- If the wiki is Ursa, use _directory.json to build the file tree for the tree view
- Roadmap feature: add editing support (external bucket, Dropbox, etc). Not in this release.
Tasks (in the app → External wikis):
- [x] EW.1 —
wiki_roots.external_url/external_links; the shared URL, link-key and_directory.jsonrules - [x] EW.2 — the API: set the source, link a page, read an ursa site's tree behind an SSRF fence
- [x] EW.3 — the app: Source and Link dialogs, the external panel (a link per campaign, map and character; no fallback but a map's Go to the campaign page), the tree, the character summary
- [x] EW.4 — require ursa 0.102.0 in the platform (
packages/server-core, andapps/help) — shipped in v0.108.1 once 0.102.0 reached npm
Bugs / Enhancements
- [x] Clicking 'Build mode' to unlock the whole character in the character detail page causes an error:
ErrorBoundary caught an error in CharacterMain: Error: [@mantine/core] Duplicate options are not supported. Option with value "597b4ea0-b8e8-4200-890c-8738036d71d9" was provided more than once
at recoverFromConcurrentError (chunk-H3OPZKO6.js?v=8f236725:18736:28) {componentStack: '\n at OptionsDropdown (http://localhost:5173/nod…components/ErrorBoundary/ErrorBoundary.tsx:173:8)'} Error Component Stack
at ErrorBoundary (ErrorBoundary.tsx:29:8)
at MainContent (MainContent.tsx:185:23)
at ContextRouteSync (AppRouter.tsx:40:3)
at ErrorBoundary (ErrorBoundary.tsx:29:8)
- [x] In the System 8 'Generate Ability Score' wizard, you can drag an unused ability score (e.g. 16) to a target (e.g. Strength), but once there, it can't be moved. Allow users to move the scores freely; if replacing an existing score, it will kick that score back to the pool. Only when "Apply" is clicked will the scores be finalized.
- Done: a placed score drags to another ability (the one it replaces goes back to the pool) or back to the pool; nothing is saved until Apply.
- [x] In the ability score edit windows (where proficiency bonus can be modified), debounce arrow keys. Rapidly pressing up or down causes race conditions. Instead, wait for the surge of key events to die down before committing the change.
- Done: steps show at once and save once after 400 ms of quiet (flushed on blur/unmount); +/− clicks too.
- [x] If a System 8 character adds a component to ability score such as 'Ancestry', the math incorrectly includes the points in that category to the proficiency point calculation. While this should trigger the 'unrecognized bonus to ability scores: Ancestry' warning, it should not modify the proficieny point allocation.
- Done: only the components the module's schema declares count as proficiency points; Ancestry still adds to the score and still warns.
- [x] Import Existing Party: fails with
GET https://api.purplecat.games/api/characters/1d237e60-51ec-40be-9283-7c4883bc7099 404. This is a character I don't own, but I do have Editor for, so I should be able to import it. Also, even if I have no access at all, that particular character shouldn't import, but the Import Party process shouldn't entirely fail as a result.- Done: the 404 was a party member in its owner's trash — party detail listed soft-deleted members. They are no longer listed; Import Party skips any member it cannot load (404/403) and names it in a notice. Editor access already worked (covered by a route test).
- [x] When dragging an image directly onto the map, a token is added with that image as its artwork. However, there are extra steps in the process. Simplify the process as follows:
- If the default token rendering mode on the current layer is Object, just create the token right away; don't pop up the Edit Token modal, but rather skip to the result if the user pressed 'Add Token' in that modal.
- If the default token rendering mode on the current layer is Token, pop up a variant of the Add to Map modal, but without the Existing Party, Generate Encounter, and Generate Map options. Selecting any other option follows the normal flow for that option, except that the dropped artwork is automatically applied to the token.
- Done: object-mode layers (Objects, Background, custom Object layers) create the token at once; Token layers open the chooser's drop variant, and the chosen flow gets the image as its artwork.
- [x] Changing the render mode from Token to Background in the Impersonated Token Context doesn't actually change the render mode. Moving the token to Objects (or a similar layer) does change it, in that the user can now freely resize the token.
- Done: the change was saved, but the token layer sized and selected by the layer's mode alone; the token's own mode now wins. Impersonated object-mode tokens get resize handles.
- [x] Avatars in chat don't use the same tokenization (pan/zoom) settings as the map.
- Done: a chat avatar matching a token on the current map draws through TokenThumbnail with that token's framing; others stay plain.