@bongos/core 1.24.48 → 1.24.50

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.
Files changed (99) hide show
  1. package/.bongos-core.json +152 -112
  2. package/config/branding.neutral.json +2 -1
  3. package/docs/copy-inventory.md +1108 -1108
  4. package/docs/copy-registry.json +1557 -1557
  5. package/docs/module-api-changelog.md +4 -0
  6. package/docs/page-inventory.json +5 -4
  7. package/docs/page-readings.json +681 -681
  8. package/modules/hall-ui/public/ai-studio.html +2 -2
  9. package/modules/hall-ui/public/atlas.html +2 -2
  10. package/modules/hall-ui/public/blockers.html +2 -2
  11. package/modules/hall-ui/public/board-room.html +2 -2
  12. package/modules/hall-ui/public/city.html +1 -1
  13. package/modules/hall-ui/public/collab.html +2 -2
  14. package/modules/hall-ui/public/commands.html +2 -2
  15. package/modules/hall-ui/public/copy-desk.html +2 -2
  16. package/modules/hall-ui/public/deploy.html +2 -2
  17. package/modules/hall-ui/public/goal-map.html +2 -2
  18. package/modules/hall-ui/public/goals.html +2 -2
  19. package/modules/hall-ui/public/government.html +2 -2
  20. package/modules/hall-ui/public/hall-render.js +1 -1
  21. package/modules/hall-ui/public/idea.html +2 -2
  22. package/modules/hall-ui/public/ideas.html +2 -2
  23. package/modules/hall-ui/public/index.html +5 -5
  24. package/modules/hall-ui/public/market-watch-item.html +2 -2
  25. package/modules/hall-ui/public/market-watch-rules.html +2 -2
  26. package/modules/hall-ui/public/market-watch.html +2 -2
  27. package/modules/hall-ui/public/modules.html +2 -2
  28. package/modules/hall-ui/public/money.html +2 -2
  29. package/modules/hall-ui/public/my-agents.html +2 -2
  30. package/modules/hall-ui/public/primer.html +2 -2
  31. package/modules/hall-ui/public/profile.html +2 -2
  32. package/modules/hall-ui/public/project-settings.html +2 -2
  33. package/modules/hall-ui/public/roster.html +2 -2
  34. package/modules/hall-ui/public/settings.html +2 -2
  35. package/modules/hall-ui/public/shell.js +1 -1
  36. package/modules/hall-ui/public/street.html +1 -1
  37. package/modules/hall-ui/public/studio.html +2 -2
  38. package/modules/hall-ui/public/style.css +7 -9
  39. package/modules/hall-ui/public/task.html +2 -2
  40. package/modules/hall-ui/public/thinking.html +2 -2
  41. package/modules/hall-ui/public/tweak-editor.html +2 -2
  42. package/modules/hall-ui/public/watch.html +2 -2
  43. package/modules/hall-ui/public/welcome-boot.css +3 -4
  44. package/modules/hall-ui/public/welcome-boot.js +5 -3
  45. package/modules/hall-ui/public/work.html +2 -2
  46. package/modules/public-landing/public/account.html +1 -5
  47. package/modules/public-landing/public/assets/cosmos.css +7 -6
  48. package/modules/public-landing/public/community.html +1 -5
  49. package/modules/public-landing/public/connections.html +1 -5
  50. package/modules/public-landing/public/contact.html +1 -5
  51. package/modules/public-landing/public/guild.html +1 -5
  52. package/modules/public-landing/public/index.html +5 -8
  53. package/modules/public-landing/public/invitations.html +1 -5
  54. package/modules/public-landing/public/privacy.html +1 -5
  55. package/modules/public-landing/public/projects.html +1 -5
  56. package/modules/public-landing/public/terms.html +1 -5
  57. package/modules/raise/views/gate.html +4 -3
  58. package/modules/raise/views/nda.html +2 -1
  59. package/modules/raise/views/page.html +4 -3
  60. package/modules/status-ui/public/explore.html +14 -4
  61. package/modules/status-ui/public/index.html +26 -126
  62. package/modules/status-ui/public/index.states.json +3 -3
  63. package/modules/status-ui/public/status.js +53 -27
  64. package/modules/status-ui/public/style.css +640 -833
  65. package/modules/ui-design/public/brand/apple-touch-icon.png +0 -0
  66. package/modules/ui-design/public/brand/favicon.svg +1 -0
  67. package/modules/ui-design/public/brand/mark-body.svg +1 -0
  68. package/modules/ui-design/public/brand/mark-dark.svg +1 -0
  69. package/modules/ui-design/public/brand/mark-dot.svg +1 -0
  70. package/modules/ui-design/public/brand/mark-light.svg +1 -0
  71. package/modules/ui-design/public/brand/mark-mono.svg +1 -0
  72. package/modules/ui-design/public/brand/mark.svg +21 -0
  73. package/package-lock.json +2 -2
  74. package/package.json +1 -1
  75. package/release-notes.json +12 -0
  76. package/scripts/bongos/fitness.js +1 -2
  77. package/scripts/bongos/gen-brand.js +99 -0
  78. package/scripts/gds/gen-brand.js +7 -0
  79. package/scripts/status-mirror-sync.js +44 -45
  80. package/src/bongos/platform-visibility-gate.js +6 -3
  81. package/src/bongos/prelaunch-gate.js +6 -3
  82. package/src/bongos/serve-internal.js +9 -7
  83. package/src/brand-mark.js +96 -0
  84. package/src/branding.js +2 -0
  85. package/src/hall-look.js +7 -7
  86. package/src/module-api.js +1 -1
  87. package/tests/brand_mark.mjs +195 -0
  88. package/tests/cloud_bongos_pack.mjs +5 -2
  89. package/tests/hall_loading.mjs +8 -10
  90. package/tests/loading_mark.mjs +7 -13
  91. package/tests/nav_permission_atoms.mjs +2 -1
  92. package/tests/raise.mjs +3 -1
  93. package/tests/status_page_chrome.mjs +195 -0
  94. package/tests/welcome_boot.mjs +5 -2
  95. package/modules/raise/public/favicon.svg +0 -1
  96. package/modules/raise/public/logo-ink.svg +0 -1
  97. package/modules/raise/public/logo-night.svg +0 -1
  98. package/modules/status-ui/public/cursors/arrow.png +0 -0
  99. package/modules/status-ui/public/cursors/hand-pointer.png +0 -0
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><style>svg{--i:#17181a;--d:#fb9569}@media (prefers-color-scheme:dark){svg{--i:#e2e1e4}}</style><path d="M137 31 A78 78 0 1 0 170 66" pathLength="100" fill="none" stroke-width="24" stroke-linecap="round" style="stroke:var(--i)"/><circle cx="100" cy="100" r="50" style="fill:var(--i)"/><circle cx="156" cy="46" r="18" style="fill:var(--d)"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" aria-hidden="true"><path d="M137 31 A78 78 0 1 0 170 66" pathLength="100" fill="none" stroke-width="15" stroke-linecap="round" style="stroke:#000000"/><circle cx="100" cy="100" r="55" style="fill:#000000"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" role="img" aria-label="logo"><path d="M137 31 A78 78 0 1 0 170 66" pathLength="100" fill="none" stroke-width="15" stroke-linecap="round" style="stroke:#e2e1e4"/><circle cx="100" cy="100" r="55" style="fill:#e2e1e4"/><circle cx="156" cy="46" r="12" style="fill:#fb9569"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" aria-hidden="true"><circle cx="156" cy="46" r="12" style="fill:#000000"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" role="img" aria-label="logo"><path d="M137 31 A78 78 0 1 0 170 66" pathLength="100" fill="none" stroke-width="15" stroke-linecap="round" style="stroke:#17181a"/><circle cx="100" cy="100" r="55" style="fill:#17181a"/><circle cx="156" cy="46" r="12" style="fill:#fb9569"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" role="img" aria-label="logo"><path d="M137 31 A78 78 0 1 0 170 66" pathLength="100" fill="none" stroke-width="15" stroke-linecap="round" style="stroke:#000000"/><circle cx="100" cy="100" r="55" style="fill:#000000"/><circle cx="156" cy="46" r="12" style="fill:#000000"/></svg>
@@ -0,0 +1,21 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" role="img" aria-label="mark">
2
+ <!-- THE Cloud Bongos mark: the one source (task 1005088). An open ring, the
3
+ planet in it, and the moon in the gap. Every page draws it from here:
4
+ - inline, by reference: an svg with viewBox 0 0 200 200 holding
5
+ use href="/modules/ui-design/public/brand/mark.svg#mark"
6
+ (the parts are #mark-ring, #mark-disc and #mark-dot, for a page that animates them);
7
+ - coloured by two custom properties on the referencing element: mark-ink (the ring
8
+ and the planet; currentColor when unset) and mark-dot (the moon; the page's
9
+ accent when unset), each written with the two leading dashes in CSS;
10
+ - server-side (the gates, a look's favicon) through src/brand-mark.js;
11
+ - as files (light, dark, mono, the favicon set) written beside this one by
12
+ scripts/bongos/gen-brand.js from the pack's colours. Never edit those by hand.
13
+ Keep the ids and the two custom properties: src/brand-mark.js and
14
+ tests/brand_mark.mjs read them. An XML comment may not hold two dashes in a row,
15
+ or no browser will read this file. -->
16
+ <g id="mark">
17
+ <path id="mark-ring" d="M137 31 A78 78 0 1 0 170 66" pathLength="100" fill="none" stroke-width="15" stroke-linecap="round" style="stroke:var(--mark-ink,currentColor)"/>
18
+ <circle id="mark-disc" cx="100" cy="100" r="55" style="fill:var(--mark-ink,currentColor)"/>
19
+ <circle id="mark-dot" cx="156" cy="46" r="12" style="fill:var(--mark-dot,var(--accent,currentColor))"/>
20
+ </g>
21
+ </svg>
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.48",
3
+ "version": "1.24.50",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.48",
9
+ "version": "1.24.50",
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.48",
3
+ "version": "1.24.50",
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",
@@ -11259,5 +11259,17 @@
11259
11259
  "id": "1005084",
11260
11260
  "text": "The builders' hall now has one button and one text box style everywhere instead of eight button sizes and five box styles; main buttons are the full 46px pill with dark text on the orange, and the setup screen's save button is"
11261
11261
  }
11262
+ ],
11263
+ "1.24.49": [
11264
+ {
11265
+ "id": "1005088",
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
+ }
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
+ }
11262
11274
  ]
