@bongos/core 1.24.49 → 1.24.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.49",
3
+ "version": "1.24.51",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.49",
9
+ "version": "1.24.51",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.49",
3
+ "version": "1.24.51",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -11265,5 +11265,17 @@
11265
11265
  "id": "1005088",
11266
11266
  "text": "The Cloud Bongos logo and the browser-tab icon now come from one file, so every page, the hall, the welcome screen, the gates and the fundraising page show exactly the same mark, and phones get a proper home-screen icon."
11267
11267
  }
11268
+ ],
11269
+ "1.24.50": [
11270
+ {
11271
+ "id": "1005089",
11272
+ "text": "The public status page now looks like the rest of Cloud Bongos: same colours, fonts, logo and panels, with a dark mode, and its off-site backup copy is built from the same source so the two always match."
11273
+ }
11274
+ ],
11275
+ "1.24.51": [
11276
+ {
11277
+ "id": "1005090",
11278
+ "text": "The public status page now talks plainly: 'Top builders', 'Recently shipped', 'Spending' instead of the old ancient-city words, and costs show in dollars."
11279
+ }
11268
11280
  ]
11269
11281
  }
@@ -23,15 +23,19 @@
23
23
  // mirror, and the publish REFUSES rather than shipping a
24
24
  // page whose banner points nowhere.
25
25
  //
26
- // DESIGN: status.js + style.css + cursors/ are byte-identical between the
27
- // droplet copy and the mirror (#737 parameterized the one differing constant via
28
- // window.STATUS_API_BASE), so they are PURE COPIES. Only index.html carries the
29
- // three off-box additions, injected here at stable anchors:
26
+ // DESIGN: status.js + style.css + the shared sheets and brand files the page
27
+ // links (SHARED_ASSETS, at the same paths) are byte-identical between the server
28
+ // copy and the mirror (#737 parameterized the one differing constant via
29
+ // window.STATUS_API_BASE), so they are PURE COPIES. index.html is built from the
30
+ // SAME functions the server serves it with (task 1005089): the server's brand-token
31
+ // stamp (applyBrandTokens) and its theme splice (themeMarkup — the pack's palette,
32
+ // faces, font links and favicon), so the mirror cannot drift from the live page.
33
+ // On top of that it carries the three off-box additions, injected at stable anchors:
30
34
  // 1. <script>window.STATUS_API_BASE = <domains.publicOrigin></script> (head)
31
35
  // 2. <div id="offbox-status">…</div> (after <body>)
32
36
  // 3. the availability-banner driver <script> (reads domains.statusDataUrl) (before status.js)
33
37
  // If any anchor is missing the script THROWS — a silent mis-sync is worse than
34
- // a failed run.
38
+ // a failed run. tests/status_mirror_sync.mjs holds the mirror equal to the live page.
35
39
  //
36
40
  // USAGE:
37
41
  // node scripts/status-mirror-sync.js --repo-dir <path-to-mirror-checkout> [--push]
@@ -48,35 +52,25 @@ const { execFileSync } = require('node:child_process');
48
52
 
49
53
  const REPO_ROOT = path.resolve(__dirname, '..');
50
54
  const SRC = path.join(REPO_ROOT, 'modules', 'status-ui', 'public');
