Files
website/server/src/router/v1/public/index.js
wtclaude 7c769ea8fd feat(atlas): serve the spawn atlas and give operators a panel for it
Protocol 3.0 order 3 (Part C), second of two website PRs. #112 built the data
pipeline; this makes it reachable — six public routes, five admin ones, two
public pages and an admin panel. Still website-only: no plugin, no sidecar, no
new event kinds, no wire change.

The API sits at /api/v1/public/atlas, not under /public/shard. Nothing here
touches the sidecar, so the pages stay complete while the shard is down, and a
/shard prefix would imply a dependency the atlas does not have. Unlike /shard/*
it IS site-mode gated, like /posts and /wiki: a bestiary is site content.

Every route carries requireFeature('atlas') and projects its response. The atlas
feature declares no sensitive fields, so the projection is a no-op today — the
call is there because v3.md 3.6.1's rule is that the FIRST field needing a gate
should be covered by construction rather than by a retrofit.

Two bugs the UI surfaced, both fixed here:

Respawn delays were stored in the wrong unit, sometimes. XmlSpawner writes
MinDelay/MaxDelay in minutes and switches to seconds only when a delay does not
divide into whole minutes, flagging it per record with DelayInSec. A `5` means
five minutes on one spawner and five seconds on the next, both plausible, and
the pipeline stored the raw number. 170 of 6,455 stock spawners are second
flagged. The parser normalises to seconds; the API and UI carry seconds.

That exposed the hash gate as a trap. "Has the tree changed?" is the wrong
question on its own: an install whose maps never change would have kept serving
the old readings forever, because the only thing compared was the tree.
PARSER_VERSION is now stored beside the source hashes and a mismatch counts as
drift, so any future parse correction lands on the next boot.

Also renamed the detail route's spawn-point array to `spawners` — it was
`points`, which is the COUNT on the search route, so one key meant a number in
one place and an array in the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
2026-07-28 19:51:22 -05:00

50 lines
2.4 KiB
JavaScript

// /api/v1/public — the anonymous public surface, assembled from per-capability
// routers.
//
// This file owns the mount table and nothing else; no route is declared here.
// Each capability router mounts at the prefix it already owned inside the old
// monolithic public.routes.js, so the emitted URL set is byte-identical — proved
// by a zero-line diff in server/routes.manifest.json (`npm run routes:manifest`).
//
// **There is deliberately no group gate.** Unlike /admin (staffOnly) and /player
// (requireAuth), this group is unauthenticated by design and must stay that way:
// the SPA renders logged-out, the Discord bot reads it anonymously, and the
// Android app's ShardStreamClient consumes /public/shard/stream with no
// Authorization header. Content visibility during maintenance is handled by the
// per-route `siteMode` middleware, not by an auth gate.
//
// See docs/website/API_V2_PLAN.md § Phase 2 for the split.
const express = require('express')
const postsRouter = require('./posts.router')
const wikiRouter = require('./wiki.router')
const pagesRouter = require('./pages.router')
const shardRouter = require('./shard.router')
const atlasRouter = require('./atlas.router')
const siteRouter = require('./site.router')
const publicRouter = express.Router()
// Content. All three are site-mode gated per route (the /pages draft-preview
// route is the one deliberate exception — see pages.router.js).
publicRouter.use('/posts', postsRouter)
publicRouter.use('/wiki', wikiRouter)
publicRouter.use('/pages', pagesRouter)
// Live shard data, never site-mode gated.
publicRouter.use('/shard', shardRouter)
// The spawn atlas: static shard CONTENT, parsed from the shard's ServUO tree
// rather than fetched from the sidecar. Deliberately not under /shard — nothing
// here depends on the bridge — and site-mode gated per route like the content
// routers above, which is the other half of that distinction.
publicRouter.use('/atlas', atlasRouter)
// The four singletons that own no path segment of their own: /settings, /status,
// /version and /contact. Mounted at the group root, last — safe only because
// site.router.js declares no router-level middleware (a bare `use(gate)` in a
// root-mounted router runs for every request passing through toward another
// mount). Same arrangement as admin/dashboard.router.js.
publicRouter.use('/', siteRouter)
module.exports = publicRouter