Keep game-system state per page and let systems add panels to NPC pages - #1051
Merged
Merged
Conversation
A game-system package needs somewhere to keep per-page state (first user:
a Draw Steel negotiation tracker) and a way to put a widget on NPC pages
without Chronicle shipping Go for it.
systemstate plugin: entity_system_state table keyed (entity, system, key)
with a public half and a gm half. GET is open to anyone who can view the
page; the gm half is returned only to the owner and DM-access members
(CanAuthorDmOnly) and is absent otherwise. PUT is DM-team only and follows
the partial-update contract: an absent half keeps the stored half, a
present half replaces it, null is rejected rather than read as absent.
Entity/campaign pairing, the show-page visibility gate and the
system-enabled check are enforced; writes publish a GM-only
system_state.updated with ids only, which is not a change-feed type. The
Foundry module reads the same document through a read-only sync route with
the same gm rule.
entity_panels: manifests may list {widget, applies_to: "npc"}. The host
mounts the widget under the title of NPC-family pages (npcTypeIDs rule)
in campaigns with the system enabled, via a resolver wired in app so
entities stays system-agnostic. Panel widgets are left out of the layout
palette so they are not placed twice.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2N6zhGuT2DHzVoiFUMwv1
6 of 8 tasks
…-mu3i3i # Conflicts: # .ai/architecture.md # internal/plugins/syncapi/api_handler.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1044
Security implication: new per-page store with a GM half and a player half. The GM half reaches only the owner and members given DM access (web GET/PUT via
cc.CanAuthorDmOnly(), sync API via the DM-grant-promoted Owner role). Writes are DM team only. Every read runs the page's own view check (private or foreign-campaign page → 404). The WS message carries ids only and isRequiresDM.Consumer-verified: Draw Steel
widgets/negotiation-tracker.js(Chronicle-Draw-Steel, branchclaude/project-thread-mu3i3i) callsGET/PUT /campaigns/:cid/entities/:eid/system-state/drawsteel/negotiationand readsisGm,gmandpublic.Mockup: Sign-offs card
mock-npcs-foundry(approved). The panel's look on the real page is on the small-differences carddiff-npc-negotiation-panel(mockup running the real panel code). This PR waits on that card.What this changes
Before: Chronicle had nowhere to keep a game system's structured state for a page. Fields are scalar, GM-only filtering strips whole fields, and NPCs are often the core
npctype, which no system preset covers. A system widget reached an entity page only as a block the GM placed by hand, and that block wasn't told whether the viewer is the GM.After: a game system can keep JSON state per page in two halves, one for the GM and one for players. It can also name a widget in a new manifest key
entity_panels, and Chronicle then mounts that widget under the title of NPC-family pages, with a GM flag. The first user is Draw Steel's negotiation tracker.How:
internal/plugins/systemstate.001_entity_system_state(FK cascade to entities and campaigns). Routes are registered only when the plugin is healthy.PUTfollows the partial contract: an absent half is kept. A null, non-object or over-16 KiB half gets a 422. Writes are refused for a system not enabled in the campaign.system_state.updated, which is not in the change feed (pinned by test).GET /api/v1/campaigns/:id/entities/:entityID/system-state/:system/:key, for the Foundry mirror.entity_panelsininternal/systems/manifest.gois validated on load: the widget must be in the same manifest, andapplies_tomust benpc.internal/app/system_panels.goreusesnpcTypeIDs.entities/system_panels.templ) sits in the title block, and above a system page renderer when one replaces the layout.Why
#1016: the approved NPCs card puts a Draw Steel negotiation tracker on NPC pages, mirrored to Foundry.
Honest deviations:
nullhalf on PUT is rejected rather than cleared, because each half must be an object.{}clears it.internal/sanitize/sanitize_invariant_snapshot.txtgained one line for the new service.make test-js, golangci-lint (local binary too old). The migration was not run against a real MariaDB; CI's Fresh-DB Migration Replay covers that.Test plan
go build ./...,go veton touched packagesgo test $(go list ./... | grep -v /integration) -count=1: 54 packages ok, 0 failures. New tests cover service validation, the role split (player, scribe without DM access, members given DM access, owner), IDOR and private pages, the PUT gate, the sync split, the change-feed pin, the publisher, manifest validation, the panel resolver and thedata-is-gmmount.tools/check-plugin-isolation.sh,tools/check-migration-immutability.sh, page-scripts, widget-mounts, templ-packages checks passTenet self-check
internal/app.ai.md,docs/system-package-rendering.md(entity_panels + state API),docs/api/openapi.yaml, websocket, systems, entities and data-model docsGenerated by Claude Code