51
- const { branding, clientBranding, applyBrandConditionals } = require('../src/branding');
52
- const { escapeHtml } = require('../src/html-escape');
53
-
54
- // Stamp instance-identity tokens into the mirror HTML. The off-box mirror is
55
- // served by a static Pages host, NOT the branding-injecting Node server — so this
56
- // script must reproduce serve-internal.applyBrandTokens (task 1705, ADR 0062 §3), else
57
- // the mirror would render literal {{worldName}}/{{currency}} placeholders. KEEP IN SYNC
58
- // with src/bongos/serve-internal.js applyBrandTokens. `brand` is the client-safe pack
59
- // and `publicOrigin` the resolved origin — both injected, never read ambiently, so the
60
- // transform stays pure and testable.
55
+ const { branding, clientBranding } = require('../src/branding');
56
+ const { stampHtml } = require('../src/asset-version');
57
+ // The server's own serving transforms: the mirror is built with them, not with a
58
+ // copy of them, so the off-box page and the live page are one page (task 1005089).
59
+ const { applyBrandTokens, themeMarkup } = require('../src/bongos/serve-internal');
60
+
61
+ // The shared files the page links from other modules, published at the SAME paths
62
+ // so the page's root-absolute hrefs resolve on the mirror's host as on the server.
63
+ const SHARED_ASSETS = [
64
+ 'modules/ui-design/public/tokens.css',
65
+ 'modules/hall-ui/public/controls.css',
66
+ 'modules/ui-design/public/brand',
67
+ ];
68
+
69
+ // Stamp instance-identity tokens into the mirror HTML: the server's applyBrandTokens
70
+ // (task 1705, ADR 0062 §3), with every origin INJECTED ('' when absent) so nothing is
71
+ // read ambiently and the transform stays pure and testable.
61
72
  function brandTokens(html, brand = {}, publicOrigin = '', buildersOrigin = '') {
62
- const world = (brand.identity && brand.identity.worldName) || '';
63
- const product = (brand.identity && brand.identity.productName) || '';
64
- const tagline = (brand.copy && brand.copy.tagline) || '';
65
- const cur = (brand.currency && brand.currency.label) || 'credits';
66
- const Cur = cur ? cur.charAt(0).toUpperCase() + cur.slice(1) : cur;
67
- const company = (brand.identity && brand.identity.company) || {};
68
- const esc = escapeHtml; // the one server escaper (task 1004876)
69
- return applyBrandConditionals(html, { companyName: company.name, companySite: company.site, companyTagline: company.tagline })
70
- .split('{{worldName}}').join(esc(world))
71
- .split('{{productName}}').join(esc(product))
72
- .split('{{Currency}}').join(esc(Cur))
73
- .split('{{currency}}').join(esc(cur))
74
- .split('{{tagline}}').join(esc(tagline))
75
- .split('{{companyName}}').join(esc(company.name))
76
- .split('{{companySite}}').join(esc(company.site))
77
- .split('{{companyTagline}}').join(esc(company.tagline))
78
- .split('{{publicOrigin}}').join(esc(publicOrigin || ''))
79
- .split('{{buildersOrigin}}').join(esc(buildersOrigin || publicOrigin || ''));
73
+ return applyBrandTokens(html, brand || {}, publicOrigin || '', buildersOrigin || publicOrigin || '', '', '', '');
80
74
  }
81
75
 
82
76
  // ---- the three off-box index.html injections -------------------------------
