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

@@ -0,0 +1,61 @@
using System;
using Server.Accounting;
using Server.Engines.Help;
namespace Server.Custom
{
/// <summary>
/// Puts a couple of genuine PageEntry tickets into the help-page queue so BridgePages
/// (poll/stream + snapshot + respond/close) can be verified without a game client.
///
/// The enqueue is real (PageQueue.Enqueue). The only accommodation for the missing client:
/// each entry's InternalTimer would remove the page on its first tick because the sender has
/// no NetState (PageQueue.cs:167 treats "no NetState" as a logout), so we call PageEntry.Stop
/// to keep the ticket in the queue for the test. Everything the bridge does — detect, snapshot,
/// respond, close — then operates on real queue entries.
///
/// Test scaffolding. Never deployed. Gated behind Bridge.PageProbeOnStart.
/// </summary>
public static class BridgePageProbe
{
public static void Initialize()
{
if (Config.Get("Bridge.PageProbeOnStart", false))
EventSink.ServerStarted += () => Timer.DelayCall(TimeSpan.FromSeconds(4.0), Run);
}
private static void Run()
{
try
{
Enqueue("seed_030", "My quest is stuck, please help.", PageType.Stuck);
Enqueue("seed_031", "Found a bug with a player vendor.", PageType.Bug);
Console.WriteLine("[PageProbe] done");
}
catch (Exception ex)
{
Console.WriteLine("[PageProbe] FAILED: " + ex);
}
}
private static void Enqueue(string account, string message, PageType type)
{
var acct = Accounting.Accounts.GetAccount(account) as Account;
var sender = acct == null ? null : acct[0];
if (sender == null)
{
Console.WriteLine("[PageProbe] {0} has no character in slot 0; seed the world first", account);
return;
}
var entry = new PageEntry(sender, message, type);
PageQueue.Enqueue(entry);
entry.Stop(); // keep it in the queue despite the offline sender
Console.WriteLine("[PageProbe] enqueued {0} page for {1} (0x{2:X})",
type, account, sender.Serial.Value);
}
}
}