// ── Public: shard live data ──────────────────────────────────────────────── // // Same-origin, token-free read endpoints backed by the data the WS ingest // pipeline persists (shard_online / shard_events / shard_economy / shard_houses) // plus a live character round-trip to the sidecar. The browser never sees the // sidecar URL or token — every sidecar call is server-side (uoLinkClient). // // The stored-data endpoints are cheap DB reads. The live /char endpoint hits the // running shard, so it is briefly cached and degrades gracefully: a 503 (shard // restarting) surfaces as a retry-able banner rather than an error. const shardEvents = require('../../../model/shardEvents/shardEvents.model') const shardState = require('../../../model/shardState/shardState.model') const uoLinkConfig = require('../../../model/uoLinkConfig/uoLinkConfig.model') const broadcast = require('../../../utils/shardBroadcast') const log = require('../../../utils/logger')('public-shard') // GET /public/shard/status — connection state + online count + latest economy. async function getStatus(req, res) { try { const config = await uoLinkConfig.getSafe() const [online, economy] = await Promise.all([ shardState.onlineCount(), shardState.latestEconomy(), ]) return res.json({ enabled: config.enabled, status: config.status, pluginConnected: config.pluginConnected, lastEventAt: config.lastEventAt, onlineCount: online, economy, }) } catch (err) { log.error('shard.getStatus', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // GET /public/shard/feed?kind=&limit= — recent notable events from the log, // restricted to the public-safe allowlist so staff audit / cheat / link events // (which are stored for the admin channel) can never leak to the public. async function getFeed(req, res) { try { const { kind, limit } = req.query let events if (kind) { // A specific kind is only served if it is itself public-safe. if (!broadcast.PUBLIC_KINDS.has(kind)) return res.json([]) events = await shardEvents.list({ kind, limit }) } else { events = await shardEvents.list({ kinds: [...broadcast.PUBLIC_KINDS], limit }) } return res.json(events) } catch (err) { log.error('shard.getFeed', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // GET /public/shard/economy — gold-supply series, oldest → newest. async function getEconomy(req, res) { try { return res.json(await shardState.listEconomy(req.query.limit)) } catch (err) { log.error('shard.getEconomy', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // GET /public/shard/online — players online now whose account is linked to a // STAFF website user (admin/editor/moderator). Shows name + location (map + // coordinates); no vitals or account. Non-staff players are never listed. async function getOnline(req, res) { try { const rows = await shardState.listOnlineLinked() return res.json(rows.map((r) => ({ serial: r.serial, name: r.name, map: r.map, x: r.x, y: r.y, z: r.z }))) } catch (err) { log.error('shard.getOnline', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // GET /public/shard/idoc — houses currently in danger (stage IDOC). async function getIdoc(req, res) { try { return res.json(await shardState.listIdoc()) } catch (err) { log.error('shard.getIdoc', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // GET /public/shard/champs — the current champion-spawn board (all categories). // Served from our own store; live deltas (champ.update / champ.remove) arrive on // the public SSE stream so the page can update in place. async function getChamps(req, res) { try { return res.json(await shardState.listChamps()) } catch (err) { log.error('shard.getChamps', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // GET /public/shard/guilds — the current guild board. Served from our store; // live via guild.update / guild.remove / guild.join on the public SSE stream. async function getGuilds(req, res) { try { return res.json(await shardState.listGuilds()) } catch (err) { log.error('shard.getGuilds', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // GET /public/shard/governors — the current town-governor board (empty on shards // without City Loyalty). Live via city.update on the public SSE stream. async function getGovernors(req, res) { try { return res.json(await shardState.listGovernors()) } catch (err) { log.error('shard.getGovernors', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // GET /public/shard/governors/:city/history — the term ledger for one city // (look-back: "who were all the governors of Britain?"), newest first. async function getGovernorHistory(req, res) { try { return res.json(await shardState.listGovernorHistory(req.params.city, req.query.limit)) } catch (err) { log.error('shard.getGovernorHistory', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // GET /public/shard/presence — the online-population aggregate (count + per-facet // + per-region). Live via presence.online on the public SSE stream. async function getPresence(req, res) { try { return res.json(await shardState.latestPresence()) } catch (err) { log.error('shard.getPresence', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // GET /public/shard/houses — the house registry (every house we've seen via // house.update). Live via house.update / house.remove on the public SSE stream. async function getHouses(req, res) { try { return res.json(await shardState.listHouses()) } catch (err) { log.error('shard.getHouses', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // GET /public/shard/stream — public live-event SSE channel (safe kinds only). function stream(req, res) { broadcast.subscribe(req, res, 'public') } module.exports = { getStatus, getFeed, getEconomy, getOnline, getIdoc, getChamps, getGuilds, getGovernors, getGovernorHistory, getPresence, getHouses, stream, }