@bongos/core 1.24.40 → 1.24.41
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/.bongos-core.json +75 -90
- package/docs/canonical-permissions.md +2 -1
- package/docs/copy-inventory.md +116 -120
- package/docs/copy-registry.json +151 -207
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +11 -69
- package/docs/page-readings.json +382 -763
- package/modules/hall-ui/public/ai-studio.html +2 -2
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/city.html +2 -2
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +1 -1
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/gate.js +12 -5
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +2 -2
- package/modules/hall-ui/public/market-watch-item.html +2 -2
- package/modules/hall-ui/public/market-watch-rules.html +2 -2
- package/modules/hall-ui/public/market-watch.html +2 -2
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/money.html +2 -2
- package/modules/hall-ui/public/my-agents.html +2 -2
- package/modules/hall-ui/public/palette.js +10 -0
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.js +21 -10
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +1 -2
- package/modules/hall-ui/public/street.html +2 -2
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +120 -7
- package/modules/hall-ui/public/watch.js +11 -0
- package/modules/hall-ui/public/watch.states.json +49 -1
- package/modules/hall-ui/public/work.html +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/bongos/gen-hall-sitemap.js +2 -0
- package/src/bongos/serve-internal.js +15 -22
- package/src/module-api.js +1 -1
- package/tests/autonomy_builder_scope.mjs +2 -1
- package/tests/hall_audit.mjs +1 -1
- package/tests/hall_loading.mjs +13 -2
- package/tests/hall_nav.mjs +5 -2
- package/tests/hall_oversight_tabs.mjs +162 -0
- package/tests/hall_oversight_world.mjs +23 -9
- package/tests/hall_page_gate_map.mjs +7 -2
- package/tests/hall_roster_world.mjs +14 -9
- package/tests/hall_sessions_ledger.mjs +2 -1
- package/tests/nav_permission_atoms.mjs +3 -2
- package/tests/sessions_kit_pager.mjs +8 -4
- package/modules/hall-ui/public/gate.html +0 -76
- package/modules/hall-ui/public/gate.states.json +0 -60
- package/modules/hall-ui/public/sessions.html +0 -128
- package/modules/hall-ui/public/sessions.states.json +0 -59
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// tests/hall_oversight_tabs.mjs — one oversight page (task 1005049, owner decision
|
|
2
|
+
// 2026-11-03, idea 1001315 item 1).
|
|
3
|
+
//
|
|
4
|
+
// Watch, Sessions and Autobongos (/gate) were three oversight pages in the same
|
|
5
|
+
// sidebar group. Sessions and Autobongos are tabs on Watch now:
|
|
6
|
+
//
|
|
7
|
+
// * OLD URLS LAND ON THE RIGHT TAB. /sessions and /gate 302 to /watch#sessions
|
|
8
|
+
// and /watch#autobongos (serve-internal.js RETIRED_TO_SECTION), with any query
|
|
9
|
+
// string kept; the real middleware runs here, on a bare app.
|
|
10
|
+
// * EVERYTHING THE TWO PAGES SHOWED IS ON WATCH. The tabs exist, their panes hold
|
|
11
|
+
// the ledger, the transcript reader and the runner panel, and the scripts that
|
|
12
|
+
// drew the pages still draw them.
|
|
13
|
+
// * A TAB LOADS WHEN IT OPENS. Each session-ledger load is an audited read on the
|
|
14
|
+
// server, so sessions.js and gate.js ask for nothing until Watch first opens
|
|
15
|
+
// their tab, and only once.
|
|
16
|
+
// * THE PALETTE STILL FINDS THEM: its Watch jumps are real tab ids.
|
|
17
|
+
//
|
|
18
|
+
// Run: node tests/hall_oversight_tabs.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 vm from 'node:vm';
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
32
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
33
|
+
const WATCH_HTML = read('watch.html');
|
|
34
|
+
const WATCH_JS = read('watch.js');
|
|
35
|
+
|
|
36
|
+
async function withHall(fn) {
|
|
37
|
+
const express = require('express');
|
|
38
|
+
const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
|
|
39
|
+
const app = express();
|
|
40
|
+
mountInternalSurfaces(app);
|
|
41
|
+
const server = app.listen(0, '127.0.0.1');
|
|
42
|
+
await new Promise((r) => server.once('listening', r));
|
|
43
|
+
const { port } = server.address();
|
|
44
|
+
const get = (p, host) => new Promise((resolve, reject) => {
|
|
45
|
+
http.get({ host: '127.0.0.1', port, path: p, headers: { host } }, (res) => {
|
|
46
|
+
res.resume();
|
|
47
|
+
res.on('end', () => resolve({ status: res.statusCode, location: res.headers.location || '' }));
|
|
48
|
+
}).on('error', reject);
|
|
49
|
+
});
|
|
50
|
+
try { await fn(get); } finally { server.close(); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const TAB_OF = { sessions: 'sessions', gate: 'autobongos' };
|
|
54
|
+
|
|
55
|
+
test('RUN: /sessions and /gate each 302 to their tab on Watch', async () => {
|
|
56
|
+
await withHall(async (get) => {
|
|
57
|
+
for (const [page, tab] of Object.entries(TAB_OF)) {
|
|
58
|
+
for (const form of [`/${page}`, `/${page}/`, `/${page}.html`]) {
|
|
59
|
+
const r = await get(form, 'builders.127.0.0.1');
|
|
60
|
+
assert.equal(r.status, 302, `${form} is a temporary redirect`);
|
|
61
|
+
assert.equal(r.location, `/watch#${tab}`, `${form} lands on #${tab}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const q = await get('/sessions?from=mail', 'builders.127.0.0.1');
|
|
65
|
+
assert.equal(q.location, '/watch?from=mail#sessions', 'a query string rides along; the tab is the fragment');
|
|
66
|
+
// The map the two joined still carries the page it was built for (task 1005047).
|
|
67
|
+
assert.equal((await get('/fleet', 'builders.127.0.0.1')).location, '/money#hosting');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('Watch has a Sessions and an Autobongos tab, each with its pane', () => {
|
|
72
|
+
assert.match(WATCH_JS, /\{ id: 'sessions', label: 'Sessions', panel: 'pane-sessions' \}/);
|
|
73
|
+
assert.match(WATCH_JS, /\{ id: 'autobongos', label: 'Autobongos', panel: 'pane-autobongos' \}/);
|
|
74
|
+
for (const pane of ['pane-sessions', 'pane-autobongos']) {
|
|
75
|
+
assert.match(WATCH_HTML, new RegExp(`<div id="${pane}" role="tabpanel" aria-labelledby="tab-${pane.slice(5)}" hidden>`));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('everything the two pages showed is in those panes', () => {
|
|
80
|
+
const sessions = WATCH_HTML.slice(WATCH_HTML.indexOf('<div id="pane-sessions"'), WATCH_HTML.indexOf('<div id="pane-autobongos"'));
|
|
81
|
+
for (const id of ['sess-sealed', 'stats-scroll', 'sess-ledger-scroll', 'session-type-filter', 'sessions-table', 'sessions-pager-mount', 'transcript-scroll']) {
|
|
82
|
+
assert.match(sessions, new RegExp(`id="${id}"`), `the Sessions tab holds #${id}`);
|
|
83
|
+
}
|
|
84
|
+
const runner = WATCH_HTML.slice(WATCH_HTML.indexOf('<div id="pane-autobongos"'));
|
|
85
|
+
for (const id of ['gate-sealed', 'runner-body', 'gate-archon-only', 'fence-panel', 'fence-body', 'fence-builders']) {
|
|
86
|
+
assert.match(runner, new RegExp(`id="${id}"`), `the Autobongos tab holds #${id}`);
|
|
87
|
+
}
|
|
88
|
+
// Their scripts load before watch.js, which boots them.
|
|
89
|
+
const i = (s) => WATCH_HTML.indexOf(s);
|
|
90
|
+
assert.ok(i('/builders/sessions.js') > 0 && i('/builders/sessions.js') < i('/builders/watch.js'));
|
|
91
|
+
assert.ok(i('/builders/gate.js') > 0 && i('/builders/gate.js') < i('/builders/watch.js'));
|
|
92
|
+
// No element id on the page is used twice: three pages' markup now share one document.
|
|
93
|
+
const ids = [...WATCH_HTML.replace(/<!--[\s\S]*?-->/g, '').matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]);
|
|
94
|
+
assert.deepEqual(ids.filter((x, k) => ids.indexOf(x) !== k), [], 'every id on Watch is said once');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('the pages are gone, and nothing in the sidebar points at them', () => {
|
|
98
|
+
assert.ok(!fs.existsSync(path.join(HALL, 'sessions.html')));
|
|
99
|
+
assert.ok(!fs.existsSync(path.join(HALL, 'gate.html')));
|
|
100
|
+
assert.ok(!/href: '\/(sessions|gate)'/.test(read('shell.js')));
|
|
101
|
+
assert.ok(!/'gate', 'sessions'/.test(read('craft-mode.js')), 'Governing mode no longer lists them');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function bootTab(file, { sharedMe = false } = {}) {
|
|
105
|
+
const requests = [];
|
|
106
|
+
const el = () => ({ hidden: true, textContent: '', innerHTML: '', value: '', dataset: {}, style: {}, addEventListener() {}, querySelector: () => null, querySelectorAll: () => [], appendChild() {} });
|
|
107
|
+
const els = {};
|
|
108
|
+
const getEl = (id) => (els[id] = els[id] || el());
|
|
109
|
+
const document = { getElementById: getEl, querySelector: (s) => getEl(String(s).replace(/^#/, '')), querySelectorAll: () => [], createElement: el, addEventListener() {} };
|
|
110
|
+
const window = {
|
|
111
|
+
BongosClient: { createClient: () => ({ request: async (m, p) => { requests.push(p); return { status: 200, ok: true, data: { builder: { id: 1, rank: 'xenos' } } }; } }) },
|
|
112
|
+
OTB: { escapeHtml: (s) => String(s), $: document.querySelector, num: Number, fmtInt: String, fmtCompact: String, fmtDate: String, rankLabel: (r) => r, toast() {} },
|
|
113
|
+
OTBKit: { emptyStateHtml: (m) => m, makePager: () => ({ page: 1, perPage: 25 }) },
|
|
114
|
+
location: { assign() {} },
|
|
115
|
+
localStorage: { getItem: () => null, setItem() {} },
|
|
116
|
+
addEventListener() {},
|
|
117
|
+
};
|
|
118
|
+
// The shell's own /me read (shell.js probeMe shares it on window.__hallReads).
|
|
119
|
+
// A viewer below the floor, so each tab stops at its own sealed notice after /me.
|
|
120
|
+
if (sharedMe) window.__hallReads = { '/me': Promise.resolve({ builder: { id: 1, rank: 'xenos' } }) };
|
|
121
|
+
const sandbox = { window, document, console: { log() {}, warn() {}, error() {} }, setTimeout, clearTimeout, Promise, URLSearchParams };
|
|
122
|
+
vm.createContext(sandbox);
|
|
123
|
+
vm.runInContext(read(file), sandbox, { filename: file });
|
|
124
|
+
return { window, requests, getEl };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const [file, tab] of [['sessions.js', 'sessions'], ['gate.js', 'autobongos']]) {
|
|
128
|
+
test(`${file} asks for nothing until Watch opens its tab, then boots once`, async () => {
|
|
129
|
+
const t = bootTab(file);
|
|
130
|
+
await new Promise((r) => setImmediate(r));
|
|
131
|
+
assert.equal(t.requests.length, 0, `${file} read the server before its tab opened`);
|
|
132
|
+
assert.equal(typeof t.window.OTBWatchTabs[tab], 'function', `${file} hands Watch a boot for #${tab}`);
|
|
133
|
+
t.window.OTBWatchTabs[tab]();
|
|
134
|
+
await new Promise((r) => setImmediate(r));
|
|
135
|
+
const first = t.requests.length;
|
|
136
|
+
assert.ok(first >= 1, 'opening the tab loads it');
|
|
137
|
+
t.window.OTBWatchTabs[tab]();
|
|
138
|
+
await new Promise((r) => setImmediate(r));
|
|
139
|
+
assert.equal(t.requests.length, first, 'opening it again does not load it twice');
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const [file, tab] of [['sessions.js', 'sessions'], ['gate.js', 'autobongos']]) {
|
|
144
|
+
test(`${file} adopts the shell's /me read rather than asking again`, async () => {
|
|
145
|
+
const t = bootTab(file, { sharedMe: true });
|
|
146
|
+
t.window.OTBWatchTabs[tab]();
|
|
147
|
+
for (let i = 0; i < 5; i++) await new Promise((r) => setImmediate(r));
|
|
148
|
+
assert.ok(!t.requests.some((p) => /\/me$/.test(p)), `${file} asked /me although the shell had`);
|
|
149
|
+
assert.equal(t.getEl(file === 'gate.js' ? 'gate-sealed' : 'sess-sealed').hidden, false, 'and still acts on the answer');
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
test('watch.js boots a tab from its own change hook', () => {
|
|
154
|
+
assert.match(WATCH_JS, /onChange: \(id\) => \{\s*const boot = window\.OTBWatchTabs && window\.OTBWatchTabs\[id\];\s*if \(boot\) boot\(\);/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('the palette\'s Watch jumps are real watch.js tab ids', () => {
|
|
158
|
+
const ids = [...WATCH_JS.matchAll(/\{ id: '([a-z-]+)', label: '[^']*', panel:/g)].map((m) => m[1]);
|
|
159
|
+
const declared = [...read('palette.js').matchAll(/href:\s*'\/watch#([a-z-]+)'/g)].map((m) => m[1]);
|
|
160
|
+
assert.deepEqual(declared, ids, 'one jump per Watch tab, in order');
|
|
161
|
+
assert.ok(declared.includes('sessions') && declared.includes('autobongos'));
|
|
162
|
+
});
|
|
@@ -17,7 +17,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
17
17
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
18
|
const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
19
19
|
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
|
|
20
|
-
|
|
20
|
+
// task 1005049: the /gate page is Watch's Autobongos tab; its script and sheet stay.
|
|
21
|
+
const PAGES = ['government', 'watch'];
|
|
21
22
|
|
|
22
23
|
test('every oversight page links the family sheet, before its own', () => {
|
|
23
24
|
for (const p of PAGES) {
|
|
@@ -27,6 +28,11 @@ test('every oversight page links the family sheet, before its own', () => {
|
|
|
27
28
|
assert.ok(fam > 0, `${p}.html links oversight.css`);
|
|
28
29
|
assert.ok(own > fam, `${p}.html links its own sheet after the family's`);
|
|
29
30
|
}
|
|
31
|
+
// Watch's two folded-in tabs bring their sheets, after the family's too.
|
|
32
|
+
const watch = read('watch.html');
|
|
33
|
+
for (const sheet of ['sessions', 'gate']) {
|
|
34
|
+
assert.ok(watch.indexOf(`/builders/${sheet}.css`) > watch.indexOf('/builders/oversight.css'), `watch.html links ${sheet}.css after the family's`);
|
|
35
|
+
}
|
|
30
36
|
});
|
|
31
37
|
|
|
32
38
|
test('the family sheet spends only the fifteen: no literal colour', () => {
|
|
@@ -110,6 +116,10 @@ test('the sealed state is a panel with the floor written from the pack, on all f
|
|
|
110
116
|
assert.match(read(`${p}.html`), /class="scroll" id="[a-z-]*sealed(-scroll)?" /, `${p}.html seals with a .scroll panel`);
|
|
111
117
|
assert.match(read(`${p}.js`), /rankLabel\('metic'\)/, `${p}.js writes the floor from the pack's rank label`);
|
|
112
118
|
}
|
|
119
|
+
// The Autobongos and Sessions tabs keep their own sealed panels inside Watch.
|
|
120
|
+
assert.match(read('watch.html'), /class="scroll" id="gate-sealed" /);
|
|
121
|
+
assert.match(read('watch.html'), /class="scroll" id="sess-sealed" /);
|
|
122
|
+
assert.match(read('gate.js'), /rankLabel\('metic'\)/);
|
|
113
123
|
});
|
|
114
124
|
|
|
115
125
|
test('the vocabulary wall holds: the family is named oversight on the government surface', () => {
|
|
@@ -278,7 +288,9 @@ test('RUN: a metric its snapshots never carried is skipped, not drawn as a cliff
|
|
|
278
288
|
|
|
279
289
|
// ---- task 1004829: the government pages read like the rebuilt Home ----------
|
|
280
290
|
|
|
281
|
-
const GOV_PAGES = ['watch', '
|
|
291
|
+
const GOV_PAGES = ['watch', 'project-settings', 'money', 'deploy', 'government'];
|
|
292
|
+
// task 1005049: gate.js and sessions.js draw Watch's tabs now; their scripts still must not.
|
|
293
|
+
const GOV_SCRIPTS = [...GOV_PAGES, 'gate', 'sessions'];
|
|
282
294
|
|
|
283
295
|
test('every government page head holds only its h1, and no script writes into it', () => {
|
|
284
296
|
// No page shows its head (task 1004836). A sub-line there is a line nobody
|
|
@@ -291,7 +303,7 @@ test('every government page head holds only its h1, and no script writes into it
|
|
|
291
303
|
assert.doesNotMatch(head[1], /page-head__eyebrow|page-head__sub/, `${page}.html's hidden head holds only the h1`);
|
|
292
304
|
}
|
|
293
305
|
const subIds = ['watch-sub', 'gate-sub', 'ledger-sub', 'ps-sub', 'dp-sub', 'fl-sub', 'gov-sub'];
|
|
294
|
-
for (const page of
|
|
306
|
+
for (const page of GOV_SCRIPTS) {
|
|
295
307
|
const js = read(`${page}.js`);
|
|
296
308
|
for (const id of subIds) assert.ok(!js.includes(id), `${page}.js no longer writes into #${id}`);
|
|
297
309
|
}
|
|
@@ -300,7 +312,7 @@ test('every government page head holds only its h1, and no script writes into it
|
|
|
300
312
|
test('a failed load on Watch and Sessions is said in the page body', () => {
|
|
301
313
|
assert.match(read('watch.js'), /showSealed\('Could not check your access\. Reload to try again\.', 'Could not load'\)/);
|
|
302
314
|
assert.match(read('sessions.js'), /showSealed\('Could not check your access\. Reload to try again\.', 'Could not load'\)/);
|
|
303
|
-
assert.match(read('
|
|
315
|
+
assert.match(read('watch.html'), /id="ledger-status" role="status"/, 'the count and a failed page load sit on the ledger itself');
|
|
304
316
|
});
|
|
305
317
|
|
|
306
318
|
test('the page at /gate is the Autobongos panel: no held-PR list, and a Metic is told the controls are the Archon\'s', () => {
|
|
@@ -312,10 +324,12 @@ test('the page at /gate is the Autobongos panel: no held-PR list, and a Metic is
|
|
|
312
324
|
assert.ok(!/renderRow\(pr|OTBAsk/.test(js), 'no held-PR row or ask card is drawn');
|
|
313
325
|
assert.match(js, /if \(rank !== 'archon'\) \{ showArchonOnly\(\); return; \}/, 'a Metic gets the note, not an empty page');
|
|
314
326
|
assert.match(js, /The Autobongos controls are for the \$\{rankLabel\('archon'\)\}/, 'the rank is named from the pack');
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
assert.match(
|
|
327
|
+
// task 1005049: the panel is Watch's Autobongos tab, and /gate lands there.
|
|
328
|
+
const html = read('watch.html');
|
|
329
|
+
const pane = html.slice(html.indexOf('<div id="pane-autobongos"'));
|
|
330
|
+
assert.match(pane, /id="gate-archon-only"/);
|
|
319
331
|
assert.ok(!/gate-list|hall-ask\.js/.test(html), 'the list mount and the ask-card script are gone');
|
|
320
|
-
assert.match(read('
|
|
332
|
+
assert.match(read('watch.js'), /\{ id: 'autobongos', label: 'Autobongos', panel: 'pane-autobongos' \}/, 'the tab says what it is');
|
|
333
|
+
assert.ok(!/href: '\/gate'/.test(read('shell.js')), 'the sidebar no longer lists it');
|
|
334
|
+
assert.match(fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'utf8'), /gate: 'watch#autobongos'/, 'and the old URL lands on the tab');
|
|
321
335
|
});
|
|
@@ -120,8 +120,10 @@ const ORACLE = {
|
|
|
120
120
|
|
|
121
121
|
// Metic+ operator surfaces (the Governance nav group).
|
|
122
122
|
'/builders/watch': 'requireGovernmentPage',
|
|
123
|
-
|
|
124
|
-
'
|
|
123
|
+
// Gone (task 1005049, owner decision 2026-11-03): Sessions and Autobongos are tabs
|
|
124
|
+
// on Watch, behind Watch's gate, and both URLs 302 to their tab before any gate runs.
|
|
125
|
+
'/builders/gate': null,
|
|
126
|
+
'/builders/sessions': null,
|
|
125
127
|
// The five fundamentals of the project (task 1003280, goal 1000072). Same tier as
|
|
126
128
|
// its neighbours: a steering surface, metic+ per ADR 0157. Its two WRITES gate
|
|
127
129
|
// independently on `project.settings.manage`, so this row is only about the shell.
|
|
@@ -360,6 +362,9 @@ const GONE_PAGES = {
|
|
|
360
362
|
'/builders/ranks': 'task 1005046 — the rank ladder is a Primer chapter; /ranks redirects there',
|
|
361
363
|
'/builders/drachmae': 'task 1005046 — credits and karma is a Primer chapter; /drachmae redirects there',
|
|
362
364
|
'/builders/diagrams': 'task 1005046 — each diagram sits in its Primer chapter; /diagrams redirects there',
|
|
365
|
+
// Task 1005049 — one oversight page: each is a tab on Watch, and the old URL 302s to it.
|
|
366
|
+
'/builders/sessions': 'task 1005049 — the session log is Watch\'s Sessions tab; /sessions redirects there',
|
|
367
|
+
'/builders/gate': 'task 1005049 — the runner panel is Watch\'s Autobongos tab; /gate redirects there',
|
|
363
368
|
};
|
|
364
369
|
|
|
365
370
|
test('a row that is null because the page left the hall serves no page', () => {
|
|
@@ -230,12 +230,15 @@ test('sessions: the nowrap + wrapper pair is gone, and with it the sticky-header
|
|
|
230
230
|
const css = decomment(read('sessions.css'));
|
|
231
231
|
assert.doesNotMatch(css, /white-space:\s*nowrap/, 'no blanket nowrap on every cell');
|
|
232
232
|
assert.doesNotMatch(css, /ledger-table-wrap/, 'no wrapper — the shell\'s card strip handles a phone');
|
|
233
|
-
assert.doesNotMatch(
|
|
233
|
+
assert.doesNotMatch(sessionsPane(), /ledger-table-wrap/);
|
|
234
234
|
assert.doesNotMatch(css, /var\(--info\)|ledger-sid/, 'the session id is no longer painted in the info colour');
|
|
235
235
|
noAlias(css, 'sessions.css');
|
|
236
236
|
tokensDefined(css, 'sessions.css');
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
+
// task 1005049: the session ledger is Watch's Sessions tab; its markup is that pane.
|
|
240
|
+
const sessionsPane = () => { const w = read('watch.html'); return w.slice(w.indexOf('<div id="pane-sessions"'), w.indexOf('<div id="pane-autobongos"')); };
|
|
241
|
+
|
|
239
242
|
test('sessions: every id on a row is a door, the earned figure is shown, the totals ride the top strip', () => {
|
|
240
243
|
const src = read('sessions.js');
|
|
241
244
|
assert.match(src, /<a class="fact-chip" href="\/builders\/task\/\$\{encodeURIComponent\(String\(t\)\)\}">task \$\{escapeHtml\(String\(t\)\)\}<\/a>/,
|
|
@@ -244,8 +247,8 @@ test('sessions: every id on a row is a door, the earned figure is shown, the tot
|
|
|
244
247
|
assert.match(src, /<span class="ov-mono" title="\$\{escapeHtml\(s\.session_id\)\}">/, 'the session id is a mono fact with the full id on hover');
|
|
245
248
|
assert.match(src, /fmtInt\(s\.drachmae_earned\)/, 'what the session earned is on the row');
|
|
246
249
|
assert.doesNotMatch(src, /`#\$\{escapeHtml\(t\)\}`/, 'no bare hash-prefixed task text');
|
|
247
|
-
const html =
|
|
248
|
-
assert.match(html, /<section class="ov-summary" id="stats-scroll"/, 'the totals are the top-of-
|
|
250
|
+
const html = sessionsPane();
|
|
251
|
+
assert.match(html, /<section class="ov-summary" id="stats-scroll"/, 'the totals are the top-of-tab strip');
|
|
249
252
|
assert.match(html, /<div class="stat-row" id="stats-body"><\/div>/);
|
|
250
253
|
const ths = (html.match(/<th[ >]/g) || []).length;
|
|
251
254
|
// task 1002516: a tenth column, "Plain speech" — the transcript accessibility
|
|
@@ -259,8 +262,9 @@ test('sessions: every id on a row is a door, the earned figure is shown, the tot
|
|
|
259
262
|
// empty state short by a column, which no screenshot pass would catch.
|
|
260
263
|
assert.equal(ths, 11, 'eleven columns: session, builder, kind, tasks, earned, turns, tokens, tool errors, duration, plain speech, recorded');
|
|
261
264
|
assert.match(src, new RegExp(`colspan="${ths}"`), 'the empty row spans them all');
|
|
262
|
-
const
|
|
263
|
-
|
|
265
|
+
const page = read('watch.html');
|
|
266
|
+
const fam = page.indexOf('/builders/oversight.css');
|
|
267
|
+
assert.ok(fam > 0 && page.indexOf('/builders/sessions.css') > fam, 'watch.html links the family sheet before the sessions sheet');
|
|
264
268
|
});
|
|
265
269
|
|
|
266
270
|
test('sessions: the floor is written from the pack\'s rank label, never a hardcoded rank', () => {
|
|
@@ -268,9 +272,9 @@ test('sessions: the floor is written from the pack\'s rank label, never a hardco
|
|
|
268
272
|
const sealed = src.slice(src.indexOf('function showSealed'), src.indexOf('// ---- paging state'));
|
|
269
273
|
assert.match(sealed, /const floor = rankLabel\('metic'\)/);
|
|
270
274
|
// a failed load names its own heading; the rank floor is the default (task 1004829)
|
|
271
|
-
assert.match(sealed, /\$\('#
|
|
275
|
+
assert.match(sealed, /\$\('#sess-sealed-h'\)\.textContent = heading \|\| `\$\{floor\} and up`/);
|
|
272
276
|
assert.doesNotMatch(decomment(src), /Archons only|Metic rank up|Archon-only/);
|
|
273
|
-
assert.doesNotMatch(
|
|
277
|
+
assert.doesNotMatch(sessionsPane(), /Archon/);
|
|
274
278
|
});
|
|
275
279
|
|
|
276
280
|
// =========================================================================
|
|
@@ -299,8 +303,9 @@ test('the states files exercise the redesign\'s new states', () => {
|
|
|
299
303
|
const goals = JSON.parse(read('goals.states.json'));
|
|
300
304
|
assert.ok(goals.states.filters && goals.states.cards, 'goals: the Filters disclosure and the cards view are measured');
|
|
301
305
|
assert.deepEqual(goals.states.cards.expect.value, [['#f-goal-view', 'cards']]);
|
|
302
|
-
|
|
303
|
-
|
|
306
|
+
// task 1005049: the ledger is Watch's Sessions tab, measured as that page's state.
|
|
307
|
+
const watch = JSON.parse(read('watch.states.json'));
|
|
308
|
+
assert.deepEqual(watch.states.sessions.expect.visible, ['#pane-sessions', '#stats-scroll', '#sess-ledger-scroll']);
|
|
304
309
|
});
|
|
305
310
|
|
|
306
311
|
test('the roster draws the circuit divider in CSS, on the first row\'s cell and not the row', () => {
|
|
@@ -19,7 +19,8 @@ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
|
19
19
|
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
|
|
20
20
|
|
|
21
21
|
const JS = read('sessions.js');
|
|
22
|
-
|
|
22
|
+
// task 1005049: the ledger is Watch's Sessions tab; its markup lives in watch.html.
|
|
23
|
+
const HTML = (() => { const w = read('watch.html'); return w.slice(w.indexOf('<div id="pane-sessions"'), w.indexOf('<div id="pane-autobongos"')); })();
|
|
23
24
|
|
|
24
25
|
// Slice `const ACCESSIBILITY_TITLE = {...}` through the end of accessibilityCell
|
|
25
26
|
// and run it with the one helper it closes over.
|
|
@@ -95,8 +95,9 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
95
95
|
// task 1004846: the new logo (its bump landed late, in task 1004852); task 1004938: the
|
|
96
96
|
// Market Watch settings nav item and its fuel glyph; task 1005041: the Update pill replaced the
|
|
97
97
|
// core-update banner; task 1005045: the Market Watch settings item left (one sidebar item);
|
|
98
|
-
// task 1005046: Ranks, Economy and Diagrams left Docs (one reading room, the Primer)
|
|
99
|
-
|
|
98
|
+
// task 1005046: Ranks, Economy and Diagrams left Docs (one reading room, the Primer);
|
|
99
|
+
// task 1005049: Sessions and Autobongos left the sidebar (tabs on Watch).
|
|
100
|
+
const SHELL_SHA = 'bb0abbadab2508ca';
|
|
100
101
|
|
|
101
102
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
102
103
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|
|
@@ -33,7 +33,8 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
33
33
|
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
34
34
|
const KIT_JS = read('modules/hall-ui/public/hall-kit.js');
|
|
35
35
|
const SESSIONS_JS = read('modules/hall-ui/public/sessions.js');
|
|
36
|
-
|
|
36
|
+
// task 1005049: the ledger is Watch's Sessions tab, so its markup lives in watch.html.
|
|
37
|
+
const SESSIONS_HTML = read('modules/hall-ui/public/watch.html');
|
|
37
38
|
|
|
38
39
|
const { test, summary } = makeRunner();
|
|
39
40
|
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
@@ -147,6 +148,9 @@ function harness({ total = 320, failAfter = null, viewerRank = 'metic', perPageP
|
|
|
147
148
|
vm.runInContext(KIT_JS, sandbox, { filename: 'hall-kit.js' });
|
|
148
149
|
if (dropKit) delete sandbox.window.OTBKit;
|
|
149
150
|
vm.runInContext(SESSIONS_JS, sandbox, { filename: 'sessions.js' });
|
|
151
|
+
// The tab boots when Watch first opens it (task 1005049); a second open is a no-op.
|
|
152
|
+
sandbox.window.OTBWatchTabs.sessions();
|
|
153
|
+
sandbox.window.OTBWatchTabs.sessions();
|
|
150
154
|
|
|
151
155
|
const searches = () => requests.filter((u) => u.includes('/sessions/search'));
|
|
152
156
|
const q = (n) => {
|
|
@@ -161,7 +165,7 @@ function harness({ total = 320, failAfter = null, viewerRank = 'metic', perPageP
|
|
|
161
165
|
// 1. the page markup no longer carries a pager of its own
|
|
162
166
|
// ---------------------------------------------------------------------------
|
|
163
167
|
|
|
164
|
-
await test('
|
|
168
|
+
await test('the Sessions tab carries a mount, not a hand-rolled pager', () => {
|
|
165
169
|
assert.ok(!/id="ledger-pager"/.test(SESSIONS_HTML), 'the bespoke <nav class="pager"> is gone');
|
|
166
170
|
assert.ok(!/id="pager-(prev|next|pos)"/.test(SESSIONS_HTML), 'its bespoke button ids are gone');
|
|
167
171
|
assert.match(SESSIONS_HTML, /id="sessions-pager-mount"/);
|
|
@@ -307,14 +311,14 @@ await test('a missing kit degrades to a named failure, not a blank page', async
|
|
|
307
311
|
await tick(); await tick();
|
|
308
312
|
assert.equal(h.searches().length, 0, 'no corpus request without a pager to place it');
|
|
309
313
|
// in the page body: the head that used to carry it is hidden (task 1004829)
|
|
310
|
-
assert.match(h.getEl('
|
|
314
|
+
assert.match(h.getEl('sess-sealed-body').textContent, /could not be loaded/);
|
|
311
315
|
});
|
|
312
316
|
|
|
313
317
|
await test('the rank gate still runs before any corpus request', async () => {
|
|
314
318
|
const h = harness({ total: 320, viewerRank: 'xenos' });
|
|
315
319
|
await tick(); await tick();
|
|
316
320
|
assert.equal(h.searches().length, 0, 'a xenos must not even ask');
|
|
317
|
-
assert.equal(h.getEl('
|
|
321
|
+
assert.equal(h.getEl('sess-sealed').hidden, false, 'they get the sealed notice');
|
|
318
322
|
});
|
|
319
323
|
|
|
320
324
|
summary();
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Autobongos — {{worldName}}</title>
|
|
7
|
-
<meta name="description" content="The Autobongos runner: switch unattended building on or off, choose the goals it may work, and pause any builder's runner.">
|
|
8
|
-
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
-
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
-
<link rel="stylesheet" href="/builders/oversight.css">
|
|
13
|
-
<link rel="stylesheet" href="/builders/gate.css">
|
|
14
|
-
</head>
|
|
15
|
-
<body data-page="gate" data-page-title="Autobongos">
|
|
16
|
-
|
|
17
|
-
<div class="app">
|
|
18
|
-
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
19
|
-
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
20
|
-
<div class="app-body">
|
|
21
|
-
<header class="app-topbar" id="app-topbar"></header>
|
|
22
|
-
<div class="app-content">
|
|
23
|
-
<!-- The page's one h1, for screen readers: no page shows its head (task
|
|
24
|
-
1004836), so its sub-line went in task 1004829. -->
|
|
25
|
-
<div class="page-head page-head--board">
|
|
26
|
-
<h1 class="page-head__h">Autobongos</h1>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<main id="hall-main">
|
|
30
|
-
<!-- task 1005043: the page loader, shown until any section below is revealed. -->
|
|
31
|
-
<p class="hall-loading hall-loading--page hall-loading--wait" role="status">Loading…</p>
|
|
32
|
-
<!-- task 1003469: the sealed state is a panel with a heading, not a bare
|
|
33
|
-
line on the ground. gate.js writes the real floor into the lede from
|
|
34
|
-
the pack's rank label, so the copy cannot drift from ADR 0157. -->
|
|
35
|
-
<section class="scroll" id="gate-sealed" aria-labelledby="gate-sealed-h" hidden>
|
|
36
|
-
<h2 class="scroll__h" id="gate-sealed-h">Restricted</h2>
|
|
37
|
-
<p class="scroll__lede" id="gate-sealed-body">This page opens at a higher rank.</p>
|
|
38
|
-
</section>
|
|
39
|
-
|
|
40
|
-
<section id="gate-body" hidden>
|
|
41
|
-
<!-- The Autobongos fence (task 1003908, ADR 0307 §6): what is allowed to
|
|
42
|
-
reach main unattended. This page held the held-PR list too until task
|
|
43
|
-
1004859 removed it; the URL stays /gate so bookmarks keep working.
|
|
44
|
-
Archon-only — a Metic gets the note below instead, and the routes
|
|
45
|
-
behind the panel are system:true/archon server-side regardless. -->
|
|
46
|
-
<p class="scroll__lede" id="gate-archon-only" hidden></p>
|
|
47
|
-
<section class="scroll" id="fence-panel" aria-labelledby="fence-h" hidden>
|
|
48
|
-
<h2 class="scroll__h" id="fence-h">Autobongos runner</h2>
|
|
49
|
-
<!-- One panel (task 1004509): a checklist while anything the runner
|
|
50
|
-
needs is missing, one status line with the switch once it is all
|
|
51
|
-
ticked. Drawn from /autonomy/runners + /autonomy/fence together. -->
|
|
52
|
-
<div id="fence-body"><span class="kit-skel"></span></div>
|
|
53
|
-
<!-- Every builder's runner (task 1004501): what each one picked to
|
|
54
|
-
work, whether you paused it, and whether it is checking in. -->
|
|
55
|
-
<div id="fence-builders"></div>
|
|
56
|
-
</section>
|
|
57
|
-
</section>
|
|
58
|
-
</main>
|
|
59
|
-
|
|
60
|
-
<footer class="app-foot" id="app-foot"></footer>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
66
|
-
|
|
67
|
-
<script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
|
|
68
|
-
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
69
|
-
<script src="/builders/craft-mode.js?v=2026-11-05-sections"></script>
|
|
70
|
-
<script src="/builders/shell.js?v=2026-11-05-sections"></script>
|
|
71
|
-
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
72
|
-
<script src="/builders/palette.js" defer></script>
|
|
73
|
-
<script src="/builders/hall-kit.js"></script>
|
|
74
|
-
<script src="/builders/gate.js?v=2026-10-25-autobongos"></script>
|
|
75
|
-
</body>
|
|
76
|
-
</html>
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_": "Autobongos — the owner's runner panel behind requireGovernmentPage, still served at /gate (task 1004859 removed the held-PR list). Measured at three widths in both modes rather than looked at once.",
|
|
3
|
-
"page": "/builders/gate",
|
|
4
|
-
"surface": "hall-ui",
|
|
5
|
-
"stub": {
|
|
6
|
-
"prefix": "/builders/"
|
|
7
|
-
},
|
|
8
|
-
"modeQuery": false,
|
|
9
|
-
"_feeds": "The kit's own stub carries no feed for this page, so against it the page renders its EMPTY and SKELETON states — which is the point: those are states the floors must hold in. Against the hall-preview harness the same states render populated. Every declared 404 below is a fixture the harness does not carry, named rather than hidden.",
|
|
10
|
-
"ignoreRequests": [
|
|
11
|
-
"/api/(gds|bongos)/me$",
|
|
12
|
-
"/api/(gds|bongos)/me/",
|
|
13
|
-
"/api/(gds|bongos)/goals",
|
|
14
|
-
"/api/(gds|bongos)/inbox/",
|
|
15
|
-
"/api/(gds|bongos)/versions",
|
|
16
|
-
"/api/(gds|bongos)/builders",
|
|
17
|
-
"/api/(gds|bongos)/access-requests",
|
|
18
|
-
"/api/(gds|bongos)/autonomy/"
|
|
19
|
-
],
|
|
20
|
-
"ignoreConsole": [
|
|
21
|
-
"/api/(gds|bongos)/",
|
|
22
|
-
"\\b401\\b",
|
|
23
|
-
"\\b403\\b",
|
|
24
|
-
"\\b404\\b",
|
|
25
|
-
"\\b501\\b",
|
|
26
|
-
"Unauthorized",
|
|
27
|
-
"Forbidden",
|
|
28
|
-
"Not Found",
|
|
29
|
-
"Not Implemented",
|
|
30
|
-
"load failed"
|
|
31
|
-
],
|
|
32
|
-
"states": {
|
|
33
|
-
"queue": {
|
|
34
|
-
"auth": true,
|
|
35
|
-
"actions": [
|
|
36
|
-
[
|
|
37
|
-
"wait",
|
|
38
|
-
700
|
|
39
|
-
]
|
|
40
|
-
],
|
|
41
|
-
"expect": {
|
|
42
|
-
"visible": [
|
|
43
|
-
"#gate-body"
|
|
44
|
-
]
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
"out": {
|
|
48
|
-
"auth": false
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
"reducedMotion": {
|
|
52
|
-
"auth": true,
|
|
53
|
-
"actions": [
|
|
54
|
-
[
|
|
55
|
-
"wait",
|
|
56
|
-
600
|
|
57
|
-
]
|
|
58
|
-
]
|
|
59
|
-
}
|
|
60
|
-
}
|