@@ -152,7 +146,9 @@ function buildMirrorIndex({ srcHtml, apiBase, statusDataUrl, brand }) {
152
146
  if (html.includes('offbox-status')) {
153
147
  throw new Error('modules/status-ui/public/index.html already contains offbox markup — the source should be the droplet (no-banner) copy. Aborting.');
154
148
  }
155
- html = injectOnce(html, '</head>', headInject(apiBase) + brandingInject(brand), 'before');
149
+ // The off-box globals, then the pack's theme exactly as the server splices it —
150
+ // last in <head>, after the page's own sheets, so the pack wins here as it does live.
151
+ html = injectOnce(html, '</head>', headInject(apiBase) + brandingInject(brand) + themeMarkup((brand || {}).theme), 'before');
156
152
  html = injectOnce(html, '<body>', BANNER_DIV, 'after');
157
153
  // Insert the driver right before the main status.js script tag.
158
154
  const scriptAnchor = '<script src="/status.js';
@@ -188,17 +184,11 @@ if (require.main === module) {
188
184
  fs.copyFileSync(path.join(SRC, rel), path.join(repoDir, rel));
189
185
  console.error(` copied ${rel}`);
190
186
  };
191
- const copyDir = (rel) => {
192
- const from = path.join(SRC, rel), to = path.join(repoDir, rel);
193
- if (!fs.existsSync(from)) return;
194
- fs.rmSync(to, { recursive: true, force: true });
195
- fs.cpSync(from, to, { recursive: true });
196
- console.error(` copied ${rel}/`);
197
- };
198
187
 
199
188
  console.error('[status-mirror-sync] building mirror from modules/status-ui/public/ …');
200
189
  const indexHtml = buildMirrorIndex({
201
- srcHtml: fs.readFileSync(path.join(SRC, 'index.html'), 'utf8'),
190
+ // the content-hash ?v= stamp the server serves the index with (#521)
191
+ srcHtml: stampHtml(fs.readFileSync(path.join(SRC, 'index.html'), 'utf8'), { assetDir: SRC, urlPrefix: '/' }),
202
192
  apiBase: b.domains.publicOrigin,
203
193
  statusDataUrl: b.domains.statusDataUrl,
204
194
  brand: clientBranding(),
@@ -207,12 +197,21 @@ if (require.main === module) {
207
197
  console.error(' wrote index.html (with off-box injections)');
208
198
  copyFile('status.js');
209
199
  copyFile('style.css');
210
- copyDir('cursors');
200
+ // the retired bronze cursors (task 1005089) leave the mirror too
201
+ fs.rmSync(path.join(repoDir, 'cursors'), { recursive: true, force: true });
202
+ for (const rel of SHARED_ASSETS) {
203
+ const from = path.join(REPO_ROOT, rel), to = path.join(repoDir, rel);
204
+ fs.rmSync(to, { recursive: true, force: true });
205
+ fs.mkdirSync(path.dirname(to), { recursive: true });
206
+ fs.cpSync(from, to, { recursive: true });
207
+ console.error(` copied ${rel}`);
208
+ }
211
209
 
212
210
  // ---- commit + (optionally) push -------------------------------------------
213
211
 
214
212
  const git = (args) => execFileSync('git', ['-C', repoDir, ...args], { encoding: 'utf8' });
215
- git(['add', 'index.html', 'status.js', 'style.css', 'cursors']);
213
+ git(['add', '-A', '--', 'index.html', 'status.js', 'style.css', 'modules']);
214
+ git(['rm', '-r', '-q', '--cached', '--ignore-unmatch', 'cursors']);
216
215
  const status = git(['status', '--porcelain']);
217
216
  if (!status.trim()) {
218
217
  console.error('[status-mirror-sync] mirror already up to date — nothing to publish.');
@@ -228,4 +227,4 @@ if (require.main === module) {
228
227
  console.error(`[status-mirror-sync] published. The Pages host rebuilds ${b.domains.statusOrigin || 'the mirror'} in ~1 min.`);
229
228
  }
230
229
 
231
- module.exports = { buildMirrorIndex, injectOnce, brandTokens };
230
+ module.exports = { buildMirrorIndex, injectOnce, brandTokens, SHARED_ASSETS };
@@ -835,15 +835,16 @@ function mountInternalSurfaces(app) {
835
835
  const surface = surfaceOf(req);
836
836
  if (!surface) return next();
837
837
  // /api/* already handled above; everything else on a surface host serves from
838
- // the module's static dir. The index page comes from the stamped in-memory copy
839
- // (asset ?v= = content hash); everything else falls through to the on-disk
840
- // static handler.
841
- if ((req.method === 'GET' || req.method === 'HEAD') &&
842
- (req.path === '/' || req.path === '/index.html') &&
843
- surface.html['index.html']) {
838
+ // the module's static dir. Each page ("/" is the index, "/explore[.html]" its
839
+ // sibling; task 1005089) comes from the stamped + branded in-memory copy (asset
840
+ // ?v= = content hash); everything else falls through to the on-disk static handler.
841
+ // The map's keys are fixed at boot (the dir's own .html files), so a slug can only name a shipped page.
842
+ const pm = /^\/(?:([a-z0-9-]+)(?:\.html)?)?$/i.exec(req.path);
843
+ const page = pm && `${pm[1] || 'index'}.html`;
844
+ if ((req.method === 'GET' || req.method === 'HEAD') && page && surface.html[page]) {
844
845
  res.set('Cache-Control', 'public, max-age=60');
845
846
  res.type('html');
846
- return res.send(surface.html['index.html']);
847
+ return res.send(surface.html[page]);
847
848
  }
848
849
  return surface.static(req, res, next);
849
850
  });
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.49'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.51'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -170,11 +170,11 @@ test('apex: each page waits with the size that fits it', () => {
170
170
 
171
171
  test('status: its two waiting tablets wear the loader, and a failed or empty read stops it', () => {
172
172
  const html = read(STATUS, 'index.html');
173
- assert.ok(html.includes('<div id="versions" class="versions">\n <p class="empty loading-mark" role="status">The masons are at their tools…</p>'));
174
- assert.ok(html.includes('<div id="world-snapshot" class="world-snapshot">\n <p class="empty loading-mark" role="status">The cartographers are still at their survey…</p>'));
175
- // the header's "Tablet being consulted…" is rewritten in place before any read
173
+ assert.ok(html.includes('<div id="versions" class="versions">\n <p class="empty loading-mark" role="status">Loading the versions…</p>'));
174
+ assert.ok(html.includes('<div id="world-snapshot" class="world-snapshot">\n <p class="empty loading-mark" role="status">Loading the latest snapshot…</p>'));
175
+ // the header's "Checking for updates…" is rewritten in place before any read
176
176
  // starts (stampRefreshTime), so it is never a wait and keeps its plain words
177
- assert.match(html, /<span class="herm__updated" id="last-updated">Tablet being consulted…<\/span>/);
177
+ assert.match(html, /<span class="herm__updated" id="last-updated">Checking for updates…<\/span>/);
178
178
  // The Kill Switch is the last thing in the status sheet, so nothing can outrank it.
179
179
  assert.match(STATUS_CSS.trimEnd(), /@media \(prefers-reduced-motion: reduce\) \{\n {2}\*, \*::before, \*::after \{ animation: none !important; transition: none !important; \}\n\}$/);
180
180
 
@@ -186,12 +186,12 @@ test('status: its two waiting tablets wear the loader, and a failed or empty rea
186
186
  // ...and stopWaiting itself, run: it takes the class off and leaves the words alone.
187
187
  const src = js.match(/function stopWaiting\(el\) \{[\s\S]*?\n\}/)[0];
188
188
  const removed = [];
189
- const wait = { textContent: 'The masons are at their tools…', classList: { remove: (c) => removed.push(c) } };
189
+ const wait = { textContent: 'Loading the versions…', classList: { remove: (c) => removed.push(c) } };
190
190
  const host = { querySelectorAll: (sel) => (sel === '.loading-mark' ? [wait] : []) };
191
191
  const ctx = vm.createContext({});
192
192
  vm.runInContext(`${src}; this.stopWaiting = stopWaiting;`, ctx);
193
193
  ctx.stopWaiting(host);
194
194
  ctx.stopWaiting(null);
195
195
  assert.deepEqual(removed, ['loading-mark']);
196
- assert.equal(wait.textContent, 'The masons are at their tools…');
196
+ assert.equal(wait.textContent, 'Loading the versions…');
197
197
  });
@@ -26,7 +26,7 @@ test('a pack with no company drops the whole attribution sentence, not half of i
26
26
  const out = stamp({ name: '', site: 'https://ignored.example', tagline: 'ignored' });
27
27
  assert.doesNotMatch(out, /is the work of|ignored/);
28
28
  assert.doesNotMatch(out, /\{\{|brand-if/);
29
- assert.match(out, /Tokens are wrought/, 'the neighbouring footer lines survive');
29
+ assert.match(out, /Tokens are awarded by review/, 'the neighbouring footer lines survive');
30
30
  });
31
31
 
32
32
  test('a full company renders the linked name and its tagline', () => {
@@ -0,0 +1,195 @@
1
+ // tests/status_page_chrome.mjs — the status page on the chrome world (task 1005089,
2
+ // goal 1000138; DESIGN.md "Naming": the owner moved the status page onto this world,
3
+ // keeping its fixed status vocabulary as the hall does).
4
+ //
5
+ // What this pins, DB-free and browser-free:
6
+ // * nothing of the retired Greek ledger look survives in the surface: no parchment
7
+ // colours, no Cinzel / EB Garamond, no bronze cursors, no amphora or laurel
8
+ // sprite, no double-ruled frame;
9
+ // * every page title carries the product name from the branding pack;
10
+ // * the sheet declares no page :root, writes its dark tier twice byte-identical,
11
+ // and carries the hall's status vocabulary value for value;
12
+ // * explore is served through the branding injection, so it loads the pack's faces;
13
+ // * the off-box mirror IS the live page: the server's response for "/" on the
14
+ // status host, and the mirror's index built from the same source, are equal once
15
+ // each side's own injections are taken out — and every file the page links is one
16
+ // the mirror publishes.
17
+ //
18
+ // Run: node --test tests/status_page_chrome.mjs
19
+
20
+ import { test } from 'node:test';
21
+ import assert from 'node:assert/strict';
22
+ import fs from 'node:fs';
23
+ import http from 'node:http';
24
+ import path from 'node:path';
25
+ import { createRequire } from 'node:module';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const require = createRequire(import.meta.url);
29
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
30
+ const DIR = path.join(ROOT, 'modules', 'status-ui', 'public');
31
+ const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8').replace(/\r\n/g, '\n');
32
+ const decomment = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '');
33
+ const CSS = read('modules', 'status-ui', 'public', 'style.css');
34
+ const CODE = decomment(CSS);
35
+
36
+ function surfaceFiles(dir = DIR, out = []) {
37
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
38
+ const p = path.join(dir, e.name);
39
+ if (e.isDirectory()) surfaceFiles(p, out);
40
+ else if (/\.(html|css|js)$/.test(e.name)) out.push(p);
41
+ }
42
+ return out;
43
+ }
44
+
45
+ // The retired look, as literals. The parchment hexes are the old sheet's :root.
46
+ const RETIRED = [
47
+ /Cinzel/i, /Garamond/i, /cursors\//, /hand-pointer\.png/, /sigil-amphora/, /orn-(laurel|column|coin|scroll|map|diamond)/,
48
+ /#ece0c2|#f6ecd2|#ddc99c|#e3d0a4|#2a2110|#6e5b3a|#735f3d|#8b6f2c|#b89a55|#4a7d5c|#2f5a3f|#2d6e7e|#a85530/i,
49
+ /rgba\(\s*(139, 111, 44|60, 40, 10|42, 33, 16|243, 230, 198)/,
50
+ /upper-roman/,
51
+ ];
52
+
53
+ test('nothing of the Greek ledger look is left in the status surface', () => {
54
+ const hits = [];
55
+ for (const f of surfaceFiles()) {
56
+ const text = fs.readFileSync(f, 'utf8');
57
+ for (const re of RETIRED) if (re.test(text)) hits.push(`${path.relative(ROOT, f)}: ${re}`);
58
+ }
59
+ assert.deepEqual(hits, []);
60
+ assert.equal(fs.existsSync(path.join(DIR, 'cursors')), false, 'the bronze cursors are gone');
61
+ // no double-ruled frame: a panel is one hairline, no inset rule, no shadow
62
+ const panel = CODE.match(/\n\.tablet \{([^}]*)\}/);
63
+ assert.ok(panel, 'the panel rule exists');
64
+ assert.match(panel[1], /border: var\(--line\) solid var\(--rule-soft\);/);
65
+ assert.match(panel[1], /border-radius: var\(--ds-radius-panel\);/);
66
+ assert.doesNotMatch(panel[1], /box-shadow/);
67
+ assert.doesNotMatch(CODE, /\.tablet::(before|after)/, 'no carved corner ornaments');
68
+ });
69
+
70
+ test('every page title carries the product name from the pack', () => {
71
+ const pages = fs.readdirSync(DIR).filter((f) => f.endsWith('.html'));
72
+ assert.ok(pages.length >= 2);
73
+ for (const p of pages) {
74
+ const title = fs.readFileSync(path.join(DIR, p), 'utf8').match(/<title>([^<]*)<\/title>/);
75
+ assert.ok(title, `${p} has a title`);
76
+ assert.match(title[1], /\{\{productName\}\}/, `${p}: the title names the product`);
77
+ }
78
+ });
79
+
80
+ test('the page draws the shared mark and favicon, and names no face of its own', () => {
81
+ const html = read('modules', 'status-ui', 'public', 'index.html');
82
+ assert.ok(html.includes('<use href="/modules/ui-design/public/brand/mark.svg#mark"/>'), 'the top bar draws the one mark by reference');
83
+ assert.ok(html.includes('<link rel="icon" href="/modules/ui-design/public/brand/favicon.svg"'));
84
+ assert.doesNotMatch(html, /fonts\.googleapis\.com/, 'the faces come from the pack splice, not the page');
85
+ assert.ok(html.indexOf('/modules/hall-ui/public/controls.css') > html.indexOf('/modules/ui-design/public/tokens.css'), 'the shared controls follow the scales');
86
+ assert.ok(html.indexOf('/modules/hall-ui/public/controls.css') < html.indexOf('href="/style.css'), 'and precede the page sheet');
87
+ // exactly one header (the top bar), one main, one h1
88
+ assert.equal((html.match(/<header\b/g) || []).length, 1);
89
+ assert.equal((html.match(/<main\b/g) || []).length, 1);
90
+ assert.equal((html.match(/<h1\b/g) || []).length, 1);
91
+ });
92
+
93
+ test('the sheet declares no page :root, and its dark tier is written twice, byte-identical', () => {
94
+ assert.doesNotMatch(CODE, /(^|[\s,}]):root\b/, 'no :root — the pack splices the only one');
95
+ assert.match(CODE, /:where\(html\) \{/, 'the fallback contract tier sits at zero specificity');
96
+ const switchBody = CODE.match(/\nhtml\[data-theme="dark"\] \{([^}]*)\}/);
97
+ const mediaBody = CODE.match(/@media \(prefers-color-scheme: dark\) \{\s*html:not\(\[data-theme\]\) \{([^}]*)\}/);
98
+ assert.ok(switchBody && mediaBody, 'both dark twins exist');
99
+ const norm = (s) => s.split('\n').map((l) => l.trim()).filter(Boolean).join('\n');
100
+ assert.equal(norm(mediaBody[1]), norm(switchBody[1]));
101
+ // the dark tier re-pins no accent (the Unpinned Accent Rule)
102
+ assert.doesNotMatch(switchBody[1], /--(accent|accent-deep|sea|terracotta):/);
103
+ });
104
+
105
+ test('the status vocabulary is the hall\'s, value for value, in both modes', () => {
106
+ const hall = decomment(read('modules', 'hall-ui', 'public', 'style.css'));
107
+ const NAMES = ['--ok', '--ok-weak', '--warn', '--warn-weak', '--danger', '--danger-weak', '--info', '--info-weak'];
108
+ const valuesIn = (body) => Object.fromEntries(NAMES.map((n) => {
109
+ const m = body.match(new RegExp(`\\n\\s*${n}:\\s*([^;]+);`));
110
+ assert.ok(m, `${n} is declared`);
111
+ return [n, m[1].trim()];
112
+ }));
113
+ const lightOf = (src, open) => { const at = src.indexOf(open); assert.ok(at > -1, open); return src.slice(at, src.indexOf('}', at)); };
114
+ assert.deepEqual(valuesIn(lightOf(CODE, '\nhtml {')), valuesIn(lightOf(hall, '\n:root {')));
115
+ assert.deepEqual(valuesIn(lightOf(CODE, '\nhtml[data-theme="dark"] {')), valuesIn(lightOf(hall, '\nhtml[data-theme="dark"] {')));
116
+ });
117
+
118
+ // ── served ────────────────────────────────────────────────────────────────────
119
+ // node:http, not fetch: fetch silently drops a Host override (tests/module_asset_subdomain.mjs).
120
+ function get(port, p, host) {
121
+ return new Promise((resolve, reject) => {
122
+ const req = http.request({ host: '127.0.0.1', port, path: p, headers: { Host: host } }, (res) => {
123
+ let body = '';
124
+ res.setEncoding('utf8');
125
+ res.on('data', (c) => { body += c; });
126
+ res.on('end', () => resolve({ status: res.statusCode, body }));
127
+ });
128
+ req.on('error', reject);
129
+ req.end();
130
+ });
131
+ }
132
+ async function withServer(fn) {
133
+ const express = require('express');
134
+ const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
135
+ const app = express();
136
+ mountInternalSurfaces(app);
137
+ const server = await new Promise((resolve) => { const s = app.listen(0, () => resolve(s)); });
138
+ try { return await fn(server.address().port); } finally { await new Promise((r) => server.close(r)); }
139
+ }
140
+
141
+ test('explore is served through the branding injection: the pack\'s faces, the product name', async () => {
142
+ const { clientBranding } = require('../src/branding.js');
143
+ const brand = clientBranding();
144
+ await withServer(async (port) => {
145
+ for (const p of ['/explore', '/explore.html']) {
146
+ const r = await get(port, p, 'status.example.com');
147
+ assert.equal(r.status, 200, p);
148
+ // the href is attribute-escaped on the way in (themeMarkup), so & arrives as &amp;
149
+ for (const href of brand.theme.fonts.links) assert.ok(r.body.includes(`<link rel="stylesheet" href="${href.replace(/&/g, '&amp;')}">`), `${p} loads the pack's faces`);
150
+ assert.ok(r.body.includes(`<title>Explore — projects on the map · ${brand.identity.productName}</title>`), `${p} names the product`);
151
+ assert.doesNotMatch(r.body, /\{\{productName\}\}/);
152
+ }
153
+ // an asset still comes off disk
154
+ assert.equal((await get(port, '/explore.js', 'status.example.com')).status, 200);
155
+ });
156
+ });
157
+
158
+ test('the off-box mirror is the live page, and publishes every file the page links', async () => {
159
+ const mirror = require('../scripts/status-mirror-sync.js');
160
+ const { stampHtml } = require('../src/asset-version.js');
161
+ const { clientBranding, branding } = require('../src/branding.js');
162
+ const brand = clientBranding();
163
+ const src = stampHtml(read('modules', 'status-ui', 'public', 'index.html'), { assetDir: DIR, urlPrefix: '/' });
164
+ const apiBase = branding().domains.publicOrigin || 'https://origin.example.test';
165
+ const built = mirror.buildMirrorIndex({ srcHtml: src, apiBase, statusDataUrl: 'https://data.example.test/status.json', brand });
166
+
167
+ const live = await withServer(async (port) => (await get(port, '/', 'status.example.com')).body);
168
+ assert.ok(live.includes(brand.theme.fonts.links[0].replace(/&/g, '&amp;')), 'the live page splices the pack');
169
+
170
+ // each side minus its own injections: the server's client bundle + globals, and the
171
+ // mirror's three off-box additions + its globals
172
+ const liveCore = live
173
+ .replace(/<script src="\/lib\/bongos-client\.js[^"]*"><\/script>/, '')
174
+ .replace(/<script>window\.__BRANDING__=[\s\S]*?<\/script>/, '');
175
+ const mirrorCore = built
176
+ .replace(/ {2}<!-- #737: off-box mirror runs cross-origin against the droplet\. -->\n {2}<script>window\.STATUS_API_BASE = [^\n]*<\/script>\n {2}<script>window\.__BRANDING__ = [^\n]*<\/script>\n/, '')
177
+ .replace(/\n<!-- Off-box availability banner \(ADR 0029\)[\s\S]*?<div id="offbox-status" role="status" aria-live="polite"><\/div>\n/, '')
178
+ .replace(/<!-- Off-box availability banner driver \(ADR 0029\)[\s\S]*?<\/script>\n/, '');
179
+ // the origin token is the one value the two resolve from different places on a
180
+ // checkout with no domains; normalise it before comparing
181
+ const norm = (s) => s.split(apiBase).join('{{origin}}');
182
+ assert.equal(norm(mirrorCore), norm(liveCore));
183
+
184
+ // every root-absolute file the page links is published: its own files are copied
185
+ // from the surface dir, the shared ones at the same path
186
+ const refs = [...src.matchAll(/\b(?:href|src)="(\/[^"?#]+)/g)].map((m) => m[1]).filter((u) => !u.startsWith('//'));
187
+ assert.ok(refs.length >= 5);
188
+ for (const u of refs) {
189
+ const rel = u.slice(1).replace(/#.*$/, '');
190
+ const own = ['status.js', 'style.css'].includes(rel);
191
+ const shared = mirror.SHARED_ASSETS.some((a) => rel === a || rel.startsWith(`${a}/`));
192
+ assert.ok(own || shared, `the mirror publishes ${u}`);
193
+ assert.ok(fs.existsSync(path.join(own ? DIR : ROOT, rel)), `${u} exists to publish`);
194
+ }
195
+ });
@@ -0,0 +1,129 @@
1
+ // tests/status_page_wording.mjs — the status page speaks the project's current
2
+ // voice, not the old Greek-city ledger (task 1005090, goal 1000138; docs/voice.md).
3
+ //
4
+ // The page moved onto the chrome world in task 1005089 with its words untouched;
5
+ // this task rewrote them. What this pins: no word of the city vocabulary
6
+ // ("citizens who shape her stones", "tablet last consulted", "works wrought",
7
+ // "architects", drachmas…) is left anywhere a visitor can read it — the page's
8
+ // text, its title and meta, its accessible names, and every string status.js
9
+ // writes into the page. Class names, ids and code comments are not read, so a
10
+ // selector like `.foundry__chart` is not a finding; a sentence is.
11
+ //
12
+ // Run: node --test tests/status_page_wording.mjs
13
+
14
+ import { test } from 'node:test';
15
+ import assert from 'node:assert/strict';
16
+ import fs from 'node:fs';
17
+ import http from 'node:http';
18
+ import path from 'node:path';
19
+ import vm from 'node:vm';
20
+ import { createRequire } from 'node:module';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const require = createRequire(import.meta.url);
24
+
25
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+ const DIR = path.join(ROOT, 'modules', 'status-ui', 'public');
27
+ const read = (f) => fs.readFileSync(path.join(DIR, f), 'utf8');
28
+
29
+ // The retired vocabulary, as whole words.
30
+ const CITY = /\b(citizens?|stones?|tablets?|wrought|architects?|inscri(be|bed|ption|ptions)|lately|masons?|cartographers?|purse|treasury|foundry|forge|senate|lawmen|inspector|athens|civili[sz]ation|endeavou?rs?|portraits?|consult(ed|ing)?|archive|her stones|city|city's|ledger|hand at|hands)\b|\bdr\./i;
31
+
32
+ // What a visitor reads in the markup: text between tags, and the attributes that
33
+ // are read aloud or shown (title, alt, aria-label, placeholder, meta content).
34
+ function visibleHtml(html) {
35
+ const noComments = html.replace(/<!--[\s\S]*?-->/g, ' ').replace(/<(script|style)\b[\s\S]*?<\/\1>/gi, ' ');
36
+ const out = [];
37
+ for (const m of noComments.matchAll(/\b(?:aria-label|title|alt|placeholder|content)="([^"]*)"/g)) out.push(m[1]);
38
+ for (const m of noComments.replace(/<[^>]*>/g, '\n').split('\n')) if (m.trim()) out.push(m.trim());
39
+ return out;
40
+ }
41
+
42
+ // What status.js writes: its string literals, minus comments, minus any markup
43
+ // inside a template, minus the literals that are a selector, id or key.
44
+ function visibleJs(js) {
45
+ // line comments first: a `/*` inside one (a path like /public/*) would otherwise
46
+ // open a block comment that swallows the code after it
47
+ const code = js.replace(/(^|[^:\\'"`])\/\/.*$/gm, '$1').replace(/\/\*[\s\S]*?\*\//g, '');
48
+ const out = [];
49
+ for (const m of code.matchAll(/'((?:[^'\\\n]|\\.)*)'|"((?:[^"\\\n]|\\.)*)"|`((?:[^`\\]|\\.)*)`/g)) {
50
+ const lit = (m[1] ?? m[2] ?? m[3] ?? '').replace(/<[^>]*>/g, ' ').replace(/\$\{[^}]*\}/g, ' ').trim();
51
+ if (!lit) continue;
52
+ if (/^[a-z0-9_\-.#:[\]=" ]*$/.test(lit) && !/\s\S+\s/.test(lit)) continue; // a selector, id, class list or key
53
+ out.push(lit);
54
+ }
55
+ return out;
56
+ }
57
+
58
+ // The pages as the server serves them on the status host (stamped + branded), so
59
+ // the check reads what a visitor's browser receives, not the template.
60
+ function get(port, p) {
61
+ return new Promise((resolve, reject) => {
62
+ const req = http.request({ host: '127.0.0.1', port, path: p, headers: { Host: 'status.example.com' } }, (res) => {
63
+ let body = '';
64
+ res.setEncoding('utf8');
65
+ res.on('data', (c) => { body += c; });
66
+ res.on('end', () => resolve({ status: res.statusCode, body }));
67
+ });
68
+ req.on('error', reject);
69
+ req.end();
70
+ });
71
+ }
72
+ async function servedPages() {
73
+ const express = require('express');
74
+ const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
75
+ const app = express();
76
+ mountInternalSurfaces(app);
77
+ const server = await new Promise((resolve) => { const s = app.listen(0, () => resolve(s)); });
78
+ try {
79
+ const port = server.address().port;
80
+ const out = {};
81
+ for (const f of fs.readdirSync(DIR).filter((n) => n.endsWith('.html'))) {
82
+ const r = await get(port, f === 'index.html' ? '/' : `/${f}`);
83
+ assert.equal(r.status, 200, f);
84
+ out[f] = r.body;
85
+ }
86
+ return out;
87
+ } finally {
88
+ await new Promise((r) => server.close(r));
89
+ }
90
+ }
91
+
92
+ test('no Greek-city word is left in what the status page shows', async () => {
93
+ const hits = [];
94
+ const pages = await servedPages();
95
+ assert.ok(Object.keys(pages).length >= 2);
96
+ for (const [f, html] of Object.entries(pages)) {
97
+ for (const line of visibleHtml(html)) if (CITY.test(line)) hits.push(`${f}: ${line.slice(0, 120)}`);
98
+ }
99
+ for (const f of ['status.js', 'explore.js']) {
100
+ for (const line of visibleJs(read(f))) if (CITY.test(line)) hits.push(`${f}: ${line.slice(0, 120)}`);
101
+ }
102
+ assert.deepEqual(hits, []);
103
+ });
104
+
105
+ test('the reader really reads the page (a guard that sees nothing passes everything)', async () => {
106
+ const html = visibleHtml((await servedPages())['index.html']);
107
+ assert.ok(html.includes('Recently shipped'), 'a heading is read');
108
+ assert.ok(html.some((l) => /^How .+ is being built, in the open/.test(l)), 'the meta description is read');
109
+ assert.ok(html.includes('Filter shipped tasks by date range'), 'an accessible name is read');
110
+ const js = visibleJs(read('status.js'));
111
+ assert.ok(js.includes('Nothing shipped in this window.'), 'a string status.js writes is read');
112
+ assert.ok(!js.includes('foundry__chart'), 'a class name is not');
113
+ // and the old lines would have been caught
114
+ for (const old of ['Architects of {{worldName}}', 'tablet last consulted at', 'works wrought, last 7 days', 'Citizens who have set the most stone.', '12 dr.']) {
115
+ assert.ok(CITY.test(old), `the guard catches "${old}"`);
116
+ }
117
+ });
118
+
119
+ test('money reads as dollars, not drachmas', () => {
120
+ // run the page's own two formatters
121
+ const js = read('status.js');
122
+ const fn = (name) => js.match(new RegExp(`function ${name}\\(n\\) \\{[\\s\\S]*?\\n\\}`))[0];
123
+ const ctx = vm.createContext({});
124
+ vm.runInContext(`${fn('fmtUsd')}\n${fn('fmtMoney')}\nthis.fmtMoney = fmtMoney;`, ctx);
125
+ assert.equal(ctx.fmtMoney(12.5), '$12.50');
126
+ assert.equal(ctx.fmtMoney(48297.4), `$${(48297).toLocaleString()}`);
127
+ assert.equal(ctx.fmtMoney(null), '—');
128
+ assert.doesNotMatch(js, /fmtDr|dr\.`/);
129
+ });