feat(pages): help-page (support) queue — stream, snapshot, respond, close

Phase 2 of docs/ADMIN_CONTROLS.md: surface the in-game help-page queue to the
website.

- BridgePages.cs: the queue has no EventSink, so it is polled (PageSweepSeconds,
  default 5s) and diffed, keyed by sender serial (one page per player) ->
  page.new / page.updated / page.closed. Inbound pages.snapshot -> pages.list;
  page.respond delivers a staff reply to the player (online: a gump now; offline:
  queued for next login; shows as "Staff") and can close; page.close removes it.
- BridgeConfig/Bridge.cfg: PageSweepSeconds. BridgeBoot: reload re-arms the poll,
  status reports it.
- sidecar/src/web.rs: GET /pages, POST /pages/{id}/respond, POST /pages/{id}/close.
- INTEGRATION.md: page events (§4) and endpoints (§6).
- tools/scaffolding/BridgePageProbe.cs: gated headless verification.

Verified live (probe-seeded tickets): snapshot returns the queue, the poll emits
page.new for both and page.closed on removal, respond -> 200, close removes the
page, unknown page -> 404.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
This commit is contained in:
2026-07-13 02:54:19 -05:00
parent 11169c52a6
commit 17e1c91fb3
8 changed files with 572 additions and 1 deletions

View File

@@ -185,6 +185,15 @@ Every event has `t` (epoch ms) and `kind`. A nested actor object looks like `{"s
|------|--------|-------|
| `link.request` | `code`, `account`, `char`, `ttlSec` | A player ran `[link` in game. Show them a prompt to enter `code` on the site; you then confirm it via `POST /link/confirm`. See §6. |
#### Help-page (support) queue
| kind | fields | notes |
|------|--------|-------|
| `page.new` | `pageId`, `sender`, `type`, `message`, `map`, `x`,`y`,`z`, `sentMs`, `handled`, `handler` | A player opened a help page (support ticket). `pageId` is the sender's serial (one page per player). `type` is `Bug`/`Stuck`/`Account`/`Question`/`Suggestion`/`Other`/`VerbalHarassment`/`PhysicalHarassment`. `sender` is the usual actor object (with `webId` if the account is linked). |
| `page.updated` | same as `page.new` | A page's handled state changed (a staffer claimed/released it in game). |
| `page.closed` | `pageId` | The page left the queue (resolved, cancelled, or the player logged out). |
The queue has no in-game event, so it's polled (`PageSweepSeconds`, default 5s) — expect a few seconds' latency, and use `GET /pages` for the authoritative current queue on connect. See §6 to snapshot, respond, and close.
---
## 5. REST — read queries
@@ -351,6 +360,31 @@ Each applied action also emits an unsolicited **`admin.audit`** frame on the Web
`origin:"web"`, so every connected dashboard — not just the caller — sees it. In-game moderation
by staff in the game client surfaces the same way with `origin:"in-game"`.
### Help-page (support) queue
Read the open queue, respond to a player, or close a page. Staff-facing — gate behind your own
roles, like the moderation endpoints above.
```
GET /pages # the open queue, newest state
POST /pages/{pageId}/respond { "message":"...", "close": false }
POST /pages/{pageId}/close
```
- **GET /pages** → `pages.list` with a `pages` array; each entry is the same shape as a `page.new`
event's fields (§4). This is the authoritative queue — use it on (re)connect, then keep it live
with the `page.new` / `page.updated` / `page.closed` events.
- **respond** delivers a message to the player exactly as an in-game staff reply does: a gump now if
they're online, otherwise queued for their next login. It shows as coming from "Staff". Pass
`"close": true` to resolve the page in the same call. → **200** `page.ok`.
- **close** removes the page from the queue. → **200** `page.ok`.
- Unknown `pageId` → **404** `page.error`; a respond with no `message` → **400**.
```json
POST /pages/0x24C/respond { "message": "A GM is on the way.", "close": true }
→ { "kind":"page.ok", "action":"respond", "pageId":"0x24C", "closed":true }
```
### History (from the sidecar's database)
```