@bongos/core 1.24.39 → 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 +87 -102
- package/docs/canonical-permissions.md +2 -1
- package/docs/copy-inventory.md +122 -127
- package/docs/copy-registry.json +179 -244
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +11 -69
- package/docs/page-readings.json +434 -822
- 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.css +10 -0
- package/modules/hall-ui/public/roster.html +6 -3
- package/modules/hall-ui/public/roster.js +168 -9
- 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.css +0 -6
- package/modules/hall-ui/public/watch.html +121 -15
- package/modules/hall-ui/public/watch.js +28 -166
- package/modules/hall-ui/public/watch.states.json +49 -1
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/builders-roster.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/gen-hall-sitemap.js +2 -0
- package/src/bongos/routes/me.js +3 -1
- 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/rank_tier_single_source.mjs +3 -1
- package/tests/roster_government_columns.mjs +369 -0
- package/tests/sessions_kit_pager.mjs +8 -4
- package/tests/watch_sealed_floor.mjs +6 -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
- package/tests/watch_roster_self_row.mjs +0 -296
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Metic+ monitoring view (Archon-only until ADR 0157 delegated the operational
|
|
4
4
|
// permissions to the working rank). One self-contained page that stitches together
|
|
5
5
|
// the oversight surfaces: pending access requests, security (red-team reports +
|
|
6
|
-
// override queue + audit log), the
|
|
6
|
+
// override queue + audit log), the stale-claim tally (the roster itself is the
|
|
7
|
+
// Builders page since task 1005048), per-builder + project-wide
|
|
7
8
|
// review grades, estimation drift, repo health, and a tally of recorded sessions.
|
|
8
9
|
//
|
|
9
10
|
// Note the page floor and the panel floors are no longer identical: the page and
|
|
@@ -20,6 +21,9 @@
|
|
|
20
21
|
// loads independently in its own try/catch so one failing endpoint cannot
|
|
21
22
|
// break the others.
|
|
22
23
|
//
|
|
24
|
+
// TASK 1005049 folded the Sessions page (sessions.js) and the Autobongos runner
|
|
25
|
+
// panel (gate.js) in as two more tabs; /sessions and /gate redirect to them.
|
|
26
|
+
//
|
|
23
27
|
// TASK 1003469 put the page in the chrome world's composition: every list is
|
|
24
28
|
// the kit's .ledger through makeLedger (eight of nine were a page-local table
|
|
25
29
|
// that duplicated it with a hand-rolled th voice), every figure row is the oversight
|
|
@@ -165,7 +169,7 @@
|
|
|
165
169
|
// NOTE: loadMarks is the deliberate outlier — its failure state is a plain
|
|
166
170
|
// "empty" message, not "unavailable", so it keeps its own try/catch.
|
|
167
171
|
// sealedText — name the floor the DENIAL reported, never one written here.
|
|
168
|
-
// Each `sealedMsg` below is the SUBJECT clause only ("The
|
|
172
|
+
// Each `sealedMsg` below is the SUBJECT clause only ("The audit log is not visible
|
|
169
173
|
// at your rank."); the rank sentence in front of it is composed from the 403.
|
|
170
174
|
// That is what keeps this page from telling a builder a permission model the
|
|
171
175
|
// server stopped implementing, which is the whole of task 1003449 — and why the
|
|
@@ -865,164 +869,19 @@
|
|
|
865
869
|
}
|
|
866
870
|
|
|
867
871
|
// =========================================================================
|
|
868
|
-
// 3.
|
|
872
|
+
// 3. STALE CLAIMS — a tally only (task 1005048)
|
|
869
873
|
// =========================================================================
|
|
870
874
|
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
const isViewer = (b) => viewerId != null && b && b.id != null && String(b.id) === String(viewerId);
|
|
882
|
-
|
|
883
|
-
function renderRoster(data, target) {
|
|
884
|
-
const builders = Array.isArray(data.builders) ? data.builders : [];
|
|
885
|
-
setAttention('att-stale', builders.filter((b) => b.stale_claim).length);
|
|
886
|
-
target.innerHTML = '';
|
|
887
|
-
if (!builders.length) {
|
|
888
|
-
target.innerHTML = emptyStateHtml('No builders on the roster.');
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
ledger(target, {
|
|
892
|
-
columns: [
|
|
893
|
-
'Builder', 'Rank',
|
|
894
|
-
{ label: (window.OTBBranding?.currencyLabel?.({ caps: true }) ?? 'Credits'), class: 'num' },
|
|
895
|
-
{ label: 'Karma', class: 'num' }, { label: 'Active claims', class: 'num' },
|
|
896
|
-
'Standing', 'Action',
|
|
897
|
-
],
|
|
898
|
-
pager: makePager('watch-roster', 'otb.watch.rosterPerPage'),
|
|
899
|
-
rowHtml: (b) => {
|
|
900
|
-
// THE VIEWER'S OWN ROW OFFERS NOTHING (task 1003720, re-filed from 1002951
|
|
901
|
-
// — "I cannot press offboard", reported by the owner from Discord).
|
|
902
|
-
//
|
|
903
|
-
// The roster returns every builder, so the reader's own row is in it. Both
|
|
904
|
-
// routes behind that row refuse a self-change OUTRIGHT and always will:
|
|
905
|
-
// PATCH /builders/:id/rank answers cannot_change_own_rank and
|
|
906
|
-
// /builders/:id/status answers cannot_change_own_status — the guard that
|
|
907
|
-
// stops the last Archon demoting or offboarding themselves and locking
|
|
908
|
-
// everyone out. The row painted a live rank <select> and a live Offboard
|
|
909
|
-
// pill anyway, so from the owner's seat the page carried a control that
|
|
910
|
-
// could only ever answer with a raw refusal slug in a toast. That is what
|
|
911
|
-
// "I cannot press offboard" describes.
|
|
912
|
-
//
|
|
913
|
-
// This is not a SOFT gate (the primer's acknowledge control, which opens
|
|
914
|
-
// once you have read enough and so stays in the tab order as
|
|
915
|
-
// aria-disabled) — it is an action that can never open for this reader.
|
|
916
|
-
// So the row renders no control at all and the section's foot line says
|
|
917
|
-
// why, rather than painting a dead one.
|
|
918
|
-
const self = isViewer(b);
|
|
919
|
-
const opts = RANK_OPTIONS.slice();
|
|
920
|
-
if (b.rank && !opts.includes(b.rank)) opts.unshift(b.rank);
|
|
921
|
-
const rankCell = self
|
|
922
|
-
? `<span class="watch-self">${escapeHtml(rankLabel(b.rank))}</span>`
|
|
923
|
-
: `<select data-roster-rank data-id="${escapeHtml(b.id)}" data-current="${escapeHtml(b.rank || '')}" aria-label="Rank">` +
|
|
924
|
-
opts.map((rk) => `<option value="${escapeHtml(rk)}"${rk === b.rank ? ' selected' : ''}>${escapeHtml(rankLabel(rk))}</option>`).join('') +
|
|
925
|
-
'</select>';
|
|
926
|
-
const isInactive = String(b.status) === 'inactive';
|
|
927
|
-
const statusBtn = isInactive
|
|
928
|
-
? ghost(`data-roster-status="active" data-id="${escapeHtml(b.id)}"`, 'Reactivate')
|
|
929
|
-
: ghost(`data-roster-status="inactive" data-id="${escapeHtml(b.id)}"`, 'Offboard', true);
|
|
930
|
-
const actionCell = self
|
|
931
|
-
? '<span class="watch-self">That\'s you</span>'
|
|
932
|
-
: `<div class="watch-actions">${statusBtn}</div>`;
|
|
933
|
-
const claims = num(b.active_claim_count);
|
|
934
|
-
const staleFlag = b.stale_claim ? ' <span class="badge badge--sev-medium">stale</span>' : '';
|
|
935
|
-
// task 1467: the roster name links to the builder's public profile page.
|
|
936
|
-
const rosterName = escapeHtml(b.display_name || b.github_login || ('#' + b.id));
|
|
937
|
-
const rosterNameCell = b.id != null
|
|
938
|
-
? `<a class="lb-name" href="/profile?id=${encodeURIComponent(b.id)}">${rosterName}</a>`
|
|
939
|
-
: rosterName;
|
|
940
|
-
return (
|
|
941
|
-
'<tr>' +
|
|
942
|
-
`<td>${rosterNameCell}` +
|
|
943
|
-
(b.github_login ? ` <span class="ov-mono">@${escapeHtml(b.github_login)}</span>` : '') +
|
|
944
|
-
'</td>' +
|
|
945
|
-
`<td>${rankCell}</td>` +
|
|
946
|
-
`<td class="num">${fmtInt(b.total_credits)}</td>` +
|
|
947
|
-
`<td class="num">${fmtInt(b.karma)}</td>` +
|
|
948
|
-
`<td class="num${b.stale_claim ? ' warn' : ''}">${fmtInt(claims)}${staleFlag}</td>` +
|
|
949
|
-
`<td><span class="badge badge--status">${escapeHtml(b.status || '—')}</span></td>` +
|
|
950
|
-
`<td>${actionCell}</td>` +
|
|
951
|
-
'</tr>'
|
|
952
|
-
);
|
|
953
|
-
},
|
|
954
|
-
}).render(builders);
|
|
955
|
-
|
|
956
|
-
// The one asymmetry in the table, stated once under it rather than repeated
|
|
957
|
-
// on the row. Appended as an ELEMENT, not with `innerHTML +=`: re-parsing the
|
|
958
|
-
// body would drop the pager's own wiring. The rank is written from
|
|
959
|
-
// rankLabel(), never a hardcoded "Archon", so the floor cannot drift.
|
|
960
|
-
if (builders.some(isViewer)) {
|
|
961
|
-
const note = document.createElement('p');
|
|
962
|
-
note.className = 'ov-foot';
|
|
963
|
-
note.textContent = `Your own row carries no controls. Nobody can change their own rank or standing — that guard is what stops the last ${rankLabel('archon')} locking everyone out — so another ${rankLabel('archon')} does it.`;
|
|
964
|
-
target.appendChild(note);
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// Rank change via the <select>. Bespoke: a 'change' listener that rolls the
|
|
968
|
-
// select back to its previous value on failure, delegated so a page flip
|
|
969
|
-
// cannot strand a row without one.
|
|
970
|
-
if (!target['wired:rank']) {
|
|
971
|
-
target['wired:rank'] = true;
|
|
972
|
-
target.addEventListener('change', async (e) => {
|
|
973
|
-
const sel = e.target.closest('[data-roster-rank]');
|
|
974
|
-
if (!sel || !target.contains(sel)) return;
|
|
975
|
-
const id = sel.getAttribute('data-id');
|
|
976
|
-
const rank = sel.value;
|
|
977
|
-
const prev = sel.getAttribute('data-current');
|
|
978
|
-
if (rank === prev) return;
|
|
979
|
-
sel.disabled = true;
|
|
980
|
-
try {
|
|
981
|
-
await patchJson(`/builders/${encodeURIComponent(id)}/rank`, { rank });
|
|
982
|
-
toast(`Rank set to ${rankLabel(rank)}.`);
|
|
983
|
-
await loadRoster();
|
|
984
|
-
} catch (err) {
|
|
985
|
-
if (err && err.kind === '401') return;
|
|
986
|
-
sel.value = prev; // roll back the select on failure
|
|
987
|
-
sel.disabled = false;
|
|
988
|
-
toast(`Could not change rank: ${err.message || err}`);
|
|
989
|
-
}
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// Status change (offboard / reactivate). Bespoke: it gathers an offboarding
|
|
994
|
-
// reason via window.prompt BEFORE disabling, so not delegate().
|
|
995
|
-
if (!target['wired:status']) {
|
|
996
|
-
target['wired:status'] = true;
|
|
997
|
-
target.addEventListener('click', async (e) => {
|
|
998
|
-
const btn = e.target.closest('[data-roster-status]');
|
|
999
|
-
if (!btn || !target.contains(btn)) return;
|
|
1000
|
-
const id = btn.getAttribute('data-id');
|
|
1001
|
-
const status = btn.getAttribute('data-roster-status');
|
|
1002
|
-
let reason = null;
|
|
1003
|
-
if (status === 'inactive') {
|
|
1004
|
-
reason = window.prompt('Reason for offboarding (optional):', '') || null;
|
|
1005
|
-
}
|
|
1006
|
-
btn.disabled = true;
|
|
1007
|
-
try {
|
|
1008
|
-
await patchJson(`/builders/${encodeURIComponent(id)}/status`, { status, reason });
|
|
1009
|
-
toast(status === 'inactive' ? 'Builder offboarded.' : 'Builder reactivated.');
|
|
1010
|
-
await loadRoster();
|
|
1011
|
-
} catch (err) {
|
|
1012
|
-
if (err && err.kind === '401') return;
|
|
1013
|
-
btn.disabled = false;
|
|
1014
|
-
toast(`Could not change standing: ${err.message || err}`);
|
|
1015
|
-
}
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
function loadRoster() {
|
|
1021
|
-
return section($('#roster-body'), () => fetchJson('/builders/roster'), renderRoster, {
|
|
1022
|
-
sealedMsg: 'The roster is not visible at your rank.',
|
|
1023
|
-
errMsg: 'Could not load the roster.',
|
|
1024
|
-
name: 'roster',
|
|
1025
|
-
});
|
|
875
|
+
// The roster that lived here is the Builders page now (owner decision on idea
|
|
876
|
+
// 1001313: one list of builders). The attention tile keeps its count, read
|
|
877
|
+
// from the same route, and links to that list filtered to the stale claims.
|
|
878
|
+
function loadStaleCount() {
|
|
879
|
+
return fetchJson('/builders/roster')
|
|
880
|
+
.then((data) => {
|
|
881
|
+
const builders = Array.isArray(data && data.builders) ? data.builders : [];
|
|
882
|
+
setAttention('att-stale', builders.filter((b) => b.stale_claim).length);
|
|
883
|
+
})
|
|
884
|
+
.catch(() => setAttention('att-stale', null));
|
|
1026
885
|
}
|
|
1027
886
|
|
|
1028
887
|
// =========================================================================
|
|
@@ -1411,7 +1270,7 @@
|
|
|
1411
1270
|
|
|
1412
1271
|
function revealSections() {
|
|
1413
1272
|
[
|
|
1414
|
-
'watch-attention', 'watch-tabs', 'gate-scroll', 'watch-scroll',
|
|
1273
|
+
'watch-attention', 'watch-tabs', 'gate-scroll', 'watch-scroll',
|
|
1415
1274
|
'marks-scroll', 'mason-marks-scroll', 'surveyor-scroll', 'ledger-scroll', 'sessions-scroll',
|
|
1416
1275
|
].forEach((id) => {
|
|
1417
1276
|
const el = document.getElementById(id);
|
|
@@ -1427,7 +1286,15 @@
|
|
|
1427
1286
|
{ id: 'people', label: 'People', panel: 'pane-people' },
|
|
1428
1287
|
{ id: 'operations', label: 'Operations', panel: 'pane-operations' },
|
|
1429
1288
|
{ id: 'security', label: 'Security', panel: 'pane-security' },
|
|
1289
|
+
// task 1005049 (owner decision 2026-11-03, idea 1001315): the Sessions and
|
|
1290
|
+
// Autobongos pages are tabs here. Each boots the first time it opens.
|
|
1291
|
+
{ id: 'sessions', label: 'Sessions', panel: 'pane-sessions' },
|
|
1292
|
+
{ id: 'autobongos', label: 'Autobongos', panel: 'pane-autobongos' },
|
|
1430
1293
|
],
|
|
1294
|
+
onChange: (id) => {
|
|
1295
|
+
const boot = window.OTBWatchTabs && window.OTBWatchTabs[id];
|
|
1296
|
+
if (boot) boot();
|
|
1297
|
+
},
|
|
1431
1298
|
});
|
|
1432
1299
|
}
|
|
1433
1300
|
}
|
|
@@ -1441,11 +1308,6 @@
|
|
|
1441
1308
|
if (!res.ok) throw new Error(`me → ${res.status}`);
|
|
1442
1309
|
const payload = res.data;
|
|
1443
1310
|
builder = (payload && payload.builder) ? payload.builder : payload;
|
|
1444
|
-
// Kept for the roster, which must know which row is the reader's own
|
|
1445
|
-
// (task 1003720). Null when /me answered without one — isViewer then
|
|
1446
|
-
// matches nothing, so the roster degrades to the old every-row-acts shape
|
|
1447
|
-
// rather than marking the wrong row as yours.
|
|
1448
|
-
viewerId = (builder && builder.id != null) ? builder.id : null;
|
|
1449
1311
|
} catch (err) {
|
|
1450
1312
|
console.error('[watch] failed to read viewer', err);
|
|
1451
1313
|
showSealed('Could not check your access. Reload to try again.', 'Could not load');
|
|
@@ -1454,7 +1316,7 @@
|
|
|
1454
1316
|
}
|
|
1455
1317
|
|
|
1456
1318
|
// ADR 0157: Watch sits at the `page.view.government` floor (metic), and every panel
|
|
1457
|
-
// it reads (
|
|
1319
|
+
// it reads (stale claims, security reports, audit log, sessions, box costs) delegated to
|
|
1458
1320
|
// metic with it. Cosmetic — requireGovernmentPage + the route gates are the wall.
|
|
1459
1321
|
if (!builder || (builder.rank !== 'metic' && builder.rank !== 'archon')) {
|
|
1460
1322
|
showSealed(`Available from the ${rankLabel('metic')} rank up. Your current rank does not grant access.`);
|
|
@@ -1469,7 +1331,7 @@
|
|
|
1469
1331
|
loadEngagementDocs();
|
|
1470
1332
|
loadOverrides();
|
|
1471
1333
|
loadAudit();
|
|
1472
|
-
|
|
1334
|
+
loadStaleCount();
|
|
1473
1335
|
loadMarks();
|
|
1474
1336
|
loadMasonMarks();
|
|
1475
1337
|
loadSurveyor();
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"/api/(gds|bongos)/versions",
|
|
16
16
|
"/api/(gds|bongos)/watch",
|
|
17
17
|
"/api/(gds|bongos)/analytics",
|
|
18
|
-
"/api/(gds|bongos)/tasks"
|
|
18
|
+
"/api/(gds|bongos)/tasks",
|
|
19
|
+
"/api/(gds|bongos)/sessions",
|
|
20
|
+
"/api/(gds|bongos)/autonomy/"
|
|
19
21
|
],
|
|
20
22
|
"ignoreConsole": [
|
|
21
23
|
"/api/(gds|bongos)/",
|
|
@@ -92,6 +94,52 @@
|
|
|
92
94
|
"#watch-scroll"
|
|
93
95
|
]
|
|
94
96
|
}
|
|
97
|
+
},
|
|
98
|
+
"sessions": {
|
|
99
|
+
"auth": true,
|
|
100
|
+
"actions": [
|
|
101
|
+
[
|
|
102
|
+
"wait",
|
|
103
|
+
700
|
|
104
|
+
],
|
|
105
|
+
[
|
|
106
|
+
"click",
|
|
107
|
+
"#tab-sessions"
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
"wait",
|
|
111
|
+
500
|
|
112
|
+
]
|
|
113
|
+
],
|
|
114
|
+
"expect": {
|
|
115
|
+
"visible": [
|
|
116
|
+
"#pane-sessions",
|
|
117
|
+
"#stats-scroll",
|
|
118
|
+
"#sess-ledger-scroll"
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"autobongos": {
|
|
123
|
+
"auth": true,
|
|
124
|
+
"actions": [
|
|
125
|
+
[
|
|
126
|
+
"wait",
|
|
127
|
+
700
|
|
128
|
+
],
|
|
129
|
+
[
|
|
130
|
+
"click",
|
|
131
|
+
"#tab-autobongos"
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
"wait",
|
|
135
|
+
500
|
|
136
|
+
]
|
|
137
|
+
],
|
|
138
|
+
"expect": {
|
|
139
|
+
"visible": [
|
|
140
|
+
"#pane-autobongos"
|
|
141
|
+
]
|
|
142
|
+
}
|
|
95
143
|
}
|
|
96
144
|
},
|
|
97
145
|
"reducedMotion": {
|
|
@@ -173,8 +173,8 @@
|
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
174
|
<script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
|
|
175
175
|
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
176
|
-
<script src="/builders/craft-mode.js?v=2026-11-05-
|
|
177
|
-
<script src="/builders/shell.js?v=2026-11-05-
|
|
176
|
+
<script src="/builders/craft-mode.js?v=2026-11-05-oversight"></script>
|
|
177
|
+
<script src="/builders/shell.js?v=2026-11-05-oversight"></script>
|
|
178
178
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
179
179
|
<script src="/builders/palette.js" defer></script>
|
|
180
180
|
<!-- task 1004727: the ONE karma vote control every task/idea list renders -->
|
|
@@ -37,3 +37,5 @@ The owner's Studio idea 1001297 (a circuit-trace image as a table divider; mock
|
|
|
37
37
|
- **The pseudo-elements sit on the row's first `td`, never on the `tr`.** On the `tr` they became two extra table cells and pushed every cell in the row two columns right (seen in the first render). The `tr` only carries `position: relative`, so the `td`'s absolute children size against the whole row.
|
|
38
38
|
- **Two masks, two tokens.** Traces are `--ink-faint` and the orange nodes `--accent`, each shown through an SVG mask, so light and dark follow the world with no hard-coded colour. Left cap, right cap and the stretching hairline are three layers of one mask.
|
|
39
39
|
- **The empty-state row has no trace** (`:not(:has(.ledger__empty))`). Checked at desktop and 375px, light and dark: the table scrolls inside its card on a phone and the divider spans it; the page does not scroll sideways.
|
|
40
|
+
|
|
41
|
+
**Since task 1005048 it is the one list.** Watch's own roster folded in: a viewer the `GET /builders/roster` route lets in also sees active claims, the stale flag, standing and the rank/offboard controls, and the route's refusal leaves this page exactly the public list described above.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.41",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.41",
|
|
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.
|
|
3
|
+
"version": "1.24.41",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -11197,5 +11197,17 @@
|
|
|
11197
11197
|
"id": "1005077",
|
|
11198
11198
|
"text": "The governor street is now a work-first square: the working areas left the street, the extra header and footer are gone, a pin takes you straight into its building, and inside a building the drawing is a compact floor picker s"
|
|
11199
11199
|
}
|
|
11200
|
+
],
|
|
11201
|
+
"1.24.40": [
|
|
11202
|
+
{
|
|
11203
|
+
"id": "1005048",
|
|
11204
|
+
"text": "The Builders page is now the one list of everyone who builds here. If you hold a government seat you also see each builder's active claims, any stale claim, their standing, and the controls to change a rank or offboard someone"
|
|
11205
|
+
}
|
|
11206
|
+
],
|
|
11207
|
+
"1.24.41": [
|
|
11208
|
+
{
|
|
11209
|
+
"id": "1005049",
|
|
11210
|
+
"text": "Watch is now the one oversight page. Sessions and Autobongos moved off the sidebar and became two tabs on Watch, next to People, Operations and Security. Old links to /sessions and /gate open the right tab. Who can see and pre"
|
|
11211
|
+
}
|
|
11200
11212
|
]
|
|
11201
11213
|
}
|
|
@@ -62,6 +62,8 @@ const ROUTES = {
|
|
|
62
62
|
'/ranks': { to: '/primer', why: 'now a Primer chapter, redirects (task 1005046)' },
|
|
63
63
|
'/drachmae': { to: '/primer', why: 'now a Primer chapter, redirects (task 1005046)' },
|
|
64
64
|
'/diagrams': { to: '/primer', why: 'now a Primer chapter, redirects; its images still serve (task 1005046)' },
|
|
65
|
+
'/sessions': { to: '/watch', why: 'now a Watch tab, redirects (task 1005049)' },
|
|
66
|
+
'/gate': { to: '/watch', why: 'Autobongos, now a Watch tab, redirects (task 1005049)' },
|
|
65
67
|
};
|
|
66
68
|
// Targets that leave the hall on purpose: sign-in, the hub's /account (platform-server.js;
|
|
67
69
|
// settings.js points the link at the hub), the API, module assets, and any file with an extension.
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -146,7 +146,9 @@ const discordIsLinked = (builderId) =>
|
|
|
146
146
|
// buttons for a viewer who cannot vote on ideas, without re-deriving it from rank.
|
|
147
147
|
// `blocker.review` (task 1004824): the Blockers page and its overlay offer Resolve and
|
|
148
148
|
// Link only to a builder who holds it; POST /blockers/:id/{resolve,link} stays the wall.
|
|
149
|
-
|
|
149
|
+
// `builder.roster.read` (task 1005048): the Builders page asks GET /builders/roster
|
|
150
|
+
// for its government columns only of a viewer who holds it; the route stays the wall.
|
|
151
|
+
const NAV_PERMISSION_ATOMS = Object.freeze(['board.vote.cast', 'core.pin.move', 'idea.triage', 'board.item.open', 'idea.vote', 'blocker.review', 'builder.roster.read']);
|
|
150
152
|
|
|
151
153
|
// navPermissionsFor(builderId) → { '<atom>': boolean } over NAV_PERMISSION_ATOMS,
|
|
152
154
|
// or null when authority could not be established (government off, port missing,
|
|
@@ -927,23 +927,13 @@ function mountInternalSurfaces(app) {
|
|
|
927
927
|
// The apex hands /builders/* (ADR 0036) and /deploy (task 1004736) to the hall's own host.
|
|
928
928
|
mountApexHallRedirects(app, { skip: (req) => isModuleSurfaceHost(req) || isBuildersHost(req), hostOf: (req) => siteOf(req).host });
|
|
929
929
|
|
|
930
|
-
// The /watch monitoring page
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
// (Defense in depth: every data endpoint these pages call carries its own
|
|
937
|
-
// server-side gate.)
|
|
930
|
+
// The /watch monitoring page sits behind `page.view.government` — Metic and up
|
|
931
|
+
// since ADR 0157: a builder without the permission never receives the shell
|
|
932
|
+
// (requireGovernmentPage redirects them to the hall, and an unauthenticated
|
|
933
|
+
// visitor into sign-in). Its Sessions and Autobongos tabs were the /sessions and
|
|
934
|
+
// /gate pages until task 1005049, behind this same gate. (Defense in depth: every
|
|
935
|
+
// data endpoint the page calls carries its own server-side gate.)
|
|
938
936
|
const WATCH_PAGE_RE = /^\/builders\/watch(?:\.html)?\/?$/;
|
|
939
|
-
const GATE_PAGE_RE = /^\/builders\/gate(?:\.html)?\/?$/;
|
|
940
|
-
// /sessions (the session-log ledger) belongs to that same Governance group and
|
|
941
|
-
// its data route (`session.search`) carries the same metic floor since ADR 0157
|
|
942
|
-
// — but it had NO page gate at all: sessions.js only called showSealed() after
|
|
943
|
-
// /api/bongos/me, so the shell was served to anyone who asked for the URL. The
|
|
944
|
-
// data never leaked (the route gate held), but the page was the one item in the
|
|
945
|
-
// group without a wall. Gated here so the group is uniform (task 1002710).
|
|
946
|
-
const SESSIONS_PAGE_RE = /^\/builders\/sessions(?:\.html)?\/?$/;
|
|
947
937
|
// /project-settings — the five fundamentals of the project (task 1003280, goal
|
|
948
938
|
// 1000072). Government tier, beside /watch and /sessions: it is a STEERING surface,
|
|
949
939
|
// and ADR 0157 put that floor at metic. Both writes behind it gate independently on
|
|
@@ -1049,14 +1039,18 @@ function mountInternalSurfaces(app) {
|
|
|
1049
1039
|
return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/market-watch${qs}#settings`);
|
|
1050
1040
|
});
|
|
1051
1041
|
|
|
1052
|
-
//
|
|
1053
|
-
|
|
1042
|
+
// Pages folded into a section of another page; 302 and no gate, as above. Task 1005047:
|
|
1043
|
+
// Fleet costs is /money#hosting. Task 1005049: Sessions and Autobongos are Watch tabs.
|
|
1044
|
+
const RETIRED_TO_SECTION = Object.freeze({ fleet: 'money#hosting', sessions: 'watch#sessions', gate: 'watch#autobongos' });
|
|
1045
|
+
const RETIRED_SECTION_RE = /^\/builders\/(fleet|sessions|gate)(?:\.html)?\/?$/;
|
|
1054
1046
|
app.use((req, res, next) => {
|
|
1055
1047
|
if (isModuleSurfaceHost(req)) return next();
|
|
1056
1048
|
if (req.method !== 'GET' && req.method !== 'HEAD') return next();
|
|
1057
|
-
|
|
1049
|
+
const m = RETIRED_SECTION_RE.exec(path.posix.normalize(req.path));
|
|
1050
|
+
if (!m) return next();
|
|
1058
1051
|
const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
|
|
1059
|
-
|
|
1052
|
+
const [page, section] = RETIRED_TO_SECTION[m[1]].split('#');
|
|
1053
|
+
return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/${page}${qs}#${section}`);
|
|
1060
1054
|
});
|
|
1061
1055
|
|
|
1062
1056
|
// task 1005046 (owner decision 2026-11-03, idea 1001311): ONE reading room. Ranks,
|
|
@@ -1104,8 +1098,7 @@ function mountInternalSurfaces(app) {
|
|
|
1104
1098
|
if (isModuleSurfaceHost(req)) return next();
|
|
1105
1099
|
if (!req.path.startsWith('/builders')) return next();
|
|
1106
1100
|
const norm = path.posix.normalize(req.path);
|
|
1107
|
-
if (WATCH_PAGE_RE.test(norm) ||
|
|
1108
|
-
|| SESSIONS_PAGE_RE.test(norm) || PROJECT_SETTINGS_PAGE_RE.test(norm)
|
|
1101
|
+
if (WATCH_PAGE_RE.test(norm) || PROJECT_SETTINGS_PAGE_RE.test(norm)
|
|
1109
1102
|
|| MONEY_PAGE_RE.test(norm)) {
|
|
1110
1103
|
return gdsAuth.requireGovernmentPage(req, res, next);
|
|
1111
1104
|
}
|
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.
|
|
84
|
+
const CORE_VERSION = '1.24.41'; // 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');
|
|
@@ -460,7 +460,8 @@ test('the owner page draws the PROJECT fence and pauses one builder; the setting
|
|
|
460
460
|
assert.match(gate, /const fence = raw\.project \|\| raw;/, 'the switch panel must not draw the owner’s own narrowed picks as the project list');
|
|
461
461
|
assert.match(gate, /\/autonomy\/runners\/all/);
|
|
462
462
|
assert.match(gate, /\/autonomy\/builders\/\$\{encodeURIComponent\(id\)\}\/pause/);
|
|
463
|
-
|
|
463
|
+
// task 1005049: the owner panel is Watch's Autobongos tab.
|
|
464
|
+
const html = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/watch.html'), 'utf8');
|
|
464
465
|
assert.match(html, /id="fence-builders"/);
|
|
465
466
|
|
|
466
467
|
const panel = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/settings-autobongos.js'), 'utf8');
|
package/tests/hall_audit.mjs
CHANGED
|
@@ -233,7 +233,7 @@ test('the session log pages on the kit — the last hand-roll is gone', () => {
|
|
|
233
233
|
assert.match(js, new RegExp(`kit\\.${fn}\\(`), `sessions.js must page through the kit's ${fn}`);
|
|
234
234
|
}
|
|
235
235
|
assert.doesNotMatch(js, /const PAGE_SIZE\b/, 'its fixed page size is gone — per-page is the kit\'s now');
|
|
236
|
-
assert.match(read('
|
|
236
|
+
assert.match(read('watch.html'), /hall-kit\.js/, 'and Watch, whose Sessions tab it is (task 1005049), has to load the kit');
|
|
237
237
|
});
|
|
238
238
|
|
|
239
239
|
// ---- 6b. every hall bar series is drawn by the kit (task 1003445) ---------
|
package/tests/hall_loading.mjs
CHANGED
|
@@ -83,7 +83,7 @@ test('the page loaders step aside by themselves, and never show without :has()',
|
|
|
83
83
|
assert.match(STYLE, /\.hall-loading--wait, \.hall-loading--zones \{ display: none; \}/);
|
|
84
84
|
assert.match(STYLE, /@supports selector\(:has\(a\)\) \{\s*\.hall-loading--wait:not\(:has\(~ :not\(\[hidden\], script, template, style\)\)\) \{ display: flex; \}\s*\.hall-loading--zones:not\(:has\(~ :not\(\[hidden\]\) > :not\(\[hidden\], script, template, style\)\)\) \{ display: flex; \}\s*\}/);
|
|
85
85
|
// A page whose sections all start [hidden] carries the --wait loader FIRST in its main.
|
|
86
|
-
for (const page of ['board-room', 'deploy', '
|
|
86
|
+
for (const page of ['board-room', 'deploy', 'government', 'money', 'project-settings']) {
|
|
87
87
|
const html = read(`${page}.html`);
|
|
88
88
|
const main = html.slice(html.indexOf('<main id="hall-main">'));
|
|
89
89
|
const first = main.replace(/<main id="hall-main">\s*(<!--[\s\S]*?-->\s*)*/, '');
|
|
@@ -95,6 +95,17 @@ test('the page loaders step aside by themselves, and never show without :has()',
|
|
|
95
95
|
}
|
|
96
96
|
assert.match(read('index.html'), /<section class="view" id="view-home"[^>]*>\s*<!--[^>]*-->\s*<p class="hall-loading hall-loading--page hall-loading--zones" role="status">Loading…<\/p>/, 'Home waits on its zones');
|
|
97
97
|
assert.match(read('watch.html'), /<main id="hall-main">\s*<!--[^>]*-->\s*<p class="hall-loading hall-loading--page hall-loading--zones" role="status">Loading…<\/p>/, 'Watch waits on its panes');
|
|
98
|
+
// task 1005049: the Sessions and Autobongos tabs (once their own pages) open on the
|
|
99
|
+
// --wait loader, so a tab whose sections are all still hidden says it is loading.
|
|
100
|
+
for (const pane of ['pane-sessions', 'pane-autobongos']) {
|
|
101
|
+
const html = read('watch.html');
|
|
102
|
+
const inner = html.slice(html.indexOf(`<div id="${pane}"`));
|
|
103
|
+
const body = inner.slice(inner.indexOf('>') + 1).replace(/^\s*(<!--[\s\S]*?-->\s*)*/, '');
|
|
104
|
+
assert.match(body, /^<p class="hall-loading hall-loading--page hall-loading--wait" role="status">Loading…<\/p>/, `${pane} opens on the page loader`);
|
|
105
|
+
const kids = [...body.slice(body.indexOf('</p>') + 4, body.indexOf('\n </div>')).matchAll(/^ {10}<(section|div|p|nav)\b([^>]*)>/gm)];
|
|
106
|
+
assert.ok(kids.length >= 1, `${pane} has sections after the loader`);
|
|
107
|
+
for (const [, tag, attrs] of kids) assert.match(attrs, /\bhidden\b/, `${pane}: a <${tag}> after the loader starts hidden`);
|
|
108
|
+
}
|
|
98
109
|
// Settings' layout is never [hidden] while it loads, so its loader is put away by hideLayout().
|
|
99
110
|
assert.match(read('settings.html'), /id="settings-groups"[^>]*>\s*<!--[^>]*-->\s*<p class="hall-loading hall-loading--page" id="settings-loading" role="status">/);
|
|
100
111
|
assert.match(read('settings.js'), /function hideLayout\(hide\) \{[\s\S]*?getElementById\('settings-loading'\)[\s\S]*?loading\.hidden = true;/);
|
|
@@ -105,7 +116,7 @@ test('no hall page waits on a bare line of text any more', () => {
|
|
|
105
116
|
// the class would stay behind on the finished sentence. Each is named with why.
|
|
106
117
|
const IN_PLACE = new Set([
|
|
107
118
|
'atlas.html#caption', // the map's caption, rewritten by atlas.js
|
|
108
|
-
'
|
|
119
|
+
'watch.html#ledger-status', // the Sessions tab's status line, rewritten by sessions.js
|
|
109
120
|
'goals.html#goal-deps-card-lede', // the dependency card's lede, rewritten by goals-page.js
|
|
110
121
|
'goals.html#goal-deps-lede',
|
|
111
122
|
'profile.html#activity-placeholder', // reused for the signed-out notice by profile.js
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -335,7 +335,10 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
335
335
|
// 'market-watch-settings' (task 1004938) LEFT the group in task 1005045: the owner saw
|
|
336
336
|
// "Market Watch" twice and had to guess which was which (owner decision 2026-11-03,
|
|
337
337
|
// idea 1001310). The settings are an owner-only Settings tab on /market-watch now.
|
|
338
|
-
|
|
338
|
+
// 'gate' (Autobongos) and 'sessions' LEFT the group in task 1005049 (owner decision
|
|
339
|
+
// 2026-11-03, idea 1001315): they are tabs on Watch, and /gate + /sessions redirect there.
|
|
340
|
+
assert.deepEqual(nav[5].items.map((i) => i.id), ['watch', 'project-settings', 'money', 'deploy', 'government']);
|
|
341
|
+
assert.ok(!nav.flatMap((g) => g.items).some((i) => i.href === '/gate' || i.href === '/sessions'), 'no item points at a page that became a Watch tab');
|
|
339
342
|
// task 1005045: and no label is left twice anywhere in the sidebar, so the next pair
|
|
340
343
|
// fails here instead of in front of the owner.
|
|
341
344
|
const labels = [...boot().sidebarHtml.matchAll(/<a class="nav-item" [^>]*>[\s\S]*?<span class="nav-item__label">([^<]*)<\/span>/g)].map((m) => m[1]);
|
|
@@ -787,7 +790,7 @@ test('no glyph path data reads as an IP address to the release leak scan', () =>
|
|
|
787
790
|
// block (task 1004713). These drive the real shell and the real gov-positions.js.
|
|
788
791
|
|
|
789
792
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
790
|
-
const GOVERNING = ['city', 'board-room', 'watch', '
|
|
793
|
+
const GOVERNING = ['city', 'board-room', 'watch', 'project-settings', 'money', 'deploy', 'government'];
|
|
791
794
|
const lead = (building, title) => ({ kind: 'lead', seat: 'lead:' + building, building, title });
|
|
792
795
|
const block = (held) => ({ held, title: held.length ? held[0].title : null, has_position: held.length > 0 });
|
|
793
796
|
|