Expose a small backend identity/version descriptor (§8.4 of the Android plan)
so a client can positively recognize a Runic Gateway backend on first-run and
run a version-mismatch guard, instead of inferring from an incidental shape.
- New config/version.js: { service: 'runic-gateway', api: 'v1', server: <pkg> }.
- GET /public/status now includes a `version` block (the app already calls this
on first-run, so it gets identity + version in one round trip).
- New GET /public/version: a lightweight, DB-free identity endpoint — the
canonical target for the version guard and a cheap liveness check.
- Swagger: PublicVersion schema + version on PublicStatus; /version annotated.
- test/publicVersion.test.js covers the config shape and the DB-free 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
The public "Staff online" list on the Shard page exposed each staff
member's in-game location (map + coordinates) to everyone, including
logged-in players and unauthenticated visitors.
Location is now privileged data:
- Server: getOnline inspects the caller's role via getUserFromRequest
(the same non-rejecting helper siteMode uses on public routes) and
only includes map/x/y/z for admin/moderator callers. For everyone
else the fields are omitted from the JSON entirely, so they can't be
read from the network tab. serial + name (online status) still shown.
- Client: Shard.jsx gates the location span on the viewer's role from
useAuth() (same pattern as RoleGate); non-privileged viewers see who
is online but no location field is rendered.
Tests: publicShardOnline.test.js covers admin + moderator (location
included), player + unauthenticated + editor (location omitted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmHdsbnLzDMAVQkAoTQSBe
Per request, split the single public house registry into three role-scoped views:
- Public /site/houses → only houses in DANGER (IDOC), by LOCATION (region + map/
coords). No owner, price, co-owners or decay detail. Renamed "Houses in danger";
kept live via the public house.decay feed. The full-registry deltas
(house.update / house.remove — which carry owner/price) are REMOVED from the
public SSE allowlist so they never reach the public channel.
- Staff full registry → new /admin/houses (admin + moderator, RoleGate + MOD_PATHS)
backed by GET /admin/shard/houses (modAccess), with owner/price/co-owners/decay
and search, kept live on the admin SSE channel.
- Player portal → "My houses" home-status section (own houses only, with decay/
IDOC status) via GET /player/shard/houses, scoped to the caller's linked accounts.
Server tests green, client build clean, swagger regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 of the Protocol 2.0/2.1 integration: the read/ingest backend for the four
new uo-link boards, following the established champs/pages pattern (ingest → our
MariaDB + snapshot-on-reconnect + public SSE + token-free public endpoint).
- Schema: shard_guilds, shard_governors, shard_governor_terms, shard_presence;
extend shard_houses with the house.update registry columns (owner_name,
co_owners, friends, price, decay, in_registry) so the decay-transition and
registry feeds share one house row without clobbering each other.
- Ingest: route guild.update/remove, city.update, presence.online,
house.update/remove; log guild.join (real-time joins feed); region.enter is
broadcast-only. All new public kinds added to the SSE allowlist.
- Governor term history captured from day one: on every observed governor CHANGE
the open term is closed and a new one opened, idempotent so backfill/duplicate
city.update never spawn spurious terms. votes stays NULL (the feed carries only
candidate count, not tallies) — we never fabricate vote numbers.
- Client + backfill: getGuilds/getGovernors/getHouses/getPresence; snapshot each
board on every WS (re)connect, independently guarded so an empty/failed board
(e.g. no City Loyalty) never wipes another.
- Public endpoints: /shard/{guilds,governors,governors/:city/history,presence,houses}.
- Tests: ingest routing for all new kinds + governor term-capture idempotency
(15 new; full suite 179/179). Swagger regenerated.
Refs .plans/protocol2-integration.md (Phase 1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire up the three uo-link sidecar surfaces that weren't integrated yet.
Champion spawns
- Ingest champ.update/champ.remove into a new shard_champs table (served from
our own store, like online/houses); public /site/champs board with a nav link,
live via the existing SSE feed (champ.* added to the public allowlist).
Staff write plane (admin + moderator)
- kick / ban / unban / broadcast via /admin/shard/*; actor is stamped server-side
from the session, never the browser. Sidecar status codes mapped (403 disabled/
protected, 404 unknown, 503/504 transient). admin.audit events are logged and
surfaced at /admin/shard/audit.
- New admin "In-Game Ops" view (/admin/shard-ops), plus per-account Kick/Ban/Unban
on the user-detail and character views (ShardAccountActions, self-gated to staff).
Help-page (support) queue
- Ingest page.new/updated/closed into a new shard_pages table; respond/close via
/admin/shard/pages/*. Champ board and page queue are snapshotted from the
sidecar's /champs and /pages on every WS (re)connect (guarded so a failed call
never wipes local state).
Verified live end-to-end against MariaDB + the Rust sidecar + ServUO; unit tests
cover ingest routing (shardIngest.champsPages.test.js). Swagger regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
Public "Online now" now lists only players whose game account is linked
to a STAFF website user (admin/editor/moderator) — linked players are no
longer exposed publicly with their name and location. listOnlineLinked
joins through to users and filters on role; the section is relabeled
"Staff online".
Character/roster/vendor reads gain an admin bypass: admins may view any
character's data, while players (and editor/moderator staff) stay limited
to accounts they have personally linked. The bypass lives in the shared
player controller and only ever widens access for genuine admins.
Also finalizes the uo-link character/vendor front end (player + admin
character sheets, VendorSales component, ShardChar removed) and
regenerates swagger-output.json.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kj5s1QCKobuFPYmqxjy1q
Front ends for the rest of the sidecar data, plus a security fix the live data
surfaced.
- lib/shardEvents.js: shared describe()/category/label for every event kind
(sales, deaths & PvP, skills, fame/karma, quests, world, and staff kinds).
- Public /site/shard/activity (ShardActivity): the full event log with category
filter tabs and a live tail (history + SSE merged, de-duped). Linked from the
Shard page. Shard page now reuses the shared describe().
- Admin: a "Live feed (all events)" panel on the Shard admin page subscribing to
the admin SSE channel — shows every kind incl. audit/cheat/login attempts.
useShardFeed generalized to take a stream url; api.adminShardStreamUrl added.
Security fix: GET /public/shard/feed now restricts to the public-safe kind
allowlist (shardEvents.list gains a `kinds` IN-filter). Previously it returned
whatever was logged — including audit.* / cheat.* / link.request. Those are
still stored for the admin channel but never served publicly (verified: a
public request for audit.command returns 0 rows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3
Turns the raw shard endpoints into proper, navigable pages in the site's visual
language.
- components/CharacterSheet.jsx: reusable sheet — attribute tiles, vitals bars,
resistances, skills (with bars), and equipment — styled with the shared
panel/grid vocabulary.
- Player portal with a nav bar: PlayerPortalLayout (Characters / Account tabs +
sign-out) wraps /player and /account. /player (PlayerCharacters) tells the
logged-in player if they haven't linked a game account (with the [link code
prompt) or, once linked, shows their characters grouped by account; each
character opens its sheet at /player/char/:serial. Account security moved into
the same shell (the buried "Game accounts" block was removed from it).
Login/register now land on /player.
- Public: GET /public/shard/online (redacted name+serial+map) drives an
"Online now" list on /site/shard that links to public character sheets at
/site/shard/char/:serial (ShardChar). Swagger: ShardOnlinePlayer + regenerated.
- api.shard.online added.
Verified live against the running shard: Darrow's full sheet (STR 120, 58
skills, 3 equipment) renders through the browser-facing proxy; the online list
returns the live roster; player routes 401 without a session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3
Curated, same-origin, token-free reads so the browser never sees the sidecar
URL or token:
- public/shard.controller.js:
- GET /public/shard/status — connection state + online count + latest economy
(from the site's ingested data).
- GET /public/shard/feed?kind=&limit= — recent notable events from the log.
- GET /public/shard/economy — gold-supply series (oldest → newest).
- GET /public/shard/idoc — houses currently at IDOC.
- GET /public/shard/char/:serial — live sheet round-trip via uoLinkClient,
briefly cached; 503 (shard restarting) serves a stale cache or a retry
banner rather than an error.
- GET /public/shard/stream — public SSE channel (safe kinds only).
- Wired into public.routes.js with express-validator guards and #swagger
annotations; new "Public · Shard" tag + ShardStatus/ShardEvent/
ShardEconomyPoint/ShardHouse schemas; swagger-output.json regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3
Backend for the CMS page builder, all under the existing /api/v1:
- pages.model: authoritative save gate — validates blocks against the
registry and sanitizes them on every create/update; maps rows to/from the
grouped API shape (metadata / settings); slug validated + reserved-checked
at create and immutable after; `protected` can be set true via PATCH but
only cleared via the unprotect path; published_at stamped on first publish.
- sanitizeBlocks: post-validation normalizer (applies each block's sanitize,
stamps version, defaults visible, recurses container slots).
- reservedSlugs: guards page slugs from shadowing named routes/API namespaces.
- Admin routes (staff-gated): GET/POST /pages, GET/PATCH/DELETE /pages/:id,
POST /pages/:id/unprotect (password step-up, verified against the caller's
own hash, never logged), POST /pages/:id/preview (1h token). Audit-logs
create/publish/unpublish/protect/unprotect/delete.
- Public routes: GET /public/pages/:slug (published; staff see drafts; site-
mode gated) and GET /public/pages/:id/preview/:token (ungated, token is the
access control). Preview token primitives added to auth/token.js.
- Swagger annotations for all new endpoints.
Verified end-to-end: model integration test against the dev DB (sanitize,
invalid-block rejection, slug immutability, protected/unprotect, dup/reserved
slug, published_at) + authenticated HTTP smoke (201 create, 400 invalid
blocks, publish, public slug fetch, preview mint+fetch, 403 delete-protected,
401 wrong-password unprotect).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The route-level annotations were 100% present, but the committed/served
spec (swagger-output.json) was stale and several response schemas had
drifted from the controllers. This aligns the docs with actual behavior
and regenerates the spec.
Served spec was stale (64/67 operations). Regenerating picks up three
routes that were added after the last generation:
- POST /api/v1/auth/sso/totp
- GET /api/v1/admin/discord-bot/config
- PUT /api/v1/admin/discord-bot/config
plus a stale /auth/logout summary.
Response-shape corrections (annotation now matches controller output):
- Mutation endpoints do NOT return the generic { message } envelope.
Deletes echo { id } / { slug }; toggles return { deleted },
{ unlinked }, { totp_enabled }, or { ip, removed }. Documented as-is
via new DeletedId/DeletedSlug/DeletedFlag/UnlinkedFlag/TotpState/
UnbanResult components. (The API is intentionally inconsistent here;
recorded rather than normalized — see follow-up note.)
- POST /account/totp/setup: otpauth_url -> otpauthUrl (TotpSetup)
- PUT /admin/site-mode: { mode } -> { site_mode, changed_at, changed_by }
- GET /account: full User -> AccountStatus (id/username/role/totp_enabled)
- GET /account/identities: add linked_at (LinkedIdentity)
- GET /public/status: add status_message (PublicStatus)
- POST /auth/sso/totp: user is SafeUser, not full User
- GET /dashboard: description/shape corrected (posts+users, no wiki)
Schema completeness:
- Provider (public discovery): { id, name, icon, loginUrl, priority },
not { id, name, kind }
- ProviderConfig: add hasSecret, builtin, health (ProviderHealth)
- Post: add excerpt, author_id, published_at
- MobileTokenResponse.expiresIn: duration string ("15m"), not integer
Config: declare the Admin · Discord Bot tag (was used but undeclared).
Auth model and the internal/external boundary were verified correct and
left unchanged: cookie + bearer are both accepted on session routes (dual
security annotations are accurate), and /internal/* runs on a separate
listener already excluded from the scan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
Generate an OpenAPI 3.0 spec from route annotations and serve it with
Swagger UI so the full REST API is browsable and testable.
- Add swagger-ui-express (runtime) and swagger-autogen (dev) deps, plus
an `npm run swagger` script.
- server/swagger/swagger.js: generator config with API metadata, servers,
14 tag groups, cookie + bearer security schemes, and 28 reusable
component schemas. Follows the Express mount chain from src/app.js so
generated paths are fully-qualified (/api/v1/...).
- Annotate every route (auth, mobile, sso, public, admin, health) with
#swagger tags/summaries/parameters/request bodies/security and the
actual response codes each handler returns (400/401/403/404/409/429/
302/502, multipart uploads).
- Serve Swagger UI at /api/docs and the raw spec at /api/docs.json,
guarded so a missing spec disables docs instead of crashing.
- Commit the generated swagger-output.json so docs work with no build
step; swagger-autogen stays dev-only and is not needed at runtime.
- README: new "API documentation (Swagger)" section plus tech-stack and
project-structure entries.
Covers 51 paths / 64 operations. Existing test suite (83) still passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final phase of the wiki upgrade (see WIKI_UPGRADE.md).
Schema (additive): wiki_revisions table (per-save content snapshots).
The FULLTEXT index on wiki_pages(title, body) shipped in Phase 1.
Search:
- MATCH ... AGAINST natural-language search over title + body, ordered by
relevance
- public: GET /public/wiki?q= (published only); admin: GET /admin/wiki?q=
(all statuses)
- public wiki index gains a search box; admin list gains a search field
Revision history:
- every create/update snapshots the page into wiki_revisions
- admin endpoints: list revisions, get one, and restore (restore overwrites
the page, rebuilds links, and appends a new revision — history stays
append-only); logged as wiki.revision.restore
- editor gains a History modal: revision list + word-level diff (jsdiff) of a
chosen revision against the current page, with one-click restore
Verified end-to-end: search matches body and title; two edits produce three
revisions; diff renders added/removed words; restore reverts and records a new
revision. No console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Connectivity phase of the wiki upgrade (see WIKI_UPGRADE.md).
Schema (additive new tables): wiki_tags, wiki_page_tags, wiki_links.
Internal links & backlinks:
- new wiki.links.js parses a saved body for /wiki/<slug> (and data-wiki-slug)
targets; wiki_links is rebuilt on every save
- article shows a "Linked from" section (published backlinks) and renders
links to non-existent pages as red links (server returns missing_links)
- editor gains an internal-link picker listing existing pages
Tags:
- pages accept a tags[] array; tags upsert on save, page tag-set is replaced,
and orphaned tags are auto-pruned (on save and delete)
- public/admin list filter by ?tag=; /wiki/tags lists tags with published counts
- article shows tag chips; the index has a flat tag-filtered view; editor has a
comma-separated tags field
Verified end-to-end: A->B backlink appears, red link detected, link index
rebuilds on edit, tag filtering + chips + pruning all work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Foundation & safety phase of the wiki upgrade (see WIKI_UPGRADE.md).
Schema (additive, idempotent via ensureSchema):
- new wiki_categories table; wiki_pages gains category_id, excerpt,
published, published_at, sort_order, and a FULLTEXT index
- migration ALTERs guarded with IF NOT EXISTS for existing databases
- seed reworked into 4 sections with the 8 starter pages assigned
Security:
- new utils/sanitizeHtml.js (sanitize-html allowlist); wiki bodies are
sanitized on every save, and the article renders through DOMPurify
- strips <script>, event handlers (onerror), and javascript: URLs
Backend:
- public: published-only list with ?category filter + /wiki/categories
- admin: extended page CRUD, PATCH publish toggle, category CRUD;
drafts visible to admin, hidden from public
- all writes logged to activity_log
Frontend:
- data-driven public wiki index (sections + real descriptions; removed
hardcoded blurbs/Roman numerals) with ?category filtering
- article: category breadcrumb + sanitized render
- admin: Section/Status columns, draft/publish + section + excerpt in the
editor, and a Manage sections modal
Verified end-to-end against MariaDB 11: migration clean, XSS neutralized,
drafts hidden, client builds, server boots.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>