# Configuration Management Web Service

The optional configuration dashboard runs as a **separate Railway service**.
It has no BC connection and is independent from the bot process. The
workspace uses the configured room's effective stored map as a visual index
for locations and doors. Typed configuration forms support administration;
writes require explicit deployment opt-in and an authenticated `admin`.
Existing deployments remain read-only by default. The original door, shower,
and casino views remain available as read-only summaries. Appearance itemsets
have a dedicated BC-catalog-backed editor.

## Navigation, theme and Map tools

The dashboard uses a responsive app shell: a left sidebar (collapsible to an icon
rail on tablets, a slide-in drawer on phones), deep-linkable sections
(`#/workspace`, `#/walkthrough`, …) and a quick-find palette (Ctrl/Cmd+K) for
sections and actions. The theme follows the system setting by default; the
**Theme** button cycles Auto/Light/Dark and stores only that preference (and the
sidebar state) in the browser's local storage. The sign-in and invitation pages
share the same light/dark styling and load only same-origin CSS.

The **Map tools** group (Walkthrough, Map analysis, Image export) replaces the
retired standalone `map-web` service and is available to viewers. See
[Map tools](./MAP_ANALYSIS.md#map-tools-in-the-admin-dashboard).

## Versioned BC room settings

Administrators can open **Room settings** in **Map & management** and use its
five-step wizard to review and edit persistent room properties: description,
background, space, member limit, game, language, blocked categories, access and
visibility roles, admin/whitelist/ban member lists, and the supported custom
image/music settings. Custom media URLs must be HTTP(S) and contain no embedded
credentials or secret query parameters. The connected bot account is retained
as a room admin when settings are applied. The room name remains controlled by
`BC_ROOM`; map data is managed separately through stored map revisions.

Each save appends a room-scoped revision to `prototypeRoomConfig`; optimistic
version checks reject stale edits, and the regular sanitized audit records the
mutation without copying room-setting values. The UI lists the latest 50
revisions. Restoring an earlier revision appends a new revision; room-setting
history is not deleted or automatically pruned. **Delete settings override**
appends the legacy `veratownRooms` template as the active revision; previous
revisions remain available for restore. Delete is offered only when that legacy
template exists. When no version exists yet, **New BC room settings** opens the
wizard with the legacy template as its starting value, and the first save
creates revision 1. If no version is created in Admin-Map, the first supervisor
startup imports the synchronized BC room as the baseline instead.

The web service still has no BC connection. A supervisor with no saved
room-settings revision imports the synchronized BC room properties first,
preserving the live admin, whitelist, and ban lists as the initial baseline.
On each supervisor process startup, the latest saved revision is applied through
the journaled BC room-update action and must be confirmed by a matching inbound
room sync; a response packet by itself is not treated as success. Failure to
observe the requested settings fails startup rather than reporting a successful
apply. Web edits do not change the live room immediately: the supervisor applies
them the next time it starts. A room-settings change stream refreshes the
missing-room creation template, while existing live room state remains
BC-observed.

The wizard manages persistent room configuration, not live moderation
operations such as moving or kicking individual occupants. Those actions are
not represented by a room-settings revision.

## Map-centred workspace

Open **Map & management** to load the same effective stored-map selection used
by room creation: the highest validated revision, or the existing legacy
fallback. The source and revision are shown; a corrupt head is not silently
replaced by an older map.

**Map legend and linked records** is collapsed by default; expand it to search
and browse the map's linked records.

This is the **stored creation map**, not a live BC observation. BC uses it when
creating a missing room. Saving configuration, restoring a revision, restarting,
or rejoining an existing room does not replace that room's live map. There is
no web BC connection, live-map capture or live door control.

### Visual layout editing and integrated analysis

**Visual map editor** paints the stored tile and object layers using searchable
pinned-catalog brushes. Tools include single-cell painting, contiguous flood
fill, two-corner rectangle fill, rectangular selection and copy/paste of both
layers. Paste beyond the 40-by-40 boundary fails without changing the draft.
Arrow keys move focus; Enter or Space applies the selected tool. Undo/redo keeps
up to 50 edits. Selection, focus and changed cells have distinct outlines.
Drag painting interpolates skipped cells and records the whole stroke as one
undo step. The hover outline previews the brush position. Choose tiles-only,
objects-only or combined visualization; doors and other objects use distinct
markers. Ctrl/Cmd+Z undoes; Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y redoes while the editor
has focus (native input editing is not intercepted). Selection movement buttons
and Alt+arrow keys move the **selection outline only**, not map contents; copy/
paste transfers selected contents to a new anchor.

Review shows the changed-cell count and the first 100 before/after catalog
styles. **Confirm save map revision** appends an audited `layout_edit` revision
under the normal expected-version check. Effects, map metadata and `CellData`
are preserved; objects with existing private cell metadata cannot be replaced
here. The editor never copies sign text or teleporter metadata. Conflicts retain
the draft and lock further writes until refresh; a failed refresh after
persistence is reported separately, not retried as another save.
Deploy the compatible bot build before the first layout save: older readers
that do not recognize `layout_edit` reject that revision rather than silently
loading an older map. No database backfill or live-room change is required.

Both layout and ordinary record reviews include **Configuration impact
review**. Layout review lists locations and doors intersecting changed cells,
including their trigger and occupancy geometry. Draft health is compared with
the stored configuration: new/existing warnings and notices are distinguished,
and resolved findings counted. Lists are bounded to 100 affected records and
100 findings. Overlapping regions are valid and do not produce findings.
Geometric warnings do not block an explicitly reviewed save, while invalid
input, protected metadata and stale versions remain server-rejected.

**Map analysis, candidates & walkthrough** reuses the existing
[map analyzer](./MAP_ANALYSIS.md), including uploaded exports, clickable cells,
region/fixture inventories, surfaces, adjacency, findings, labeled/candidate/
isometric images and the keyboard-controlled first-person preview. The analyzer
uses separate **Walkthrough** and **Map analysis** views; the walkthrough pairs
the 3D-style preview with a compact map and configured-door list rather than
stacking all analysis content into one long page. The admin and analyzer views
share the dashboard's light palette and responsive panel/button treatment.
In the stored-map explorer, `EmptySpace / Passable` is implicit and has no
marker; other catalog objects retain their schematic markers.

Analyze the stored layout or the current unsaved visual draft. When the analyzed
geometry matches the current managed map, the walkthrough overlays saved door
definitions at their configured coordinates, shows enabled/disabled state,
keypad and auto-open triggers, and highlights the selected door's inside region.
Configured locked/unlocked preview modes affect enabled configured doors only;
disabled doors retain preview openings. Actual door/gate objects and saved
door coordinates cut openings through wall tiles; ordinary `EmptySpace /
Passable` objects do not erase walls. Walls, half-walls, water, solid objects,
and all four map edges block preview movement, with a small camera clearance
to prevent clipping corners. Door frames and lintels remain visible around
openings. Beds, seating, tables, storage, vegetation, windows, and other object
families use distinct schematic shapes/materials, not BC image assets.
Low furniture does not hide walls behind it. Roof/ceiling planes cover exact
analyzed interior-region cells (including gaps correctly), while exterior
regions remain open to the sky. Interior classification is heuristic, not
stored building metadata or proof of a real roof.
**Walkthrough camera → Walkable isometric** offers a second, camera-following
3D-style view with the same player position, collision rules and door previews.
W/S or ↑/↓ moves north/south; A/D or ←/→ moves west/east on the map. The current
indoor region's roof is cut away to keep the player visible; other roofs remain.
Exterior tiles and pavement stay roofless even in mixed indoor/outdoor regions,
so parks, roads, sidewalks and outdoor gates do not acquire inferred roofs.
Mismatches between a door's configured styles and the map object are flagged,
and a selected configured door can be opened in its normal editor. These are
configuration previews, not live BC state, movement certification or permission
checks; access-code values are not sent to the analyzer. If the analyzed map
does not match the stored/current draft geometry, managed-door overlays are
hidden. Candidate review and import controls are kept in a separate collapsed
panel below the explorer so they do not lengthen the walkthrough view.

Analysis is authenticated, bounded, in-memory and available to viewers; it
neither writes configuration nor connects to BC. Exports are limited to 64 KiB;
geometry is exactly 1,600 cells per layer. Sign text, effects content and private
cell metadata are not returned.

Download reports, analysis JSON, exact row-span candidate manifests and import
review drafts. Choose include/exclude decisions and candidate identities in the
GUI, select a door's inside region, and explicitly confirm the target room.
Undecided candidates can be explicitly excluded together. Export review decisions
or reopen a reviewed draft JSON (maximum 2 MiB). The shared importer validates
every decision, source fingerprint/catalog, exact geometry and door defaults.
Validation opens included records in the normal editors **one at a time**, with
their normal version checks and explicit review/save; there is no bulk import.
New candidates start disabled. Region type, door styles, timing and triggers
can be refined in the ordinary editors. Unsupported automatic door defaults
must be resolved explicitly, not guessed.

The **Graphical candidate geometry** grid supports mouse and keyboard selection
with exact cell highlighting. Toggle multiple regions to merge them without
filling gaps or joining disconnected components. Geometry selection is separate
from import include/exclude decisions. Open the selection as one disabled new
location, or choose an existing region location/door and **Replace existing
target geometry draft**. Existing locations change only `region`; door targets
change only `insideRegion`, or, when explicitly selected, only the focused
door candidate's position. Identity, enabled state, timing, appearance rules,
triggers and other settings are retained. Replacement is not union with the
existing target geometry. The ordinary editor still requires review/save with
the target's original version; no bulk write or implicit activation occurs.

Analyzed source geometry may differ from the stored map; this is labelled and
does not replace it automatically. **Open candidate as disabled draft** is a
shortcut with a blank new identity, not approval of a complete import draft.
Collision/wall previews are geometric heuristics, not actual BC walkability,
live permissions or character observations.

### Linked configuration editing

The schematic map uses the pinned catalog and actual stored 40-by-40 tile
layout, not upstream BC artwork. Zoom and scroll to inspect it; toggle location,
door and trigger overlays. The map stays prominent beside the editor while
configuration is grouped under a clear resource and record heading. Click a tile
to open its square tooltip with catalog tile/object details and any matching
locations, doors, keypad/auto-open triggers or inside regions; matching records
link to their editors. Overlapping locations remain separate records.

Select a management category and a record, or create a new one:

- Point locations can be placed by clicking the map.
- Feature references to locations or doors offer **Select from map**. Click a
  matching point, region or door trigger square, then choose the listed record
  from the tile details. The picker highlights eligible map overlays and can be
  cancelled without changing the stored value.
- Rectangle regions use two opposite corners, inclusive.
- Exact tile regions support toggling individual cells, including gaps.
- Doors have map tools for placement, keypad tiles, auto-open tiles and the
  inside occupancy region. Blue outlines denote keypads; green denote auto-open
  tiles. Those are configuration triggers, not observed door states.
- Door appearance restrictions use searchable BC-catalog selectors for blocked
  and required items/groups, plus each conditional Blocked Appearance Rule's
  blocked and unless items/groups. The source and revision are shared with the
  appearance set builder, but door filters include all catalog assets, not only
  assets supported for itemset mutation. Legacy selections absent from the
  catalog remain visible and are preserved until explicitly removed. BC assets
  with an empty item name are valid exact references and appear as `(Default)`.
- Door **Additional access conditions** and **Conditional block** use guided
  flat AND/OR groups with typed comparisons. Suggested facts include live
  membership/role checks, keypad-code status, and catalog-backed appearance.
  Missing evidence fails closed. The local sample preview is not live BC state.
- Condition autocomplete is trigger-specific: door membership suggestions use
  the selected door's and room-wide stored groups; location value suggestions
  use this room's stored map locations; appearance suggestions use the complete
  pinned BC catalog. It does not read live BC characters, appearance, or roles.
- Structured objects, lists, enums, numbers and booleans use typed form
  controls. Unstructured metadata uses a bounded JSON field, never a raw
  database-document editor. These fields are identified by their setting path
  and record context; configuration summaries present nested values as labeled
  fields and lists instead of an undifferentiated JSON block.

    Location metadata includes guided **Location entry message**, **Delayed
    appearance**, and **Location entry behavior override** editors. Messages
    have a default template and ordered rules; each rule can group up to 16
    typed conditions with AND/OR, and placeholder buttons insert supported live
    character, appearance, location, occupancy, time, and feature-availability
    values. Each message rule stacks its conditions, full-width message and
    rule controls vertically. Condition fields wrap to fit the editor width,
    with reorder/remove controls on a separate row, including on mobile.
    Delayed appearance supports ordered conditional rules, each selecting
    an itemset and delay, plus an optional fallback; its rules use the same
    condition builder and searchable itemset references. Both editors can
    preview supplied sample facts locally; previews are not live BC evidence.
    Behavior overrides
    configure the enabled state, optional message, and shared appearance
    itemsets using dropdown selectors. Other feature metadata stays in a
    separate JSON field so editing a guided setting does not discard it. The
    server validates metadata before any location write. Map-analysis
    candidates and reviewed import records open in the same ordinary location
    editor, so the guided configuration is available on those assisted drafts
    before their individual review and save.

    Global and room configuration offers typed forms for known keys such as
    `casino.rules`, `shop.rules`, `location-entry-behavior.rules` and
    `command.aliases`. Location-entry rules use a location-type dropdown to add
    reusable defaults; itemset references use the shared itemset selector. The
    legacy `bunny.rules` key remains editable for compatibility. **Other
    configuration key** creates or edits any other key as a validated JSON value
    (object, array, text, number, boolean or null). Configuration is visible to
    viewers, so values with secret-like field names (passwords, tokens, access
    codes, member identifiers and similar) are rejected on save and redacted on
    read; existing values containing such fields cannot be edited in place.
    Command aliases are validated against the statically registered command
    catalog; unknown targets are rejected rather than stored unchecked. The bot
    also checks them against its live command descriptors. Alias validation errors
    identify the rejected rule or target in the workspace instead of reporting only
    a generic value-validation failure.

    The **Staged Table** game configuration has a guided, ordered
    `settings.appearanceStages` editor. Up to ten stages can be configured
    independently of the marker limit. After warm-up, an appearance participant
    advances one stage for each non-tied hand loss or the first post-warm-up
    hand that reduces a positive score-chip balance to zero; only one stage
    advances per hand.
    Each numbered stage selects one removable
    clothing or equipment group from the pinned BC catalog and one existing
    appearance itemset. Selecting a set pins its current revision; an older
    saved revision stays visible until explicitly repinned. Search does not
    alter a selection, and reordering preserves each stage's group, set and
    revision. The `provision:party-games` operator workflow can seed six
    validated strip-only defaults under `table.stage.set.one` through
    `table.stage.set.six`; it preserves existing definitions. The editor
    previews the exact stage order and itemset effects.
    `ItemNeck`, unsupported groups or items, duplicate groups, disabled or
    missing sets, outdated revisions, extra/mismatched strip groups, variants,
    locks, and non-owner lock removal prevent review. A non-empty plan also
    requires an enabled `emergency.release.clothing` set that strips every
    planned group, covers every group affected by stage pieces, and affects no
    groups outside those disclosed stage strips and applied pieces. Missing
    catalog or itemset data blocks review rather
    than silently passing. The server
    validates the configuration again at save; a successful save changes
    stored configuration only and does not confirm a live BC appearance change.

The **Stored map metadata and history** category lists stored revisions and backups and offers
audited actions: update map metadata, create a backup, restore a backup, and
restore an earlier revision. Restores append a new revision (history is never
rewritten) and require the expected current revision, so a concurrent change
fails with a conflict. They change only the stored creation map.

### Helper bot control plane

The **Helper bots** shortcut opens the database-backed helper administration
categories. Authenticated administrators can create or update helper identities
and create, update, or delete reviewed public Chat/Emote templates, edit fleet
and task limits and the per-helper action budget, inspect recent task lifecycle and current budget
counters, and cancel a task only while it is still queued. Identity edits may
include the optional `abcl` capability; this alone does not enable ABCL, which
also requires the room-scoped `abcl.rules` configuration. Identities are bound
to the configured room and runtime target; startup locations must be enabled
point `bot_position` locations, and templates must use an enabled location.
Templates may include an optional trigger key; the shopkeeper uses enabled
`shop.*` trigger-key templates in the configured shop region. Startup inserts
24 example shop interactions only when their IDs are absent, preserving
administrator changes. See the [shopkeeper interaction guide](../features/helper-bots/README.md#shopkeeper-interactions).
Optional occupied-region flavor schedules can be configured in the helper
settings JSON field; see the [regional flavor guide](../features/helper-bots/README.md#optional-regional-flavor)
for limits, queue behavior, and helper-first deployment.

Helper records are hidden from viewers. Writes use the workspace's normal
administrator, deployment-opt-in, exact-origin, expected-version, transaction,
and sanitized-audit checks. The task view omits message payloads and member
identifiers. Claimed, running, dispatched, or uncertain work cannot be edited,
cancelled, or replayed; roulette tasks remain event-driven and cannot be
created from the dashboard. This module does not expose Railway credentials,
deployment variables, or restart controls and does not connect to BC. A saved
database change is not proof of runtime adoption; use the existing runtime
reports for independently reported consumer receipts.

Edits remain in the current browser tab, not local storage. Use **Review
changes**, then **Confirm save**; the preview names changed fields without
echoing secret values. Editing again invalidates that preview. Navigation warns
before discarding drafts. Read-only users can inspect records but cannot edit.
Conflicts preserve the draft; refresh and review the newer record rather than
blindly retrying. A failed refresh after a successful save is reported separately.

### MOTD presets and schedule

The **MOTD** tab manages reusable private greeting messages and the ordered
rules that choose them. Messages have stable generated IDs, an operator label,
and text limited to 500 characters. Each rule selects a message and may target
one or more weekdays (Sunday is 0 through Saturday 6), a start/end time, or
both. Leaving weekdays unchecked means every day; leaving both times blank
means the rule is weekday-only. Time ranges use the selected IANA time zone,
include the start, exclude the end, and may cross midnight. A cross-midnight
rule's weekday refers to the day it starts. Move rules up or down to control
which one wins when multiple rules match.

The schedule is the global `motd.schedule` key in the `prototypeGameConfig`
collection. Save and delete operations use the workspace's explicit admin
write opt-in, revision conflict check, and transactional audit. A missing,
invalid, or unmatched schedule uses the bot's `MOTD_TEXT` environment value;
deleting the schedule restores that fallback. Updates are picked up by the
running bot's existing game-configuration watcher and apply to subsequent
character entries. No live BC state changes.

### Guided administration

- **Common administration tasks** opens doors/access, locations, shop, location
  entry behavior,
  casino, games and shower settings directly. Forms group existing schema fields
  into identity, geometry, access policy, appearance, offers and game-rule
  sections without changing their persisted shape.
- **Linked configuration** on a door shows trigger counts, applicable stored
  door/room-scoped groups, code-configured status and membership links. Group
  inspectors link back to their door and grants and can prefill a new grant;
  door inspectors can prefill a new access group. Location inspectors link
  overlapping doors and configured shop/casino bindings.
- Searchable reference selectors cover existing doors, groups, locations and
  appearance itemsets; membership group choices follow the selected door, and
  location choices distinguish point and region geometry. Location and door
  references can also be chosen directly from their map geometry, including
  keypad and auto-open trigger squares. BC item and group filters use the same
  search-and-select interaction. Existing unlisted values are preserved for
  review, while new itemset keys remain explicitly authorable. Server-side
  validation remains authoritative.
- **Location metadata** supports both configurable private entry messages and
  delayed appearance-set triggers. A trigger selects a standard itemset and
  delay; it is cancelled if the entrant leaves before expiry. When a location
  references a set, its linked-configuration panel opens or creates that exact
  set in the full appearance editor, so map administration can manage the
  referenced typed/modular options and supported lock settings without copying
  or bypassing itemset validation.
- **Configuration health** reports stored-reference problems, disabled
  configuration and game readiness, with links to the relevant editor. Checks
  include orphan groups/grants, missing or disabled location/itemset references,
  incompatible casino geometry, duplicate door coordinates, and an enabled
  shower without an enabled shower point. Additional checks cover unknown
  pinned tile/object IDs, out-of-map points/triggers, non-doorway wall placement, door
  object/style mismatches and ambiguous styles, shared trigger tiles, empty
  occupancy regions and showers without enabled songs.
  Readiness findings include eligible/required prompt counts and missing
  release/policy gates. Region overlaps are allowed and are not reported.
  Unavailable singing is a notice, not automatic configuration rejection.
  An unavailable itemset endpoint is explicitly reported, not treated as a
  complete successful check.
  Doors are expected to be embedded in walls, so door placement is not a wall
  overlap warning. Point/trigger wall checks also exempt pinned `WallPath`
  openings. Invalid coordinates, incompatible door styles and other
  configuration problems remain checked.
- Admins can accept **shared door trigger tiles** as intentional.
  Acceptance is stored per room and tied to the current versions of every
  involved record; changing any involved record
  makes the acceptance stale, so it no longer suppresses the finding and must
  be confirmed again. Accepted findings have a revoke control, and resolved
  findings remain available under **Previously accepted exceptions** for
  cleanup, including legacy region-overlap acceptances. Other health findings
  are not waivable. Viewers have read-only access, and writes still require the
  configured workspace-write opt-in,
  administrator role and allowed origin.
- The searchable **Map legend and linked records** selects and focuses a
  location or door. Tile inspection includes door triggers and inside regions.
  Orange overlays preview selection and draft geometry. **Duplicate with new
  identity** creates an unsaved, disabled location/door copy with a blank key;
  the operator must provide a new identity and review geometry. Door groups,
  memberships and hidden metadata are not copied.

Health and access summaries are **stored configuration previews**, not live BC
authorization, observed appearance, current role evaluation, or proof of runtime
adoption. Health checks are deliberately limited and do not replace feature
validation. The web application cannot grant a live access decision.

### Review, validation and concurrent edits

Review shows a bounded, nested field-by-field before/after table. Numeric and
boolean changes and schema-declared choices are visible; free text, song/prompt
contents, private metadata and write-only fields are omitted or redacted.
Browser validation highlights invalid inputs, and server validation paths mark
matching fields while preserving the draft.

After a stale write, **Compare with latest** loads current stored configuration
without changing the draft. Review both changes since opening and the draft's
differences from the latest value. **Use latest as base; keep my draft** requires
explicit confirmation because keeping the whole draft can overwrite another
operator's changes. It does not merge or save automatically: review and confirm
again. If the record was deleted, an update is not offered. A later concurrent
write still fails the server's normal version check.

**Refresh stored configuration** explicitly reloads the workspace, with the
normal unsaved-draft warning. If saving succeeded but refreshing failed, further
writes stay disabled until a successful reload; do not blindly repeat the write.

## History, recovery and runtime reports

**History & recovery** is visible only to administrators. All recovery reads
are admin-authorized, scoped to `ADMIN_WEB_ROOM_KEY` and the pinned runtime
database, and unavailable to viewers. Reads work without enabling writes;
snapshot capture and restore use the existing opt-in, admin, exact-Origin and
JSON write gates. No new environment variables or BC connection are required.

### Audit history

Browse 25 sanitized audit entries per page with **Next audit page**. A stable
timestamp/ID cursor handles tied timestamps without skipping records. Entries
expire after 30 days and show category, operation, hashed record identifier and
an anonymized actor identifier. Actor identifiers may change after a service
restart. The audit contains no previous values and is **not an undo log**.

### Configuration snapshots

**Capture snapshot** stores version-1 configuration projections using a
snapshot-read MongoDB transaction, paired with a sanitized audit entry:

- Locations, doors, access groups and membership configuration.
- All supported global/room settings, shower settings and operator-authored
  songs, party-game configuration and authored prompt catalogs.
- Shared appearance itemset definitions and the effective stored creation map.

Snapshots exclude credentials, access codes, hidden/private metadata, map sign
content, player submissions, sessions, balances, wallets, action journals and
heartbeats. Configuration memberships contain their configured principal IDs;
they remain administrator-only. Authored songs/prompts are configuration, not
player submissions. Never embed secrets or private player content in authored
configuration.

Snapshots are bounded to **8 MiB and 8,000 records**, **30 days**, and **20 per
room**, whichever limit is reached first. Capture fails explicitly if any
configuration catalog exceeds its existing read bound, is invalid, or the
stored map cannot be loaded; it does not silently capture a partial catalog.
A per-room serialization document makes count pruning transactional under
concurrent captures. The newly captured snapshot survives timestamp ties.
Expired snapshots are rejected explicitly even before asynchronous TTL cleanup.
Snapshot metadata and content live in `prototypeAdminSnapshots`; an internal
per-room guard has no TTL. The collection and cursor indexes are initialized
only on writable service startup. Grant its identity the required collection
permissions alongside the existing configuration and audit permissions.

Use **Review snapshot**, filter by category/record ID, select one record and
review the bounded redacted before/after comparison. Then tick the confirmation
and choose **Restore this record**. Each request restores **one record in its
own transaction**, validated through the existing feature mutation path with
current expected versions and both configuration/recovery audit entries.
There is no automatic bulk restore, automatic retry, or deletion of records
absent from the snapshot. Restore dependencies in order: locations, doors,
groups, then memberships; itemsets/prompts before dependent game settings.

Restores preserve existing access codes and private fields. Protected metadata
containers omitted from the snapshot are retained; incompatible replacements
or changes to arrays containing hidden fields fail explicitly rather than
losing private values. Existing secret-bearing generic settings can be
restored through this protected recovery path, while ordinary generic edits
remain disallowed. Missing records are recreated without their excluded
secrets; set any required access code separately. A restored song keeps its
snapshotted enabled setting, including when recreated.

Global configuration, itemsets and prompt catalogs can affect other rooms.
Review their shared-scope warning; shared-prompt protections and game activation
readiness/release gates still apply. Truth/Dare prompt restores append a new
revision from the current latest prompt rather than overwriting history.
Stored-map restore appends a revision marked `snapshot_restore`, preserves
current sign metadata and never changes the live room map.

After success or conflict, **Reload preview** is required before another
restore. A successful transaction means configuration was persisted and
audited, **not** that the running bot adopted it. Snapshots are selective
configuration recovery, not disaster-recovery backups or a substitute for
database backups.

### Read-only runtime visibility

**Runtime reports** lists at most 50 non-expired process heartbeats for the
configured logical room, with anonymized instance IDs, reported state and last
heartbeat. A report older than **180 seconds** is marked stale; a report over
60 seconds in the future is labelled clock skew. A stale `ready` report is
still only a stored report, never a claim that the bot is connected now.

Bot heartbeats now publish optional `roomKey` without changing lifecycle or
shutdown order. Older bots without this field are omitted until redeployed;
there is no inferred room association or account/member identifier exposure.
An empty list does not prove the bot is stopped. Instrumented bots include
bounded **consumer receipts**, independent of heartbeat freshness:

- Shared game-configuration cache application reports its actual revision/time.
  This is **not** downstream game/session adoption.
- Command aliases report a pending revision until the next actual alias lookup
  installs it, then their loaded revision/time.
- Casino reports rule application to a newly created session; existing rounds
  retain their own rules. Shop reports actual command/location policy use;
  Location Entry Behavior reports policy application to messaging, disabled
  gating and itemset selection. Known shared-configuration revisions remain pending until these
  next-use paths apply them. These are shared-cache revisions, not invented
  per-document versions; older in-flight applications cannot clear a newer
  pending revision.
- Shower reports settings application to actual location-entry eligibility and
  timing. It does not claim adoption of song contents or a speculative pending
  revision.
- Door/location runtime replacement and room-creation-template installation
  report actual load times, with revision `null` because those aggregates have
  no authoritative single revision. Templates affect future missing-room
  creation, not existing rooms.
- Reload/watch/validation errors use safe identifiers and preserve historical
  last-good receipts. A previous successful receipt does not override the
  consumer's current failure/fail-closed state.
- Other consumers, unreported active-session configurations and older bots without receipts remain
  explicitly **unknown**. Persisting configuration never manufactures a
  consumer receipt or proves a live BC state.

No automatic polling, restart, live door action or BC operation is exposed.

## Database-managed bot instance profiles

The **Bot instances** workspace resource creates and edits one typed,
versioned profile per stable deployment ID. The profile's `schemaVersion` is
currently `1`; every successful create or edit advances its revision and stores
an immutable snapshot in `prototypeBotInstanceRevisions`. Expected-revision
compare-and-swap rejects stale edits. Profile writes and the existing sanitized
operator audit are committed in the same transaction. Revision history has no
TTL. Retirement is a soft delete: it disables and tombstones the profile,
preserving its history and preventing the deployment ID from being reused.

The wizard covers role selection (`supervisor` or `helper`), role-specific
settings, validation/preflight, review, enable/disable, and retirement. The
former `abcl` role is retired; existing profiles can be edited to `supervisor`
or disabled/retired, but cannot start a runtime. The ABCL feature is managed by
the supervisor and uses a separate helper process. Helper profiles must
reference a supervisor-owned helper record in the same runtime target and room;
an enabled profile requires that helper record to be enabled. Profiles
currently support the `main` runtime room.
`BOT_INSTANCE_ID` on the matching container must exactly match the profile's
stable ID.

The profile can hold the non-secret BC server URL, origin and room, the role,
helper identity, appearance qualification, and supported position-observation
settings. It never stores BC username/password, Mongo URI/database name, or
position-observation bearer tokens. Its runtime-target field is scope metadata
for mismatch validation, not a database selector; `RUNTIME_TARGET` and the
fixed database mapping remain deployment settings. Keep credentials and tokens
in the relevant deployment's secret/environment settings. Profile
changes are desired configuration only: they do not provision, restart, or
stop containers. Every profile-controlled setting currently requires an
operator-managed restart; when disabling or retiring an already-running bot,
stop its container separately. A missing, disabled, deleted, malformed, or
wrong-target profile fails closed at startup before BC connection.

**Runtime reports** shows the profile revision and role actually loaded by each
process beside the latest desired revision, role, enabled state, and
`current`, `restart_required`, or `missing` status. This reports configuration
adoption only; it does not prove that the container has restarted or that BC
state was observed.

## Appearance itemsets

The **Appearance itemsets** section builds sets from the pinned, extracted BC
catalog without connecting to BC. Search the full catalog by group, category,
item name, or description; results are capped so large matches can be narrowed
further. Each set item starts from its catalog blueprint and exposes its typed or
modular selections under **Extended configuration**, color layers, reviewed
itemset-compatible locks, and catalog-declared fields under **Item parameters**.
Parameters are shown before color and lock settings; defaults are inherited and
only overrides are saved. The pinned catalog does not declare a `Tightness`
baseline property, so there is no independent tightness override; use only the
extended options offered for each supported item. Select one or more optional appearance
groups to strip from the target's current observed appearance before applying
the listed items; strip-only sets are supported. Required groups are not offered.
Property controls show inherited catalog
defaults and save only overrides; changing the selected asset resets those
overrides and other asset-specific settings. The catalog is cached in page memory;
itemsets and access identity are refreshed when the editor opens again. Reload the
page to fetch a newly deployed catalog revision. Assets with unresolved or
unsupported configuration metadata are omitted; server-side itemset validation
remains authoritative.
The editor lists every discovered BC lock type, but unsupported or non-itemset
locks are disabled with their review/credential reason; only reviewed
itemset-compatible types can be saved. A location's map-workspace link opens this
same editor with the referenced set selected (or prefilled for creation).
Conditional variants can compare observed body traits or check whether a
catalogued item's BC prerequisites are satisfied for the target. For an
extended item, the selected option is included when that item is part of the
candidate variant. The editor uses catalog selectors only; it has no live BC
connection, and unknown runtime evidence never selects a variant.
Extended selectors such as shock levels and modular device options are catalog
data, not arbitrary property writes. No stand-alone item difficulty override is
offered because the pinned metadata does not define a validated range/semantics.
Timer locks save the displayed default duration of 300,000 milliseconds (five
minutes) even when the duration field is left untouched. Existing explicit
durations are preserved; the configuration write API rejects timer locks without
a duration rather than saving an itemset that cannot be applied independently.
Door appearance selectors use a separate, unfiltered identifier projection of
this same catalog; matching an observed asset does not require mutation support.

Create and update operations use the same authenticated, origin-checked,
opt-in workspace write path and sanitized audit log. The set key stays stable,
the server advances its revision, and an optional catalog pin can prevent use
against a different extracted revision. Deletion is not offered because
location-entry behavior and shop rules can refer to shared itemset keys. Saving changes only the
prototype database; it never applies an appearance to a BC character.

## Authentication and transport

The admin service defaults to local passkey authentication; it does not require
Cloudflare Access. Local Admin Web password login is disabled unless the
environment-operator recovery option is explicitly enabled. Set
`ADMIN_WEB_AUTH_MODE=cloudflare` only when Cloudflare Access is available and
deliberately selected. There is no automatic fallback between authentication
modes or to unauthenticated access. Local mode ignores Cloudflare identity
headers.

### Cloudflare mode (existing deployments)

Use Cloudflare Access as the identity-aware proxy in front of the Railway
service. Create an Access application for its public hostname, configure an
identity provider, and require MFA in the Access policy. The application must
have a stable audience/tag and an issuer URL of the form
`https://<team>.cloudflareaccess.com`.

The service independently verifies the RS256 signature of the
`Cf-Access-Jwt-Assertion` against the issuer's HTTPS JWKS endpoint. It also
checks the exact issuer and audience, expiry and not-before timestamps, and
requires the signed email to appear in `ADMIN_WEB_USERS`. It does not trust
identity headers without a valid signature or accept the map analyzer's shared
password. Configure every permitted email individually; do not use a wildcard
or organization-wide allow rule. Cloudflare Access terminates public HTTPS, and
the app remains protected even if someone reaches the Railway origin directly.

### Local passkey login and invitations

Admin Web uses passkeys for local sign-in. New accounts are created by an
administrator-issued invitation link; email delivery is not used. The map
analyzer continues to use its existing environment-configured password
accounts. Both services use **separate service-specific accounts, origins, and
sessions**.

Set these Railway variables on each web service:

| Variable                         | Value                                                                                                                                                                            |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WEB_AUTH_ORIGIN`                | Exact public HTTPS origin, no path: `https://map-admin.netheril.org` for admin, `https://map-analyse.netheril.org` for analyzer                                                  |
| `WEB_AUTH_USERS`                 | Admin's environment recovery operator and the analyzer's configured accounts: email maps to `{ "passwordHash": "<generated hash>", "role": "admin" }` or `viewer`; 1-50 accounts |
| `AUTHENTICATION_RECOVERY_METHOD` | Optional Admin Web local-auth recovery setting. The only accepted value is `environment-password`; unset means passkey-only.                                                     |

`WEB_AUTH_USERS` is required for local Admin Web configuration and remains the
Map Analyzer's active account source. For Admin Web it is **not** a normal
password login. Only when `AUTHENTICATION_RECOVERY_METHOD=environment-password`
is set can the configured environment operator use its password as a
break-glass recovery method. Other environment entries are not Admin Web
accounts. An operator email that also exists in the database receives that
database account's current role; role changes therefore remain effective. Do
not enable this option unless the recovery path is needed.

For first-time setup, temporarily set
`AUTHENTICATION_RECOVERY_METHOD=environment-password`, sign in as the
environment operator, and create an administrator invitation. Open the
one-time link on the recipient's device and register a FIDO2/WebAuthn passkey;
the first database account must have the `admin` role. After confirming that
the new administrator can sign in with the passkey, unset the recovery variable
and redeploy for passkey-only operation. You may keep it set for continued
operator recovery. Existing password-backed database accounts cannot use their
stored password hashes to sign in; while recovery is enabled the configured
operator can register their own passkey, and other users should be re-invited
after their legacy account is removed.

The invitation URL is shown once in the **Accounts & passkeys** tab for manual
copying; no email is sent. Its random bearer token is carried in the URL
fragment, is never sent in HTTP requests or referrers, and is stored only as a
SHA-256 hash. Invitations are bound to an email and role, expire after 24 hours,
and are single-use, revocable, and replaceable (creating a new invite for the
same email invalidates its previous link). Redemption transactionally consumes
the invite and creates a passwordless account plus its passkey. If registration
fails, an administrator can issue a fresh invitation.

`viewer` accounts remain read-only. `admin` accounts can write configuration
only when deployment writes are explicitly enabled; account security
administration is separately restricted to authenticated local admins and
same-origin requests. These variables belong to the separate web services, not
the bot. Do not add database or BC credentials to the analyzer.

Admin account, passkey, and invitation records are stored in
`prototypeAdminWebAccounts`, `prototypeAdminWebPasskeys`, and
`prototypeAdminWebInvitations` in the pinned prototype database. New account
documents contain email, role, and timestamps, with no password hash; the
reader accepts legacy password hashes but Admin Web never uses them for
authentication. Passkey records contain a WebAuthn credential ID, public key,
signature counter, and non-secret device metadata—not an authenticator's
private key or biometric/PIN data. Invitation records contain only the hashed
token and invitation metadata and are automatically removed after expiry.
These records must remain in the isolated prototype database and protected by
the MongoDB provider's encryption at rest and TLS in transit. This
implementation does not add a separate field-level encryption key. Configure
the database provider's encryption-at-rest feature; do not copy these
collections to the production/source database.

Account admins can issue/revoke invitations, change roles, and delete accounts;
the final database administrator cannot be demoted or deleted, and admins
cannot remove their own account. Account deletion revokes sessions. An
environment operator account cannot be deleted while environment password
recovery is enabled. Users can register up to ten passkeys and remove their
own credentials; at least one passkey should remain registered.

The local passkey flow requires a discoverable FIDO2/WebAuthn credential and
authenticator user verification; registration and assertions are checked
against the exact configured origin and RP ID. Generate hashes locally only
for the Map Analyzer's environment password accounts or the optional Admin
Web recovery operator. Use a password manager-generated unique password of at
least 16 characters (prefer 24+ random characters). In a Bash terminal, this
hides input and avoids putting the password in command arguments or shell
history:

```sh
read -r -s -p 'New web password: ' WEB_PASSWORD
printf '\n'
printf '%s' "$WEB_PASSWORD" | npm run --silent web:hash-password
unset WEB_PASSWORD
```

The command outputs only the salted scrypt hash (`N=32768`, `r=8`, `p=3`,
64-byte key). Put the entire hash, including `$` separators, in the
`passwordHash` field of the relevant service's `WEB_AUTH_USERS` value. Share
passwords through a password manager, not chat or logs. Example:
`{"operator@example.org":{"passwordHash":"<generated hash>","role":"admin"}}`.

The Admin Web login page is `/auth/login`; passkey assertions are submitted
only through HTTPS. With recovery disabled, the page has no password form.
Cloudflare mode delegates authentication factors to Cloudflare Access and does
not let this application enforce passkey-only identity-provider policy.
After login the browser receives an opaque random, host-only `__Host-` cookie
with `Secure`, `HttpOnly`, `SameSite=Strict`, and no domain attribute. Only a
SHA-256 key of the cookie token is stored, and sessions rotate on login and are
deleted on logout. Database account status and roles are read on each request,
so deleted or downgraded accounts take effect immediately. With the optional
environment recovery method enabled, only configured operator credentials can
authenticate by password; an existing database account's current role remains
authoritative.

| Service                  | Session storage                                                          | Idle expiry    | Absolute expiry |
| ------------------------ | ------------------------------------------------------------------------ | -------------- | --------------- |
| Admin dashboard          | `prototypeAdminWebSessions` in the pinned prototype database (TTL index) | **15 minutes** | **1 hour**      |
| Map analysis web service | Bounded process memory; lost on restart                                  | 30 minutes     | 8 hours         |

Admin sessions survive Railway sleep, redeploys and the idle database disconnect;
a database reconnect never requires a new login. If the session store is
temporarily unreachable, requests return `503` and the cookie is kept rather
than being treated as signed out. The dashboard checks `GET /auth/session`
(which reports `expiresAt`/`idleExpiresAt` and never extends the session) only
when expiry is near, and shows a warning **5 minutes** before the earlier idle or
absolute deadline. **Stay signed in** (`POST /auth/session`) extends the idle
deadline, never the 1-hour limit. **Reauthenticate with passkey** verifies a
fresh assertion and rotates the cookie without reloading, so unsaved drafts stay
in the tab. When environment password recovery is enabled, the configured
operator also has a password reauthentication form.

The admin service initializes indexes at startup. The Mongo identity needs
`createIndex`, `find`, `insert`, `update`, and `remove` on
`prototypeAdminWebSessions`, `prototypeAdminWebAccounts`, and
`prototypeAdminWebPasskeys`, and `prototypeAdminWebInvitations`. Account,
passkey and invitation collections are required for local mode; failure to
initialize them stops startup. Invitation redemption uses a MongoDB
transaction. If the session TTL index cannot be created, the service warns and
falls back to process-memory sessions that end on restart.

Every UI asset and API route requires authentication except the login page and
the invite enrollment page, its script, and the token-protected invitation
registration and passkey-login flows. Cookie-backed mutations require an exact
matching `Origin`; forged identity and forwarded-IP headers confer no access.
HTTPS is required; HTTP is accepted in configuration only for loopback
development.

When enabled for Admin Web recovery, password login requests are capped at
4 KiB, 20 attempts/minute per process, five attempts/15 minutes per configured
operator (and a shared unknown-account bucket), and two concurrent scrypt
verifications. Passkey assertions share the per-account and global attempt
limits. Attempts include successful logins.
Limits do not trust client-supplied forwarding headers. Responses report generic
credential failures and `429`/`Retry-After` when throttled. Persistent lockout
or distributed attack protection is not provided: Cloudflare WAF/rate limiting
can add a layer, and an attacker can temporarily exhaust login capacity.

**Run exactly one replica per web service; disable horizontal autoscaling.**
Throttles (and map analysis sessions) are process-local and reset on
redeploy/restart. To revoke the environment fallback account, remove it from
`WEB_AUTH_USERS` or unset `AUTHENTICATION_RECOVERY_METHOD`, then redeploy; the
password route is disabled when the recovery variable is unset. Database
account deletion revokes its sessions; a changed role is used on the next
request. Manage database-backed accounts and invitations in the **Accounts &
passkeys** tab. Passkeys require authenticator user verification and are
phishing resistant.

### Cloudflare and Railway checklist

- Keep both public application DNS records proxied and use **Full (strict)**
  TLS with valid Railway certificates. Do not cache application/auth/API
  responses; all are sent with `Cache-Control: no-store`. Avoid cache-everything
  rules for these hosts.
- An optional WAF IP allowlist is defense in depth, not the authentication
  boundary. Direct Railway-origin requests still require an application
  session; no `CF-*` identity header bypass is supported in local mode.
- Remove unused generated public Railway domains where practical. Removing
  DNS alone is not a private-network access control.
- Keep supervisor/helper observation endpoints and MongoDB private; they are
  unrelated to these public tools.
- Configure admin with a **least-privilege MongoDB identity** for the pinned
  test database first, leaving configuration writes disabled. Verify the complete login/logout flow, expired sessions,
  unauthenticated UI/API denial and direct-origin denial before production.
- When migrating the analyzer, set `WEB_AUTH_ORIGIN` and `WEB_AUTH_USERS` before
  deploying this version, then remove `MAP_WEB_PASSWORD`. Basic credentials are
  no longer accepted. For admin local mode, remove unused `ADMIN_WEB_ACCESS_*`
  and `ADMIN_WEB_USERS` variables when running local mode. No DNS or deployment changes
  are performed by the application itself.

The UI and API use a restrictive same-origin content security policy, no-store
responses, and safe text rendering. Authentication tokens and configuration
values are not logged. The dashboard displays the authenticated email and role.

## Railway service

Create a new service in the existing `veratown` project:

| Setting        | Value                            |
| -------------- | -------------------------------- |
| Root directory | Repository root                  |
| Builder        | Railpack                         |
| Build command  | `npm run build:admin-web`        |
| Start command  | `npm run start:admin-web`        |
| Node.js        | 24                               |
| `PORT`         | Leave unset; Railway supplies it |

Attach a public HTTPS hostname. Configure the common variables below plus
either the local variables above or Cloudflare mode variables:

| Variable                    | Required        | Notes                                                                                                             |
| --------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------- |
| `RUNTIME_TARGET`            | Yes             | Start with `test`; only `test`/`veratown_prototype_test` or `production`/`veratown_prototype` pairs are accepted. |
| `MONGODB_URI`               | Yes             | Dedicated identity restricted to the selected prototype database; read-only until enabling reviewed writes.       |
| `MONGODB_DATABASE`          | Yes             | Must match the database pinned by `RUNTIME_TARGET`.                                                               |
| `ADMIN_WEB_AUTH_MODE`       | No              | `local` (default), or explicit `cloudflare` when available.                                                       |
| `ADMIN_WEB_ACCESS_ISSUER`   | Cloudflare only | Cloudflare Access team issuer URL.                                                                                |
| `ADMIN_WEB_ACCESS_AUDIENCE` | Cloudflare only | Audience value for this Access application.                                                                       |
| `ADMIN_WEB_USERS`           | Cloudflare only | JSON object mapping each permitted email to `viewer` or `admin`.                                                  |
| `ADMIN_WEB_ROOM_KEY`        | No              | Room scope shown by the dashboard; defaults to `main`.                                                            |
| `ADMIN_WEB_WRITES_ENABLED`  | No              | Defaults to `false`; only exact `true` enables authenticated administrator configuration writes.                  |

Writes also require `WEB_AUTH_ORIGIN` in Cloudflare mode. It must be the
deployment's exact public HTTPS origin; local mode already requires it.
Leave writes disabled while first configuring authentication and read access.

For example, the user map has the shape
`{"operator@example.org":"admin","viewer@example.org":"viewer"}`. Use actual
addresses only in the service's secret/environment settings, never in source
control or logs. Do **not** configure `BC_SERVER_URL`, BC credentials, helper
tokens, or the source database on this service.

First deploy against the test database with a MongoDB user that cannot write.
Verify only the intended room's data is visible, the selected login/allowlist
works (including MFA for Cloudflare mode), and unauthenticated direct-origin
requests fail.
Do not point the dashboard at production until that review is complete.

## Railway Serverless / idle sleep

Enable **Serverless** in this separate admin service's Railway settings, then
**redeploy**: the setting only applies to newly created containers. Do not
enable it on the BC bot or helper workers, which require persistent connections.
See [Railway Serverless](https://docs.railway.com/deployments/serverless).

The dashboard validates its database connection and initializes writable indexes
at startup as before. After **15 minutes without active database work**, it closes
the entire admin MongoDB client, including topology-monitoring sockets. Pool idle
timeouts alone would leave monitoring traffic running and prevent sleep. A later
authenticated database request opens a new client through the same pinned-database
guards. Concurrent requests share a connection; ongoing operations, transactions
and session cleanup finish before the idle timer starts. Failed operations are
reported normally and mutations are never automatically replayed. Shutdown drains
requests and clears the idle timer. The bot's client and configuration watchers
are unaffected.

The web UI does not poll in the background; a parked tab, login session, or unsaved
local draft does not generate keepalive traffic. The only automatic request is a
single session-status check shortly before expiry (plus one when a hidden tab
becomes visible again); after a session ends, no further checks run. Admin
session lookups use the same lazy connection as configuration reads, so an
authenticated request reopens MongoDB; static assets after login and in-memory
analysis add no long-lived traffic. Cloudflare signing keys are fetched
on demand only. Avoid external uptime probes, scheduled requests, telemetry, or
other traffic to this service, and disable Static Outbound IPs if they keep it
awake. Railway normally sleeps a container **5–10 minutes after its last outbound
packet**, so allow that interval after the database disconnect, not just after the
last page interaction. Inspect Railway deployment status and network/connection
metrics to verify sleep and reconnection on the deployed service; private traffic
also counts even when absent from Railway's public network chart.

Opening the service wakes it automatically, with cold-start latency; Railway
warns the first request may return 502. Reload a failed page, but do not blindly
repeat a save: reload stored state to determine whether the write completed.
Admin login sessions are stored in MongoDB, so a cold wake within the session
lifetime does not require signing in again. Save work before leaving; drafts
are not server-persisted by sleeping. This implementation permits sleep but cannot
guarantee it in the presence of unrelated network traffic.

## Startup troubleshooting

The admin container must run `npm run start:admin-web`, not `npm start` (the bot).
Successful startup logs `Admin web service listening` with port and authentication
mode. Failures log a safe `stage` and `reason`; no MongoDB URI, password hash,
raw driver error, or stack is printed.

- `configuration`: local mode is the default and requires `WEB_AUTH_ORIGIN`
  plus the fallback `WEB_AUTH_USERS`; Cloudflare settings are required only
  when explicitly selecting Cloudflare mode.
  `WEB_AUTH_USERS` is a JSON object of email accounts with `passwordHash` and
  `role`, not the old email-to-role map or a bare hash. Hashes must use the
  provisioner's exact `scrypt$32768$8$3$...` format. Paste without extra surrounding
  quotes or shell expansion. `WEB_AUTH_ORIGIN` is the exact HTTPS origin, no path.
- `database`: check the MongoDB connection string, authentication database
  (`authSource`), network access, and least-privilege grants for the three local
  auth collections. Never add BC credentials or relax pinned database isolation.
- `listen`: check port availability; normally leave `PORT` to Railway.

The npm `production`/`--omit=dev` warning is not the application's startup
failure. A generic failure from an older deployment does not identify which
variable or connection failed; deploy diagnostics before inferring the cause.

Failed `/api/workspace` reads still return a generic temporary-unavailability
response. The service log now includes a safe read `stage`, error class, and
numeric MongoDB error code when available; it omits driver messages, stacks,
connection details, and stored values. Use those fields to narrow the failing
read before changing stored configuration or database grants.

## Authorization and runtime boundary

The dashboard reuses the feature repositories' validated reads and the existing
casino rules schema. It omits keypad group codes and membership records from the
door view; shower song text is shown only to an authenticated, allowlisted user.
Appearance itemsets are validated and bounded to 100 entries. The configured
room key is fixed by service configuration, not selected by an API request.

Legacy summary endpoints remain read-only. The workspace is the only
configuration mutation surface and is scoped to the deployment's fixed room;
it cannot select an arbitrary database or impersonate a BC member.

### Adoption is separate from persistence

A successful save means configuration was persisted, not that BC acknowledged
anything or that a feature already used the new value. Map/location/door and
game-config watchers reload their owned configuration; feature-specific settings
can instead be read on the next interaction or round. Editing a game's settings
does not cancel, rewrite or settle an active game. Runtime sessions, wallets,
ledgers, player submissions, action journals and heartbeats are not editable
configuration.

Web login does not grant a BC role. Dynamic room-admin and room-whitelist access
groups remain evaluated by the bot from current BC state; the web interface
must not present stored membership grants as authority for those principals.
The web editor cannot independently perform a live access check.

### Rollout

First qualify administration against the isolated test database, including
viewer denials, cross-origin write rejection, stale edits, transaction rollback,
access-code redaction and reload behavior. Use a dedicated database identity
with only the collection permissions needed by the configured administration
surfaces and audit history, never a cluster-wide credential. Existing runtime
database isolation stays unchanged. Production writes require a separately
reviewed deployment decision; this code change does not enable them.
The admin service also needs access to `prototypeAdminFindingAcknowledgements`,
which stores room-scoped, reversible finding confirmations separately from the
audit log.

### Audit retention

`prototypeAdminAudit` stores sanitized operation metadata atomically with each
successful configuration mutation. It does not contain access codes, song or
prompt text, map sign contents, complete configuration values or passwords.
Entries expire after 30 days through an `expiresAt` TTL index. Failed writes
must not leave a successful audit entry, and audit failures must roll back the
mutation. These transactions require a MongoDB replica set.

`prototypeAdminFindingAcknowledgements` stores the accepted finding identity,
the versions of its related records and a hashed actor identifier. It has a
room/key index and no TTL because resolved exceptions remain revocable until an
admin removes them. The collection is bounded when read by the workspace.
