@bongos/core 1.21.22 → 1.21.24
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 +62 -62
- package/docs/copy-inventory.md +128 -123
- package/docs/copy-registry.json +222 -187
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +251 -248
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +2 -2
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +5 -7
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/record.css +4 -19
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +14 -2
- package/modules/hall-ui/public/sessions.js +111 -3
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +56 -5
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +6 -9
- package/modules/hall-ui/public/task.js +15 -50
- package/modules/hall-ui/public/task.states.json +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.css +4 -0
- package/modules/hall-ui/public/tweak-editor.html +5 -3
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/hall-navigation.md +7 -4
- package/modules/sessions/db.js +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/hall_fullscreen.mjs +88 -12
- package/tests/hall_record_world.mjs +41 -63
- package/tests/hall_sessions_ledger.mjs +40 -0
- package/tests/hall_tweak_editor.mjs +3 -2
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/session_transcript.mjs +5 -0
|
@@ -513,9 +513,16 @@
|
|
|
513
513
|
// not a drill-down and shows none: the sidebar click leaves a one-shot note
|
|
514
514
|
// (sessionStorage) naming its target, and the page it lands on reads it.
|
|
515
515
|
// The origin is `?from=` (a link that knows where it came from), else the
|
|
516
|
-
// same-origin referrer.
|
|
517
|
-
//
|
|
518
|
-
//
|
|
516
|
+
// same-origin referrer.
|
|
517
|
+
//
|
|
518
|
+
// task 1004477: this is the ONLY place a hall page's way back is decided —
|
|
519
|
+
// no page draws its own Back link any more. Two hooks cover what the page
|
|
520
|
+
// copies used to:
|
|
521
|
+
// - <body data-back-fallback="/work"> names where Back goes when there is
|
|
522
|
+
// no origin at all (a deep-linked task or idea, the tweak editor);
|
|
523
|
+
// - a link marked [data-shell-exit] is a second exit the page places (an
|
|
524
|
+
// error state's button, the tweak editor's bar in full screen, where the
|
|
525
|
+
// top bar is hidden), and the shell fills its address and its words.
|
|
519
526
|
var NAV_HOP_KEY = 'hall-nav-hop';
|
|
520
527
|
function hallPath(url) {
|
|
521
528
|
return String(url.pathname).replace(/^\/builders(?=\/|$)/, '').replace(/\.html$/, '').replace(/\/index$/, '') || '/';
|
|
@@ -527,8 +534,10 @@
|
|
|
527
534
|
window.sessionStorage.removeItem(NAV_HOP_KEY);
|
|
528
535
|
} catch (_) { /* private mode: no note, so fall through */ }
|
|
529
536
|
if (hop && hop === hallPath(location)) return null;
|
|
530
|
-
|
|
531
|
-
|
|
537
|
+
return linkOrigin() || fallbackOrigin();
|
|
538
|
+
}
|
|
539
|
+
// The page this one was opened from: ?from=, then a same-origin referrer.
|
|
540
|
+
function linkOrigin() {
|
|
532
541
|
var from = null;
|
|
533
542
|
var viaReferrer = false;
|
|
534
543
|
try {
|
|
@@ -544,9 +553,48 @@
|
|
|
544
553
|
var params = new URLSearchParams(from.search);
|
|
545
554
|
params.delete('from');
|
|
546
555
|
var search = params.toString() ? '?' + params.toString() : '';
|
|
556
|
+
// This page's address without its own ?from=, so `?from=` naming the page
|
|
557
|
+
// itself (a loop, not a way back) is caught like any other self-link.
|
|
558
|
+
var ownParams = new URLSearchParams(location.search || '');
|
|
559
|
+
ownParams.delete('from');
|
|
560
|
+
var here = hallPath(location) + (ownParams.toString() ? '?' + ownParams.toString() : '');
|
|
547
561
|
if (path + search === here) return null;
|
|
548
562
|
return { href: path + search + (from.hash || ''), path: path, label: backLabel(path, from.hash), viaReferrer: viaReferrer };
|
|
549
563
|
}
|
|
564
|
+
// Where Back goes with no origin: the page's own <body data-back-fallback>,
|
|
565
|
+
// honoured only as a path on this host (one leading slash, never the API).
|
|
566
|
+
function fallbackOrigin() {
|
|
567
|
+
var f = document.body && document.body.dataset && document.body.dataset.backFallback;
|
|
568
|
+
if (!f || !/^\/(?!\/)/.test(f) || /[\\\s]/.test(f)) return null;
|
|
569
|
+
var u;
|
|
570
|
+
try { u = new URL(f, location.origin); } catch (_) { return null; }
|
|
571
|
+
if (u.origin !== location.origin) return null;
|
|
572
|
+
var path = hallPath(u);
|
|
573
|
+
if (/^\/(api|auth|lib)(\/|$)/.test(path) || path === hallPath(location)) return null;
|
|
574
|
+
return { href: path + (u.search || '') + (u.hash || ''), path: path, label: backLabel(path, u.hash), viaReferrer: false };
|
|
575
|
+
}
|
|
576
|
+
// "Back to tasks", "Back to goal 12": a nav label reads as a place mid-sentence,
|
|
577
|
+
// so it is lowercased unless it carries an acronym ("API docs").
|
|
578
|
+
function exitWords(label) {
|
|
579
|
+
return 'Back to ' + (/[A-Z]{2}/.test(label) ? label : label.toLowerCase());
|
|
580
|
+
}
|
|
581
|
+
// Fill every [data-shell-exit] link under `root` with the way back. With no
|
|
582
|
+
// way back at all the exit still works: it goes home. A page script that
|
|
583
|
+
// renders an exit later (task.js's failure states) calls this once it has.
|
|
584
|
+
function fillExits(root) {
|
|
585
|
+
var scope = root || document;
|
|
586
|
+
if (!scope.querySelectorAll) return;
|
|
587
|
+
var target = BACK || { href: '/', label: 'Home', viaReferrer: false };
|
|
588
|
+
var links = scope.querySelectorAll('[data-shell-exit]');
|
|
589
|
+
for (var i = 0; i < links.length; i++) {
|
|
590
|
+
links[i].setAttribute('href', target.href);
|
|
591
|
+
links[i].textContent = exitWords(target.label);
|
|
592
|
+
if (!links[i].dataset.shellExitWired) {
|
|
593
|
+
links[i].dataset.shellExitWired = '1';
|
|
594
|
+
links[i].addEventListener('click', onBackClick);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
550
598
|
function backLabel(path, hash) {
|
|
551
599
|
if (path === '/') {
|
|
552
600
|
var m = /^#\/(task|idea|blocker)\/(\d+)/.exec(hash || '');
|
|
@@ -1381,6 +1429,7 @@
|
|
|
1381
1429
|
applyFullscreen(storedFullscreen());
|
|
1382
1430
|
applyAccess(null); // default: everything gated until access is known
|
|
1383
1431
|
wire();
|
|
1432
|
+
fillExits();
|
|
1384
1433
|
if (IS_INDEX) {
|
|
1385
1434
|
route({ scroll: false });
|
|
1386
1435
|
} else {
|
|
@@ -1407,5 +1456,7 @@
|
|
|
1407
1456
|
// task 1004544: a page with its own light/dark switch (the governor city
|
|
1408
1457
|
// and street) repaints the top bar's after writing 'hall-theme'.
|
|
1409
1458
|
applyTheme: applyTheme,
|
|
1459
|
+
// task 1004477: a page that renders an exit after boot asks the shell to fill it.
|
|
1460
|
+
fillExits: fillExits,
|
|
1410
1461
|
};
|
|
1411
1462
|
})();
|
|
@@ -203,7 +203,7 @@
|
|
|
203
203
|
</div>
|
|
204
204
|
</div>
|
|
205
205
|
|
|
206
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
206
|
+
<script src="/builders/shell.js?v=2026-10-02-back"></script>
|
|
207
207
|
<script src="/builders/palette.js" defer></script>
|
|
208
208
|
<script src="/builders/city-draw.js?v=2026-10-02-street1"></script>
|
|
209
209
|
<script src="/builders/street-draw.js?v=2026-10-02-street1"></script>
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
|
|
177
177
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
178
178
|
|
|
179
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
179
|
+
<script src="/builders/shell.js?v=2026-10-02-back"></script>
|
|
180
180
|
<script src="/builders/palette.js" defer></script>
|
|
181
181
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
182
182
|
<!-- The kit, for its ONE renderer: the full-size ship-time visual figure
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<link rel="stylesheet" href="/builders/record.css">
|
|
16
16
|
<link rel="stylesheet" href="/builders/task.css">
|
|
17
17
|
</head>
|
|
18
|
-
<body data-page="task" data-page-title="Task">
|
|
18
|
+
<body data-page="task" data-page-title="Task" data-back-fallback="/work">
|
|
19
19
|
|
|
20
20
|
<div class="app">
|
|
21
21
|
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
@@ -24,14 +24,11 @@
|
|
|
24
24
|
<header class="app-topbar" id="app-topbar"></header>
|
|
25
25
|
<div class="app-content">
|
|
26
26
|
<div class="page-head">
|
|
27
|
-
<!-- task
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
referrer); /work is only the fallback it ships with. -->
|
|
27
|
+
<!-- task 1004477: the way back is the shell's top-bar Back link, set
|
|
28
|
+
before the task body loads. This page is deliberately not in the
|
|
29
|
+
nav, so <body data-back-fallback> gives a deep-linked visitor one
|
|
30
|
+
even with no ?from= and no referrer. -->
|
|
32
31
|
<div class="rec-crumb">
|
|
33
|
-
<a class="rec-back" id="task-back" href="/work">
|
|
34
|
-
<svg class="rec-back__ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M9.4 3.6 5 8l4.4 4.4"/><path d="M5 8h7.2"/></svg><span id="task-back-label">Back to tasks</span></a>
|
|
35
32
|
<p class="rec-sub" id="task-sub">Loading task</p>
|
|
36
33
|
</div>
|
|
37
34
|
</div>
|
|
@@ -57,7 +54,7 @@
|
|
|
57
54
|
|
|
58
55
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
59
56
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
60
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
57
|
+
<script src="/builders/shell.js?v=2026-10-02-back"></script>
|
|
61
58
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
62
59
|
<script src="/builders/palette.js" defer></script>
|
|
63
60
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
|
|
@@ -182,57 +182,20 @@
|
|
|
182
182
|
return m ? m[1] : null;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
// task
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// never the API and never this same record.
|
|
192
|
-
const EXIT_MAX_LEN = 2048;
|
|
193
|
-
function safeHallPath(v) {
|
|
194
|
-
if (typeof v !== 'string' || !v || v.length > EXIT_MAX_LEN) return null;
|
|
195
|
-
if (!/^\/(?!\/)/.test(v) || /[\\\s]/.test(v)) return null;
|
|
196
|
-
if (/^(\/builders)?\/(api|lib)\//.test(v)) return null;
|
|
197
|
-
if (v.split(/[?#]/)[0] === location.pathname) return null;
|
|
198
|
-
return v;
|
|
199
|
-
}
|
|
200
|
-
function referrerPath() {
|
|
201
|
-
let u;
|
|
202
|
-
try { u = new URL(document.referrer); } catch (_) { return null; }
|
|
203
|
-
if (u.origin !== location.origin) return null;
|
|
204
|
-
return safeHallPath(u.pathname + u.search + u.hash);
|
|
205
|
-
}
|
|
206
|
-
// "goal 1000081", "task 12", "the work board", "sessions" — said the way the
|
|
207
|
-
// sidebar says it, so the exit reads as a place and not as a URL.
|
|
208
|
-
function exitLabel(href) {
|
|
209
|
-
const rel = href.replace(/^\/builders(?=[/?#]|$)/, '');
|
|
210
|
-
let m;
|
|
211
|
-
if ((m = /#\/goal\/(\d+)/.exec(rel))) return `goal ${m[1]}`;
|
|
212
|
-
if ((m = /^\/(task|idea)\/(\d+)/.exec(rel))) return `${m[1]} ${m[2]}`;
|
|
213
|
-
const seg = (/^\/([a-z][a-z-]*)/.exec(rel) || [])[1];
|
|
214
|
-
if (!seg) return 'home';
|
|
215
|
-
if (seg === 'work') return 'the work board';
|
|
216
|
-
return seg.replace(/-/g, ' ');
|
|
217
|
-
}
|
|
218
|
-
function exitTarget() {
|
|
219
|
-
let from = null;
|
|
220
|
-
try { from = new URLSearchParams(location.search).get('from'); } catch (_) { /* no query */ }
|
|
221
|
-
const href = safeHallPath(from) || referrerPath() || `${hallBase()}/work`;
|
|
222
|
-
return { href, label: exitLabel(href) };
|
|
223
|
-
}
|
|
185
|
+
// task 1004477 — the way back is the SHELL's, not this page's. task 1003468
|
|
186
|
+
// worked out where the reader came from here (?from=, then the referrer, then
|
|
187
|
+
// the board); shell.js now decides that for every hall page, and this page
|
|
188
|
+
// only places the exit: a [data-shell-exit] link the shell fills with the same
|
|
189
|
+
// address and words as its top-bar Back link. The static href is what a
|
|
190
|
+
// reader without the shell (a failed script) still gets.
|
|
224
191
|
function exitLinkHtml() {
|
|
225
|
-
|
|
226
|
-
return `<a class="td-btn" href="${escapeHtml(href)}">Back to ${escapeHtml(label)}</a>`;
|
|
192
|
+
return `<a class="td-btn" data-shell-exit href="${hallBase()}/work">Back to tasks</a>`;
|
|
227
193
|
}
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
function
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
if (back) back.href = href;
|
|
234
|
-
const lbl = $('#task-back-label');
|
|
235
|
-
if (lbl) lbl.textContent = `Back to ${label}`;
|
|
194
|
+
// Called after every render that places an exit, so the button and the top
|
|
195
|
+
// bar never disagree about where Back goes.
|
|
196
|
+
function fillExits(root) {
|
|
197
|
+
const shell = window.OTBShell;
|
|
198
|
+
if (shell && typeof shell.fillExits === 'function') shell.fillExits(root);
|
|
236
199
|
}
|
|
237
200
|
|
|
238
201
|
function rankText(rr) {
|
|
@@ -852,6 +815,7 @@
|
|
|
852
815
|
|
|
853
816
|
const body = $('#task-body');
|
|
854
817
|
if (body) body.innerHTML = parts.join('');
|
|
818
|
+
fillExits(body);
|
|
855
819
|
announceShown(t);
|
|
856
820
|
|
|
857
821
|
const copyBtn = $('#copy-prompt-btn');
|
|
@@ -1043,6 +1007,7 @@
|
|
|
1043
1007
|
`;
|
|
1044
1008
|
const sub = $('#task-sub');
|
|
1045
1009
|
if (sub) sub.textContent = 'Not found';
|
|
1010
|
+
fillExits(body);
|
|
1046
1011
|
}
|
|
1047
1012
|
|
|
1048
1013
|
function renderError(id) {
|
|
@@ -1055,10 +1020,10 @@
|
|
|
1055
1020
|
`;
|
|
1056
1021
|
const sub = $('#task-sub');
|
|
1057
1022
|
if (sub) sub.textContent = 'Could not load';
|
|
1023
|
+
fillExits(body);
|
|
1058
1024
|
}
|
|
1059
1025
|
|
|
1060
1026
|
async function load() {
|
|
1061
|
-
applyExit();
|
|
1062
1027
|
const id = taskIdFromPath();
|
|
1063
1028
|
if (!id) { renderNotFound(''); return; }
|
|
1064
1029
|
try {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found", "load failed"],
|
|
9
9
|
"states": {
|
|
10
10
|
"record": { "auth": true, "actions": [["wait", 600]], "expect": { "visible": ["#task-body", "#task-grade", "#task-sessions", "#copy-prompt-btn"] } },
|
|
11
|
-
"from-goal": { "auth": true, "url": "/task/2166?from=%2Fgoals%23%2Fgoal%2F1000081", "actions": [["wait", 600]], "expect": { "visible": ["#
|
|
11
|
+
"from-goal": { "auth": true, "url": "/task/2166?from=%2Fgoals%23%2Fgoal%2F1000081", "actions": [["wait", 600]], "expect": { "visible": ["#topbar-back", "#copy-prompt-btn"], "attr": [["#topbar-back", "href", "/goals#/goal/1000081"]] } },
|
|
12
12
|
"missing": { "auth": true, "url": "/task/1003999", "actions": [["wait", 600]], "expect": { "visible": ["#task-h"] } }
|
|
13
13
|
},
|
|
14
14
|
"reducedMotion": { "auth": true, "actions": [["wait", 600]] }
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
</div>
|
|
139
139
|
</div>
|
|
140
140
|
|
|
141
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
141
|
+
<script src="/builders/shell.js?v=2026-10-02-back"></script>
|
|
142
142
|
<script src="/builders/palette.js" defer></script>
|
|
143
143
|
|
|
144
144
|
<script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
|
|
@@ -177,6 +177,10 @@ html[data-fullscreen="on"] .tw-bar { width: min(1200px, calc(100% - 144px)); }
|
|
|
177
177
|
/* With the chrome showing, the shell's fixed top bar holds the window's top,
|
|
178
178
|
so the stuck bar stops under it instead of sliding beneath it. */
|
|
179
179
|
html:not([data-fullscreen="on"]) .tw-bar { top: calc(var(--topbar-h, 56px) + 12px); }
|
|
180
|
+
/* task 1004477: the bar's exit is the shell's Back link, filled by shell.js.
|
|
181
|
+
With the chrome showing, the top bar already carries it, so the bar shows it
|
|
182
|
+
only in full screen and the page never offers two. */
|
|
183
|
+
html:not([data-fullscreen="on"]) .tw-stage .tw-back { display: none; }
|
|
180
184
|
.tw-stage .tw-back {
|
|
181
185
|
display: inline-flex;
|
|
182
186
|
align-items: center;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
data-fullscreen-switch: the shell's full-screen switch (task 1004327). The
|
|
30
30
|
artboard is the editor over the whole window, so the editor opens full
|
|
31
31
|
screen the first time (tweak-editor.js), and the switch is remembered. -->
|
|
32
|
-
<body data-page="tweak-editor" data-page-title="Tweak a page" data-fullscreen-switch>
|
|
32
|
+
<body data-page="tweak-editor" data-page-title="Tweak a page" data-fullscreen-switch data-back-fallback="/studio">
|
|
33
33
|
|
|
34
34
|
<div class="app">
|
|
35
35
|
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
@@ -49,7 +49,9 @@
|
|
|
49
49
|
<!-- A <div>, not a <header>: the shell's top bar is the page's one
|
|
50
50
|
<header> (the kit's one-header-main-h1 floor). -->
|
|
51
51
|
<div class="tw-bar" id="tw-bar">
|
|
52
|
-
|
|
52
|
+
<!-- task 1004477: the shell's exit, shown only in full screen, where the
|
|
53
|
+
top bar (and its Back link) is hidden. The shell fills it. -->
|
|
54
|
+
<a class="tw-back" data-shell-exit href="/builders/studio">Back to studio</a>
|
|
53
55
|
<span class="tw-name"><span id="tw-name">Tweak a page</span> <span class="tw-surface" id="tw-surface"></span></span>
|
|
54
56
|
<span class="tw-counter" id="tw-counter" aria-live="polite"></span>
|
|
55
57
|
<div class="tw-actions">
|
|
@@ -140,7 +142,7 @@
|
|
|
140
142
|
|
|
141
143
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
142
144
|
|
|
143
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
145
|
+
<script src="/builders/shell.js?v=2026-10-02-back"></script>
|
|
144
146
|
<script src="/builders/palette.js" defer></script>
|
|
145
147
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
146
148
|
<script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
|
|
163
163
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
164
164
|
chrome exists before the page script runs. -->
|
|
165
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
165
|
+
<script src="/builders/shell.js?v=2026-10-02-back"></script>
|
|
166
166
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
167
167
|
<script src="/builders/palette.js" defer></script>
|
|
168
168
|
<!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
|
|
172
172
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
174
|
+
<script src="/builders/shell.js?v=2026-10-02-back"></script>
|
|
175
175
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
176
176
|
<script src="/builders/palette.js" defer></script>
|
|
177
177
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Hall navigation — the Back link and the standard full-screen switch (
|
|
1
|
+
# Hall navigation — the Back link and the standard full-screen switch (tasks 1004430, 1004477)
|
|
2
2
|
|
|
3
3
|
Covers: every shelled hall page (`shell.js`, `style.css`), plus `goal-map.html` joining the chrome.
|
|
4
4
|
|
|
@@ -7,16 +7,19 @@ Covers: every shelled hall page (`shell.js`, `style.css`), plus `goal-map.html`
|
|
|
7
7
|
- `shell.js` renders `#topbar-back` ("← <label>") at the head of the top bar when the page was reached from INSIDE another hall page. The origin is `?from=` first, then a same-origin `document.referrer`; the label is that page's `NAV` label (or "task N" / "idea N" / "goal N" for a record).
|
|
8
8
|
- **A sidebar hop is not a drill-down.** A sidebar link click writes a one-shot `sessionStorage['hall-nav-hop']` naming its target; the page it lands on reads and deletes it, and shows no Back link. Without this every page reached from the sidebar would carry a Back link, which is just history.
|
|
9
9
|
- When the origin came from the referrer, the click runs `history.back()`, so a list comes back with its scroll and filters; the `href` is still the real address for a new tab.
|
|
10
|
-
-
|
|
10
|
+
- **No page draws its own Back link (task 1004477).** `task.html`, `idea.html` and the tweak editor used to, and the bar skipped its link on those pages. Now the shell decides the way back for every page, with two hooks:
|
|
11
|
+
- `<body data-back-fallback="/work">` names where Back goes when there is no origin (a deep-linked task falls back to Tasks, an idea to Your thinking, the tweak editor to Studio). It is honoured only as a path on this host; a sidebar hop still shows nothing.
|
|
12
|
+
- A link marked `[data-shell-exit]` is a second exit a page places: the task record's actions row and its two failure renders, the primer and drachmae error notes, and the tweak editor's bar. The shell fills its `href` and its words ("Back to goal 12", "Back to tasks"), and with no way back at all it goes home. A page that renders one after boot calls `OTBShell.fillExits(root)`.
|
|
13
|
+
- The origin's own `?from=` is dropped so back links never chain, and a `?from=` naming the page itself is refused.
|
|
11
14
|
- `/profile?id=N` highlights **Builders**, not your own Profile, unless `/me` says the id is yours.
|
|
12
15
|
|
|
13
16
|
## Full screen is standard
|
|
14
17
|
|
|
15
18
|
- Every shelled page gets the switch. `<body data-fullscreen-switch="off">` opts out. The bare attribute means "this page lays itself out for full screen" (sky, studio, tweak editor); a page without it is **plain**, and the shell sets `html[data-fullscreen-plain="on"]` so `style.css` pads `.app-content` clear of the corner button.
|
|
16
|
-
- In full screen the button is a labelled pill, an icon plus "Exit full screen", in `--bg-card` with a ring and a drop shadow, not the hall's near-clear glass. Under 640px it drops the words, and the tweak editor keeps the 36px round button because its bar is laid out for one.
|
|
19
|
+
- In full screen the button is a labelled pill, an icon plus "Exit full screen", in `--bg-card` with a ring and a drop shadow, not the hall's near-clear glass. Under 640px it drops the words, and the tweak editor keeps the 36px round button because its bar is laid out for one. Full screen hides the top bar and its Back link, so the tweak editor's bar carries the shell's exit and shows it only in full screen (`tweak-editor.css`); with the chrome showing, the page never offers two. On the black sky in the light theme the glass button was a dark icon on black, and because the choice is remembered per page, `/thinking` looked stuck with no bars. Entering full screen (or a page opening in it) also shows a 4.5s `#fullscreen-hint` saying how to leave.
|
|
17
20
|
|
|
18
21
|
## The project sky joined the hall
|
|
19
22
|
|
|
20
23
|
`goal-map.html` now uses thinking.html's structure: sidebar and top-bar mounts, `style.css`, `shell.js`, the sky inside `<main id="hall-main" class="sky-stage">`, and a `<div class="brand">` because the shell's bar is the page's one `<header>`. In `thinking.css` both pages share one stage rule, `body[data-page] .sky-stage`. No hall page is standalone any more (`tests/hall_audit.mjs` `STANDALONE_EXEMPT` is empty).
|
|
21
24
|
|
|
22
|
-
Tests: `tests/hall_fullscreen.mjs` (the standard switch, `plain`, Back link cases), `tests/goal_map_page.mjs`, `tests/hall_audit.mjs` §8.
|
|
25
|
+
Tests: `tests/hall_fullscreen.mjs` (the standard switch, `plain`, Back link, fallback and exit cases), `tests/hall_record_world.mjs` §10 (the record places its exit), `tests/goal_map_page.mjs`, `tests/hall_audit.mjs` §8.
|
package/modules/sessions/db.js
CHANGED
|
@@ -585,6 +585,10 @@ const SESSION_SUMMARY_COLS = `
|
|
|
585
585
|
-- here means the row predates the classifier and the backfill has not reached
|
|
586
586
|
-- it, which the ledger renders as a dash rather than claiming a verdict.
|
|
587
587
|
sr.session_type,
|
|
588
|
+
-- ADR 0051 / task 1002830: whether a full transcript is stored, so the ledger only
|
|
589
|
+
-- offers a Read door where there is something behind it (rows that predate
|
|
590
|
+
-- transcript capture have none, and a door to a 404 is worse than no door).
|
|
591
|
+
EXISTS (SELECT 1 FROM session_transcripts st WHERE st.session_id = sr.session_id) AS has_transcript,
|
|
588
592
|
sr.uploaded_at`;
|
|
589
593
|
|
|
590
594
|
// searchSessions — cross-builder search over the corpus, filtered by session
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.24",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.24",
|
|
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.21.
|
|
3
|
+
"version": "1.21.24",
|
|
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
|
@@ -8888,5 +8888,17 @@
|
|
|
8888
8888
|
"id": "1004556",
|
|
8889
8889
|
"text": "New projects are now always started on the latest released version of Cloud Bongos, even when the service that sets them up is running an older one. Until now, a lagging setup service passed its own old version to every new pr"
|
|
8890
8890
|
}
|
|
8891
|
+
],
|
|
8892
|
+
"1.21.23": [
|
|
8893
|
+
{
|
|
8894
|
+
"id": "1004477",
|
|
8895
|
+
"text": "Every page in the builders hall now goes back the same way: one Back button in the top bar. Task and idea pages used to draw their own back links, and the tweak editor had its own too. Opened from a goal, a task now shows \"Bac"
|
|
8896
|
+
}
|
|
8897
|
+
],
|
|
8898
|
+
"1.21.24": [
|
|
8899
|
+
{
|
|
8900
|
+
"id": "1002830",
|
|
8901
|
+
"text": "Sessions now has a Read button on each row that opens the full conversation for that session. Only the session's owner or an administrator can read it, and administrator reads are logged."
|
|
8902
|
+
}
|
|
8891
8903
|
]
|
|
8892
8904
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.24'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -28,10 +28,12 @@ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
|
28
28
|
// is SHARED across boots on purpose: a second boot over the same store is a reload.
|
|
29
29
|
// `optIn`: true = the bare attribute (the page lays itself out for full screen),
|
|
30
30
|
// null = no attribute (a plain page — the switch is standard since task
|
|
31
|
-
// 1004430), 'off' = the page opts out. `referrer` / `search` / `session`
|
|
32
|
-
//
|
|
31
|
+
// 1004430), 'off' = the page opts out. `referrer` / `search` / `session` drive
|
|
32
|
+
// the top bar's Back link (task 1004430); `fallback` is the page's
|
|
33
|
+
// <body data-back-fallback> and `exits` how many [data-shell-exit] links the
|
|
34
|
+
// page placed for the shell to fill (task 1004477).
|
|
33
35
|
function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = false,
|
|
34
|
-
referrer = '', search = '', session = {},
|
|
36
|
+
referrer = '', search = '', session = {}, fallback = null, exits = 0, path = null, meId = null } = {}) {
|
|
35
37
|
const els = new Map();
|
|
36
38
|
const windowListeners = {};
|
|
37
39
|
const timers = [];
|
|
@@ -65,9 +67,10 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
65
67
|
return { href, dataset: {}, getAttribute: (k) => (k === 'href' ? href : null),
|
|
66
68
|
classList: { add: (c) => cls.add(c), remove: (c) => cls.delete(c), contains: (c) => cls.has(c) } };
|
|
67
69
|
});
|
|
70
|
+
const exitLinks = Array.from({ length: exits }, () => makeEl(null));
|
|
68
71
|
const attr = optIn === true ? '' : optIn === false ? null : optIn;
|
|
69
72
|
const body = {
|
|
70
|
-
dataset: { page },
|
|
73
|
+
dataset: fallback == null ? { page } : { page, backFallback: fallback },
|
|
71
74
|
hasAttribute: (k) => k === 'data-fullscreen-switch' && attr !== null,
|
|
72
75
|
getAttribute: (k) => (k === 'data-fullscreen-switch' ? attr : null),
|
|
73
76
|
appendChild() {},
|
|
@@ -77,8 +80,9 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
77
80
|
referrer,
|
|
78
81
|
documentElement: { dataset: {}, style: {}, classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } } },
|
|
79
82
|
getElementById,
|
|
80
|
-
querySelector: (
|
|
81
|
-
querySelectorAll: (sel) => (sel === '#app-sidebar .nav-item' ? navItems
|
|
83
|
+
querySelector: () => null,
|
|
84
|
+
querySelectorAll: (sel) => (sel === '#app-sidebar .nav-item' ? navItems
|
|
85
|
+
: sel === '[data-shell-exit]' ? exitLinks : []),
|
|
82
86
|
createElement() { return makeEl(null); },
|
|
83
87
|
addEventListener() {}, dispatchEvent() {},
|
|
84
88
|
get cookie() { return ''; }, set cookie(_v) {},
|
|
@@ -123,6 +127,8 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
123
127
|
plain: () => root.dataset.fullscreenPlain === 'on',
|
|
124
128
|
active: () => navItems.filter((a) => a.classList.contains('nav-item--active')).map((a) => a.href),
|
|
125
129
|
topbar: () => topbar._html,
|
|
130
|
+
exits: () => exitLinks.map((a) => ({ href: a.getAttribute('href'), text: a.textContent, wired: !!(a._on && a._on.click) })),
|
|
131
|
+
shell: () => windowObj.OTBShell,
|
|
126
132
|
session,
|
|
127
133
|
resizes: () => resizes,
|
|
128
134
|
// Press a key the way the browser dispatches it: every window listener,
|
|
@@ -170,14 +176,69 @@ test('Back: ?from= wins over the referrer, and a record origin is named as the r
|
|
|
170
176
|
assert.match(p.topbar(), />task 42<\/span>/);
|
|
171
177
|
});
|
|
172
178
|
|
|
173
|
-
test('Back: none after a sidebar hop, from another site, from the page itself
|
|
179
|
+
test('Back: none after a sidebar hop, from another site, or from the page itself', () => {
|
|
174
180
|
const hop = boot({ page: 'goals', optIn: false, referrer: 'https://h/work', session: { 'hall-nav-hop': '/goals' } });
|
|
175
181
|
assert.doesNotMatch(hop.topbar(), /topbar-back/, 'the sidebar is not a drill-down');
|
|
176
182
|
assert.equal(hop.session['hall-nav-hop'], undefined, 'the note is one-shot');
|
|
177
183
|
assert.doesNotMatch(boot({ page: 'goals', optIn: false, referrer: 'https://elsewhere.example/roster' }).topbar(), /topbar-back/);
|
|
178
184
|
assert.doesNotMatch(boot({ page: 'goals', optIn: false, referrer: 'https://h/goals' }).topbar(), /topbar-back/);
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// task 1004477: no page draws its own Back any more, so the shell's is the
|
|
188
|
+
// record's too — the task page opened from a goal shows Back to that goal.
|
|
189
|
+
test('Back: a record page gets the bar\'s Back like every other page', () => {
|
|
190
|
+
const p = boot({ page: 'task', optIn: false, path: '/task/9', search: '?from=' + encodeURIComponent('/goals#/goal/12'), fallback: '/work' });
|
|
191
|
+
assert.match(p.topbar(), /<a class="topbar-back" id="topbar-back" href="\/goals#\/goal\/12"/);
|
|
192
|
+
assert.match(p.topbar(), />goal 12<\/span>/);
|
|
193
|
+
assert.doesNotMatch(SHELL, /rec-back|data-own-back/, 'the suppression for page-drawn links is gone');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('Back: with no origin, the page\'s data-back-fallback is the way back', () => {
|
|
197
|
+
const task = boot({ page: 'task', optIn: false, path: '/task/9', fallback: '/work' });
|
|
198
|
+
assert.match(task.topbar(), /href="\/work"/, 'a deep-linked task falls back to the board');
|
|
199
|
+
assert.match(task.topbar(), />Tasks<\/span>/);
|
|
200
|
+
const idea = boot({ page: 'idea', optIn: false, path: '/idea/4', referrer: 'https://elsewhere.example/', fallback: '/thinking' });
|
|
201
|
+
assert.match(idea.topbar(), /href="\/thinking"/, 'a cross-site arrival falls back too');
|
|
202
|
+
assert.doesNotMatch(boot({ page: 'goals', optIn: false }).topbar(), /topbar-back/, 'a page with no fallback still shows none');
|
|
203
|
+
assert.doesNotMatch(boot({ page: 'studio', optIn: false, path: '/studio', fallback: '/studio' }).topbar(), /topbar-back/,
|
|
204
|
+
'a fallback to the page itself is not a way back');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('Back: a hostile ?from= is refused, and the fallback stands in', () => {
|
|
208
|
+
const bad = ['//evil.example', '/\\evil.example', 'javascript:alert(1)', 'https://evil.example/x',
|
|
209
|
+
'/api/gds/auth/web/callback', '/lib/bongos-client.js', '/task/9'];
|
|
210
|
+
for (const from of bad) {
|
|
211
|
+
const p = boot({ page: 'task', optIn: false, path: '/task/9', search: '?from=' + encodeURIComponent(from), fallback: '/work' });
|
|
212
|
+
assert.match(p.topbar(), /id="topbar-back" href="\/work"/, `from=${JSON.stringify(from)}`);
|
|
213
|
+
}
|
|
214
|
+
for (const fallback of ['//evil.example', 'https://evil.example/', '/api/x', 'work']) {
|
|
215
|
+
assert.doesNotMatch(boot({ page: 'task', optIn: false, path: '/task/9', fallback }).topbar(), /topbar-back/,
|
|
216
|
+
`a fallback is a path on this host or nothing: ${JSON.stringify(fallback)}`);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('Back: a sidebar hop beats the fallback — the sidebar is still not a drill-down', () => {
|
|
221
|
+
const p = boot({ page: 'studio', optIn: false, path: '/studio', fallback: '/work', session: { 'hall-nav-hop': '/studio' } });
|
|
222
|
+
assert.doesNotMatch(p.topbar(), /topbar-back/);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('exits: every [data-shell-exit] link carries the bar\'s way back, in a sentence', () => {
|
|
226
|
+
const p = boot({ page: 'task', optIn: false, path: '/task/9', search: '?from=' + encodeURIComponent('/goals#/goal/12'), exits: 2 });
|
|
227
|
+
assert.deepEqual(p.exits(), [
|
|
228
|
+
{ href: '/goals#/goal/12', text: 'Back to goal 12', wired: true },
|
|
229
|
+
{ href: '/goals#/goal/12', text: 'Back to goal 12', wired: true },
|
|
230
|
+
]);
|
|
231
|
+
const board = boot({ page: 'task', optIn: false, path: '/task/9', fallback: '/work', exits: 1 });
|
|
232
|
+
assert.deepEqual(board.exits()[0], { href: '/work', text: 'Back to tasks', wired: true }, 'a nav label reads lowercase mid-sentence');
|
|
233
|
+
const lost = boot({ page: 'primer', optIn: false, path: '/primer', exits: 1 });
|
|
234
|
+
assert.deepEqual(lost.exits()[0], { href: '/', text: 'Back to home', wired: true }, 'with no way back at all, an exit goes home');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('exits: a page that renders one later asks the shell, and asking twice wires it once', () => {
|
|
238
|
+
const p = boot({ page: 'task', optIn: false, path: '/task/9', fallback: '/work', exits: 1 });
|
|
239
|
+
assert.equal(typeof p.shell().fillExits, 'function', 'the shell exposes it');
|
|
240
|
+
p.shell().fillExits();
|
|
241
|
+
assert.deepEqual(p.exits()[0], { href: '/work', text: 'Back to tasks', wired: true });
|
|
181
242
|
});
|
|
182
243
|
|
|
183
244
|
test('someone else\'s profile highlights Builders; your own hands the mark back to Profile', async () => {
|
|
@@ -191,9 +252,24 @@ test('someone else\'s profile highlights Builders; your own hands the mark back
|
|
|
191
252
|
assert.deepEqual(boot({ page: 'profile', optIn: false }).active(), ['/profile'], 'no id: your own profile');
|
|
192
253
|
});
|
|
193
254
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
255
|
+
// task 1004477: the pages that used to draw their own. Source checks, labelled
|
|
256
|
+
// as such: what each page now CARRIES is markup, and the behaviour of what it
|
|
257
|
+
// carries is the shell's, driven above.
|
|
258
|
+
test('no hall page draws its own back link; each carries the shell\'s hooks instead', () => {
|
|
259
|
+
const editor = read('tweak-editor.html');
|
|
260
|
+
assert.match(editor, /<a class="tw-back" data-shell-exit href="\/builders\/studio">/,
|
|
261
|
+
'the editor\'s bar keeps an exit, because full screen hides the top bar');
|
|
262
|
+
assert.match(editor, /<body [^>]*data-back-fallback="\/studio"/);
|
|
263
|
+
assert.match(read('tweak-editor.css'), /html:not\(\[data-fullscreen="on"\]\) \.tw-stage \.tw-back \{ display: none; \}/,
|
|
264
|
+
'and shows it only in full screen, so the page never offers two');
|
|
265
|
+
assert.match(read('idea.html'), /<body [^>]*data-back-fallback="\/thinking"/);
|
|
266
|
+
for (const f of ['primer.html', 'drachmae.html']) {
|
|
267
|
+
assert.match(read(f), /<a class="btn btn--ghost" data-shell-exit href="\/builders">/, `${f}: its error exit is the shell's`);
|
|
268
|
+
}
|
|
269
|
+
for (const f of fs.readdirSync(HALL).filter((n) => n.endsWith('.html'))) {
|
|
270
|
+
assert.doesNotMatch(read(f), /class="rec-back|data-own-back/, `${f} draws no back link of its own`);
|
|
271
|
+
}
|
|
272
|
+
assert.doesNotMatch(read('record.css'), /\.rec-back/, 'and the archetype sheet styles none');
|
|
197
273
|
});
|
|
198
274
|
|
|
199
275
|
test('Back: an origin\'s own ?from= is dropped, so one page\'s way back never chains into the next', () => {
|