// ── 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/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, stream }