feat(demo): list the demo's sign-ins on /beta and link into it per game
Some checks failed
PR checks / checks (pull_request) Failing after 2m16s

The public demo (demo.runicgateway.com) is live, so the site's demo slot
can be switched on. Two decisions of record, PLAN.md §15:

- D82: /beta reads the demo's GET /api/v1/public/settings demo.logins on
  the server, caches it for a minute, and lists the published sign-ins
  with the next wipe. If the demo does not answer, /beta links to its
  sign-in page instead. Gate two reads as in place.
- D83: a deep link into a game module's pages carries data-demo-game, and
  applyBrand.mjs fills it only for games in the new mounted demoGames
  list. The Rust features gain links into /rust and /rust/servers/demo.
  UO's stay hidden on a Rust demo.

The public-demo not-built entry is removed, because its resolvedBy was met.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-28 15:32:34 -05:00
parent 8701126d64
commit 769d213c80
18 changed files with 650 additions and 63 deletions

View File

@@ -193,17 +193,38 @@ if (demoFrom !== demoTo) {
* because `global.css` hides `[data-demo-url='']` — the visibility rule stays one rule for
* both kinds of link, and only the `href` differs.
*/
const DEEP_LINK = /href="[^"]*" data-demo-url="[^"]*" data-demo-path="([^"]*)"/g;
const DEEP_LINK = /href="[^"]*" data-demo-url="[^"]*" data-demo-path="([^"]*)"(?: data-demo-game="([^"]*)")?/g;
const deepLinkTo = (demoPath) => {
const href = demoTo ? `${demoTo.replace(/\/+$/, '')}${demoPath}` : '';
/**
* Which games the demo runs (D83). A deep link into a game module's pages carries that
* game's id in `data-demo-game`, and it is filled only when the mounted `demoGames` names
* it: the demo is a Rust deployment, and a `/uo/market` link into it is a 404. Core's own
* links (`/wiki`, `/site/events`) carry no game and follow `demoUrl` alone, because every
* deployment has them.
*
* An absent or malformed list means no game's links, not every game's. A demo whose games
* nobody named is one whose game links cannot be known to work.
*/
const gamesOf = (value) => (Array.isArray(value) ? value.filter((id) => typeof id === 'string') : []);
const gamesFrom = gamesOf(previous.demoGames);
const gamesTo = gamesOf(resolved.demoGames);
const gamesChanged = gamesFrom.join(',') !== gamesTo.join(',');
if ('demoGames' in mounted && !Array.isArray(mounted.demoGames)) {
console.warn('[brand] the mounted "demoGames" is not a list of game ids — no game links will show.');
}
const deepLinkTo = (demoPath, game) => {
const root = !game || gamesTo.includes(game) ? demoTo : '';
const href = root ? `${root.replace(/\/+$/, '')}${demoPath}` : '';
return (
`href="${escapeHtml(href)}" data-demo-url="${escapeHtml(demoTo)}" ` +
`data-demo-path="${demoPath}"`
`href="${escapeHtml(href)}" data-demo-url="${escapeHtml(root)}" ` +
`data-demo-path="${demoPath}"` +
(game === undefined ? '' : ` data-demo-game="${game}"`)
);
};
if (!replacements.length) {
if (!replacements.length && !gamesChanged) {
console.log('[brand] mount matches what is already applied; nothing to rewrite.');
process.exit(0);
}
@@ -303,8 +324,8 @@ for (const file of walk(CLIENT)) {
// After the literal swaps, never before: the plain-slot replacement also matches the
// first two attributes of a deep link, so it runs first and this pass corrects the
// `href` it just wrote. Recomputing rather than replacing is what makes that safe.
after = after.replace(DEEP_LINK, (whole, demoPath) => {
const rebuilt = deepLinkTo(demoPath);
after = after.replace(DEEP_LINK, (whole, demoPath, game) => {
const rebuilt = deepLinkTo(demoPath, game);
if (rebuilt !== whole) counts.set('demoDeep', counts.get('demoDeep') + 1);
return rebuilt;
});
@@ -330,6 +351,9 @@ if (counts.get('demoDeep')) {
` ${'demoUrl deep'.padEnd(14)} ${demoTo ? `linked -> ${demoTo}/…` : 'links hidden'} (${counts.get('demoDeep')}x)`
);
}
if (gamesChanged) {
console.log(` ${'demoGames'.padEnd(14)} ${gamesTo.length ? gamesTo.join(', ') : 'no game links'}`);
}
if (inlineCollisions.length) {
console.error(