11263
11275
  }
@@ -574,8 +574,7 @@ const KERNEL_FILES = [
574
574
  // task 1004421 (ADR 0358): branding.js lays the chosen look over the pack through this
575
575
  // file — the same identity/config category, requiring only instance-config and node
576
576
  // built-ins, so it joins the roster rather than reading as a kernel→domain leak.
577
- 'src/hall-look.js',
578
- 'src/build-info.js',
577
+ 'src/hall-look.js', 'src/brand-mark.js', 'src/build-info.js', // brand-mark: the mark's one source (task 1005088), read by hall-look; same imports
579
578
  // task 1003476 (goal 1000072): the project's stale-claim timer — host configuration
580
579
  // a module reads rather than hardcodes, exactly the category instance-config.js and
581
580
  // branding.js are in. The doorway (a KERNEL_FILE) re-exports it, so leaving it off
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // gen-brand.js — the logo and favicon set, written from the one mark (task 1005088).
4
+ //
5
+ // The mark's one source is a shipped module's public/brand/mark.svg (src/brand-mark.js finds
6
+ // it). Pages that can colour it themselves reference it with <use>; everything that cannot —
7
+ // an <img>, a CSS mask, a browser tab, a home-screen icon — gets a fixed-colour file written
8
+ // here, beside the source, in the neutral pack's colours (config/branding.neutral.json):
9
+ //
10
+ // mark-light.svg the mark in Ink with the accent moon (light grounds)
11
+ // mark-dark.svg the mark in the Night ink with the accent moon (dark grounds)
12
+ // mark-mono.svg the whole mark in one ink (black), for a print or a stamp
13
+ // mark-body.svg the ring and planet only, one ink (the loaders' mask shapes:
14
+ // mark-dot.svg the moon only, one ink the ring breathes, the moon turns)
15
+ // favicon.svg the small cut, Ink by day and the Night ink by night
16
+ // apple-touch-icon.png 180x180, the light mark on the pack's ground (--png; needs a browser)
17
+ //
18
+ // Deterministic text, so `--check` is a clean gate (tests/brand_mark.mjs runs it). The PNG is
19
+ // rendered by the system browser and only written with --png.
20
+ //
21
+ // Usage:
22
+ // node scripts/bongos/gen-brand.js # write the SVG variants
23
+ // node scripts/bongos/gen-brand.js --png # … and render apple-touch-icon.png
24
+ // node scripts/bongos/gen-brand.js --check # write nothing; exit 2 if a variant is stale
25
+
26
+ const fs = require('node:fs');
27
+ const path = require('node:path');
28
+ const { NIGHT_INK, readMarkSource, markParts, markFile, faviconSvg } = require('../../src/brand-mark');
29
+
30
+ const ROOT = path.resolve(__dirname, '..', '..');
31
+
32
+ function brandDir() {
33
+ const modulesDir = path.join(ROOT, 'modules');
34
+ const hit = fs.readdirSync(modulesDir).sort()
35
+ .map((m) => path.join(modulesDir, m, 'public', 'brand'))
36
+ .find((d) => fs.existsSync(path.join(d, 'mark.svg')));
37
+ if (!hit) throw new Error('no module ships public/brand/mark.svg');
38
+ return hit;
39
+ }
40
+
41
+ function packColors() {
42
+ const pack = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8'));
43
+ return pack.theme.ui.colors;
44
+ }
45
+
46
+ // { filename: text } for every SVG variant. Pure given the source and the colours.
47
+ function brandFiles(src, colors) {
48
+ const parts = markParts(src);
49
+ if (!parts) throw new Error('mark.svg is missing #mark-ring, #mark-disc or #mark-dot');
50
+ const mono = '#000000';
51
+ return {
52
+ 'mark-light.svg': markFile(parts, { ink: colors.ink, dot: colors.accent, label: 'logo' }),
53
+ 'mark-dark.svg': markFile(parts, { ink: NIGHT_INK, dot: colors.accent, label: 'logo' }),
54
+ 'mark-mono.svg': markFile(parts, { ink: mono, dot: mono, label: 'logo' }),
55
+ 'mark-body.svg': markFile(parts, { ink: mono, dot: mono, only: ['ring', 'disc'] }),
56
+ 'mark-dot.svg': markFile(parts, { ink: mono, dot: mono, only: ['dot'] }),
57
+ 'favicon.svg': faviconSvg({ ink: colors.ink, accent: colors.accent }, src) + '\n',
58
+ };
59
+ }
60
+
61
+ async function renderTouchIcon(dir, colors) {
62
+ const { launchBrowser } = require('../../modules/ui-design/kit/lib'); // eslint-disable-line global-require
63
+ const svg = fs.readFileSync(path.join(dir, 'mark-light.svg'), 'utf8');
64
+ const browser = await launchBrowser();
65
+ try {
66
+ const page = await browser.newPage({ viewport: { width: 180, height: 180 }, deviceScaleFactor: 1 });
67
+ await page.setContent(`<html><body style="margin:0;background:${colors.bg}">` +
68
+ `<img src="data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}" style="display:block;width:128px;height:128px;margin:26px">` +
69
+ '</body></html>');
70
+ await page.waitForLoadState('load');
71
+ await page.screenshot({ path: path.join(dir, 'apple-touch-icon.png'), omitBackground: false });
72
+ } finally { await browser.close(); }
73
+ }
74
+
75
+ async function main() {
76
+ const args = process.argv.slice(2);
77
+ const dir = brandDir();
78
+ const colors = packColors();
79
+ const files = brandFiles(readMarkSource({ root: ROOT }), colors);
80
+ const stale = [];
81
+ for (const [name, text] of Object.entries(files)) {
82
+ const file = path.join(dir, name);
83
+ const cur = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
84
+ if (cur === text) continue;
85
+ stale.push(name);
86
+ if (!args.includes('--check')) fs.writeFileSync(file, text);
87
+ }
88
+ if (args.includes('--check')) {
89
+ if (stale.length) { process.stderr.write(`gen-brand: stale — ${stale.join(', ')}. Run: node scripts/bongos/gen-brand.js\n`); process.exit(2); }
90
+ process.stdout.write('gen-brand: the logo and favicon set match mark.svg\n');
91
+ return;
92
+ }
93
+ if (args.includes('--png')) await renderTouchIcon(dir, colors);
94
+ process.stdout.write(`gen-brand: ${stale.length ? `wrote ${stale.join(', ')}` : 'nothing to write'}${args.includes('--png') ? ' + apple-touch-icon.png' : ''}\n`);
95
+ }
96
+
97
+ if (require.main === module) main().catch((e) => { process.stderr.write(`${e.message}\n`); process.exit(1); });
98
+
99
+ module.exports = { brandFiles, packColors };
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/gen-brand.js — @bongos-forwarder (task 1004848). Generated by
3
+ // scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
4
+ // The real script is scripts/bongos/gen-brand.js. This forwarder keeps callers that still name
5
+ // the old path working, and warns once so they get repointed. Task 1003706 removes it.
6
+ 'use strict';
7
+ require('../bongos/forwarder.js').forward(module, 'gen-brand.js');
@@ -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 };
@@ -179,6 +179,9 @@ function relativeReturn(raw) {
179
179
 
180
180
  // The one server escaper (task 1004876); it also escapes ' , which this copy did not.
181
181
  const { escapeHtml: escAttr } = require('../html-escape');
182
+ // The mark, inlined from its one source (task 1005088) so the door still loads nothing;
183
+ // coloured by .mark's --mark-ink / --mark-dot below.
184
+ const { markSvg } = require('../brand-mark');
182
185
 
183
186
  // The door. Self-contained on purpose (the pre-launch gate's shape): it is
184
187
  // served to someone who may not see the project, so it pulls no stylesheet, no
@@ -198,8 +201,8 @@ body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;
198
201
  background:#edecee;color:#17181a}
199
202
  @media(prefers-color-scheme:dark){body{background:#000000;color:#ffffff}}
200
203
  .box{width:min(380px,100%);text-align:center}
201
- .mark{display:block;width:48px;height:48px;margin:0 auto 22px}
202
- @media(prefers-color-scheme:dark){.mark .mr{stroke:#e2e1e4}.mark .mp{fill:#e2e1e4}}
204
+ .mark{display:block;width:48px;height:48px;margin:0 auto 22px;--mark-ink:#17181a;--mark-dot:#fb9569}
205
+ @media(prefers-color-scheme:dark){.mark{--mark-ink:#e2e1e4}}
203
206
  h1{font-size:20px;font-weight:700;margin:0 0 6px}
204
207
  p{margin:0 0 20px;opacity:.72;font-size:14px}
205
208
  /* dark ink on the accent fill: the chrome world's salmon carries white at 2.19:1 */
@@ -207,7 +210,7 @@ a.btn{display:inline-block;padding:11px 22px;border-radius:999px;background:#fb9
207
210
  font:700 14px/1 inherit;text-decoration:none}
208
211
  a.btn:focus{outline:2px solid #a3481a;outline-offset:2px}
209
212
  </style></head><body><div class="box">
210
- <svg class="mark" viewBox="0 0 200 200" aria-hidden="true"><path class="mr" d="M137 31 A78 78 0 1 0 170 66" fill="none" stroke="#17181a" stroke-width="15" stroke-linecap="round"/><circle class="mp" cx="100" cy="100" r="55" fill="#17181a"/><circle cx="156" cy="46" r="12" fill="#fb9569"/></svg>
213
+ ${markSvg({ className: 'mark' })}
211
214
  <h1>${safe}</h1>
212
215
  <p>This project’s pages are open to its members. Sign in to continue.</p>
213
216
  <a class="btn" href="${escAttr(signIn)}">Sign in</a>
@@ -180,6 +180,9 @@ function relativeReturn(raw) {
180
180
 
181
181
  // The one server escaper (task 1004876); it also escapes ' , which this copy did not.
182
182
  const { escapeHtml: escAttr } = require('../html-escape');
183
+ // The mark, inlined from its one source (task 1005088) so the form still loads nothing;
184
+ // coloured by .mark's --mark-ink / --mark-dot below.
185
+ const { markSvg } = require('../brand-mark');
183
186
 
184
187
  // The form. Self-contained and token-free on purpose: it is served BEFORE the
185
188
  // visitor is allowed to see anything, so it must not pull the branding bundle,
@@ -198,8 +201,8 @@ body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;
198
201
  background:#edecee;color:#17181a}
199
202
  @media(prefers-color-scheme:dark){body{background:#000000;color:#ffffff}}
200
203
  .box{width:min(380px,100%);text-align:center}
201
- .mark{display:block;width:48px;height:48px;margin:0 auto 22px}
202
- @media(prefers-color-scheme:dark){.mark .mr{stroke:#e2e1e4}.mark .mp{fill:#e2e1e4}}
204
+ .mark{display:block;width:48px;height:48px;margin:0 auto 22px;--mark-ink:#17181a;--mark-dot:#fb9569}
205
+ @media(prefers-color-scheme:dark){.mark{--mark-ink:#e2e1e4}}
203
206
  h1{font-size:20px;font-weight:700;margin:0 0 6px}
204
207
  p{margin:0 0 20px;opacity:.72;font-size:14px}
205
208
  form{display:flex;gap:8px}
@@ -213,7 +216,7 @@ button{padding:11px 20px;border:0;border-radius:999px;background:#fb9569;color:#
213
216
  .err{margin:14px 0 0;color:#b3261e;font-size:13.5px}
214
217
  @media(prefers-color-scheme:dark){.err{color:#ffb4a8}}
215
218
  </style></head><body><div class="box">
216
- <svg class="mark" viewBox="0 0 200 200" aria-hidden="true"><path class="mr" d="M137 31 A78 78 0 1 0 170 66" fill="none" stroke="#17181a" stroke-width="15" stroke-linecap="round"/><circle class="mp" cx="100" cy="100" r="55" fill="#17181a"/><circle cx="156" cy="46" r="12" fill="#fb9569"/></svg>
219
+ ${markSvg({ className: 'mark' })}
217
220
  <h1>${name}</h1>
218
221
  <p>Not open to the world yet. Enter the password to have a look.</p>
219
222
  <form method="POST" action="${SUBMIT_PATH}">
@@ -133,6 +133,7 @@ function themeMarkup(theme) {
133
133
  // before anything reads the URL, so the neutral pack's inline `data:image/svg+xml,
134
134
  // <svg …>` favicon resolves to exactly the same bytes it does today.
135
135
  if (theme.favicon) out += `<link rel="icon" href="${esc(theme.favicon)}">`;
136
+ if (theme.appleTouchIcon) out += `<link rel="apple-touch-icon" href="${esc(theme.appleTouchIcon)}">`;
136
137
  return out;
137
138
  }
138
139
 
@@ -834,15 +835,16 @@ function mountInternalSurfaces(app) {
834
835
  const surface = surfaceOf(req);
835
836
  if (!surface) return next();
836
837
  // /api/* already handled above; everything else on a surface host serves from
837
- // the module's static dir. The index page comes from the stamped in-memory copy
838
- // (asset ?v= = content hash); everything else falls through to the on-disk
839
- // static handler.
840
- if ((req.method === 'GET' || req.method === 'HEAD') &&
841
- (req.path === '/' || req.path === '/index.html') &&
842
- 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]) {
843
845
  res.set('Cache-Control', 'public, max-age=60');
844
846
  res.type('html');
845
- return res.send(surface.html['index.html']);
847
+ return res.send(surface.html[page]);
846
848
  }
847
849
  return surface.static(req, res, next);
848
850
  });
@@ -0,0 +1,96 @@
1
+ // src/brand-mark.js — the Bongos mark for the server, read from its one source (task 1005088).
2
+ //
3
+ // The ring-and-dot mark lives in exactly one file, a shipped module's public/brand/mark.svg.
4
+ // A browser page draws it by reference (<use href="…/brand/mark.svg#mark">); the server needs
5
+ // the markup itself in two places that load nothing — the pre-launch and members-only gates,
6
+ // which are self-contained on purpose — and in a looked hall's favicon, drawn in the look's own
7
+ // colours. This file reads that source and hands those out, and scripts/bongos/gen-brand.js
8
+ // writes the static variants (light, dark, mono, the favicon) from the same functions.
9
+ //
10
+ // The source is found by its SHAPE — any shipped module's public/brand/mark.svg — never by a
11
+ // module key, which core may not name (ADR 0083 §Decision #4), as src/hall-look.js finds the
12
+ // style library. PURE given the source text; the default reader is the only I/O.
13
+ 'use strict';
14
+
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+
18
+ // The Night ink: the chrome world's ink on a dark ground, which every dark twin of the mark uses.
19
+ const NIGHT_INK = '#e2e1e4';
20
+ // The two custom properties the source colours itself by, exactly as written there.
21
+ const INK_VAR = 'var(--mark-ink,currentColor)';
22
+ const DOT_VAR = 'var(--mark-dot,var(--accent,currentColor))';
23
+ // The favicon cut: at 16px the display weights blur, so the tab icon draws a heavier ring, a
24
+ // slightly smaller planet and a larger moon. Same geometry, other weights.
25
+ const SMALL_CUT = { ring: { 'stroke-width': '24' }, disc: { r: '50' }, dot: { r: '18' } };
26
+
27
+ let _source = null;
28
+ function findSource(root) {
29
+ const base = root || require('./instance-config').resolveCoreRoot(); // eslint-disable-line global-require
30
+ const modulesDir = path.join(base, 'modules');
31
+ return fs.readdirSync(modulesDir).sort()
32
+ .map((m) => path.join(modulesDir, m, 'public', 'brand', 'mark.svg'))
33
+ .find((f) => fs.existsSync(f)) || null;
34
+ }
35
+ // The source text, read once. null when no shipped module carries it.
36
+ function readMarkSource({ root } = {}) {
37
+ if (_source !== null && !root) return _source;
38
+ let text = '';
39
+ try { const file = findSource(root); text = file ? fs.readFileSync(file, 'utf8') : ''; } catch { text = ''; }
40
+ if (!root) _source = text;
41
+ return text;
42
+ }
43
+
44
+ // The three parts as the source writes them, ids dropped (an inline copy must not collide
45
+ // with the source's ids on a page). null when the source is missing or misshapen.
46
+ function markParts(src = readMarkSource()) {
47
+ const pick = (id) => {
48
+ const m = String(src || '').match(new RegExp(`<(path|circle)\\s+id="mark-${id}"([^>]*?)\\s*/>`));
49
+ return m ? `<${m[1]}${m[2]}/>` : null;
50
+ };
51
+ const parts = { ring: pick('ring'), disc: pick('disc'), dot: pick('dot') };
52
+ return parts.ring && parts.disc && parts.dot ? parts : null;
53
+ }
54
+
55
+ function setAttrs(el, attrs) {
56
+ let out = el;
57
+ for (const [k, v] of Object.entries(attrs || {})) out = out.replace(new RegExp(`\\s${k}="[^"]*"`), ` ${k}="${v}"`);
58
+ return out;
59
+ }
60
+
61
+ // The parts' markup with the two colours set: each a CSS value (a hex, or a var()).
62
+ function partsMarkup(parts, { ink = INK_VAR, dot = DOT_VAR, cut = null, only = null } = {}) {
63
+ const names = only || ['ring', 'disc', 'dot'];
64
+ return names.map((n) => setAttrs(parts[n], cut && cut[n]).split(INK_VAR).join(ink).split(DOT_VAR).join(dot)).join('');
65
+ }
66
+
67
+ // The mark as inline markup for a server-rendered page. Coloured by --mark-ink / --mark-dot on
68
+ // the element (or its ancestors), exactly as a page's <use> reference is.
69
+ function markSvg({ className = 'mark', src } = {}) {
70
+ const parts = markParts(src === undefined ? readMarkSource() : src);
71
+ if (!parts) return '';
72
+ return `<svg class="${className}" viewBox="0 0 200 200" aria-hidden="true">${partsMarkup(parts)}</svg>`;
73
+ }
74
+
75
+ // A standalone SVG file of the mark in fixed colours (the light, dark and mono logos, and the
76
+ // two mask shapes the loaders cut from it).
77
+ function markFile(parts, { ink, dot, only, label = '' } = {}) {
78
+ const a11y = label ? ` role="img" aria-label="${label}"` : ' aria-hidden="true"';
79
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"${a11y}>${partsMarkup(parts, { ink, dot, only })}</svg>\n`;
80
+ }
81
+
82
+ // The favicon: the small cut, in the pack's ink by day and the Night ink by night.
83
+ function faviconSvg({ ink, accent, night = NIGHT_INK }, src = readMarkSource()) {
84
+ const parts = markParts(src);
85
+ if (!parts) return '';
86
+ return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">' +
87
+ `<style>svg{--i:${ink};--d:${accent}}@media (prefers-color-scheme:dark){svg{--i:${night}}}</style>` +
88
+ partsMarkup(parts, { ink: 'var(--i)', dot: 'var(--d)', cut: SMALL_CUT }) + '</svg>';
89
+ }
90
+
91
+ // An SVG as a data: URI for a <link rel="icon"> (single quotes, # escaped), or '' for ''.
92
+ function svgDataUri(svg) {
93
+ return svg ? 'data:image/svg+xml;utf8,' + svg.replace(/"/g, "'").replace(/#/g, '%23') : '';
94
+ }
95
+
96
+ module.exports = { NIGHT_INK, readMarkSource, markParts, markSvg, markFile, faviconSvg, svgDataUri };
package/src/branding.js CHANGED
@@ -268,6 +268,8 @@ function clientBranding(b = branding()) {
268
268
  links: Array.isArray(ui.fonts?.links) ? ui.fonts.links : [],
269
269
  },
270
270
  favicon: ui.favicon || '',
271
+ // the home-screen icon (a 180x180 PNG; task 1005088), '' when the pack names none
272
+ appleTouchIcon: ui.appleTouchIcon || '',
271
273
  backdrop: ui.backdrop || '',
272
274
  // The look (ADR 0358): which one this hall wears ('' = its own pack), whether
273
275
  // its night is tinted (only ever true with a look), and its logo address.
package/src/hall-look.js CHANGED
@@ -23,6 +23,7 @@
23
23
 
24
24
  const fs = require('node:fs');
25
25
  const path = require('node:path');
26
+ const { faviconSvg, svgDataUri } = require('./brand-mark');
26
27
 
27
28
  const HEX = /^#[0-9a-f]{6}$/i;
28
29
  const LOOK_NAME = /^[a-z][a-z-]{0,39}$/;
@@ -53,13 +54,10 @@ function readLibraryLook(name, { root } = {}) {
53
54
  }
54
55
 
55
56
  // The favicon a looked hall shows without a logo: the Bongos mark (task 1004846) in the
56
- // look's own ink with its accent as the moon, and the Night ink twin in dark mode.
57
+ // look's own ink with its accent as the moon, and the Night ink twin in dark mode. Drawn from
58
+ // the mark's one source (task 1005088); the colours are the look's validated hex.
57
59
  function lookFavicon(colors) {
58
- const c = (k) => encodeURIComponent(colors[k]);
59
- return "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'>" +
60
- `<style>.r{stroke:${c('ink')}}.p{fill:${c('ink')}}@media (prefers-color-scheme:dark){.r{stroke:%23e2e1e4}.p{fill:%23e2e1e4}}</style>` +
61
- "<path class='r' d='M137 31A78 78 0 1 0 170 66' fill='none' stroke-width='24' stroke-linecap='round'/>" +
62
- `<circle class='p' cx='100' cy='100' r='50'/><circle cx='156' cy='46' r='18' fill='${c('accent')}'/></svg>`;
60
+ return svgDataUri(faviconSvg({ ink: colors.ink, accent: colors.accent }));
63
61
  }
64
62
 
65
63
  // What the hall wears: { name, ui } or null for its own pack. PURE given readLook.
@@ -91,8 +89,10 @@ function applyLook(resolved, readLook = readLibraryLook) {
91
89
  ui.look = look.name;
92
90
  ui.nightTint = project.nightTint !== 'off';
93
91
  ui.favicon = lookFavicon(ui.colors);
92
+ // the home-screen icon is the pack's own colours; a hall in other colours goes without
93
+ ui.appleTouchIcon = '';
94
94
  }
95
- if (logo) { ui.logo = logo; ui.favicon = logo; }
95
+ if (logo) { ui.logo = logo; ui.favicon = logo; ui.appleTouchIcon = ''; }
96
96
  resolved.theme.ui = ui;
97
97
  return resolved;
98
98
  }
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.48'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.50'; // 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');