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:
@@ -63,6 +63,10 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
|
||||
.route("/admin/ban", post(admin_ban))
|
||||
.route("/admin/unban", post(admin_unban))
|
||||
.route("/admin/broadcast", post(admin_broadcast))
|
||||
// Help-page (support) queue: snapshot the open queue, respond to / close a page.
|
||||
.route("/pages", get(pages_list))
|
||||
.route("/pages/:id/respond", post(page_respond))
|
||||
.route("/pages/:id/close", post(page_close))
|
||||
// History, read from SQLite rather than the shard.
|
||||
.route("/history", get(history))
|
||||
.route("/economy", get(economy))
|
||||
@@ -330,6 +334,45 @@ async fn admin_broadcast(State(st): State<AppState>, Json(body): Json<Value>) ->
|
||||
admin_call(&st, "admin.broadcast", body).await
|
||||
}
|
||||
|
||||
// ---- help-page queue handlers ----
|
||||
|
||||
/// The open help-page queue, correlated on reqId. Returns a pages.list.
|
||||
async fn pages_list(State(st): State<AppState>) -> impl IntoResponse {
|
||||
let req_id = st.rpc.next_req_id();
|
||||
let cmd = json!({"kind": "pages.snapshot", "reqId": req_id});
|
||||
respond(st.rpc.call(&st.shard, cmd, &req_id).await)
|
||||
}
|
||||
|
||||
/// Body: {"message":"...","close":<bool, optional>}. Delivers a staff response to the player.
|
||||
async fn page_respond(
|
||||
State(st): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(body): Json<Value>,
|
||||
) -> impl IntoResponse {
|
||||
let message = body.get("message").and_then(|m| m.as_str()).unwrap_or_default();
|
||||
if message.trim().is_empty() {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "message is required"})),
|
||||
);
|
||||
}
|
||||
let close = body.get("close").and_then(|c| c.as_bool()).unwrap_or(false);
|
||||
|
||||
let req_id = st.rpc.next_req_id();
|
||||
let cmd = json!({
|
||||
"kind": "page.respond", "reqId": req_id,
|
||||
"pageId": id, "message": message, "close": close
|
||||
});
|
||||
respond(st.rpc.call(&st.shard, cmd, &req_id).await)
|
||||
}
|
||||
|
||||
/// Removes a page from the queue.
|
||||
async fn page_close(State(st): State<AppState>, Path(id): Path<String>) -> impl IntoResponse {
|
||||
let req_id = st.rpc.next_req_id();
|
||||
let cmd = json!({"kind": "page.close", "reqId": req_id, "pageId": id});
|
||||
respond(st.rpc.call(&st.shard, cmd, &req_id).await)
|
||||
}
|
||||
|
||||
// ---- query handlers ----
|
||||
|
||||
async fn char_by_slot(
|
||||
|
||||
Reference in New Issue
Block a user