@bongos/core 1.19.504 → 1.19.506
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 +28 -18
- package/docs/copy-inventory.md +23 -26
- package/docs/copy-registry.json +44 -61
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/sessions.html +10 -6
- package/modules/hall-ui/public/sessions.js +101 -40
- package/modules/hall-ui/public/task-detail.js +9 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hall_audit.mjs +38 -0
- package/tests/promote_goal_id.mjs +4 -1
- package/tests/sessions_kit_pager.mjs +312 -0
- package/tests/task_detail_ui.mjs +710 -0
|
@@ -64,11 +64,11 @@
|
|
|
64
64
|
</thead>
|
|
65
65
|
<tbody id="sessions-body"></tbody>
|
|
66
66
|
</table>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
<!-- The kit stamps the pager bar here (sessions.js, OTBKit.pagerBarHtml).
|
|
68
|
+
makeLedger is deliberately NOT used: it stamps table + pager into one
|
|
69
|
+
mount, and this page keeps them separate so the bar sits below the
|
|
70
|
+
ledger rather than inside whatever scrollport the table is in. -->
|
|
71
|
+
<div id="sessions-pager-mount"></div>
|
|
72
72
|
<p class="ledger__note" id="ledger-foot"></p>
|
|
73
73
|
</section>
|
|
74
74
|
</main>
|
|
@@ -88,7 +88,11 @@
|
|
|
88
88
|
<!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
|
|
89
89
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
90
90
|
<script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
|
|
91
|
+
<!-- The shared component kit — the pager contract this page now uses instead of
|
|
92
|
+
its own (task 1003279-sibling follow-up of 1003281). Must load BEFORE
|
|
93
|
+
sessions.js, which reads window.OTBKit at boot. -->
|
|
94
|
+
<script src="/builders/hall-kit.js"></script>
|
|
91
95
|
<script src="/builders/brand-apply.js?v=2026-06-19-r56"></script>
|
|
92
|
-
<script src="/builders/sessions.js?v=2026-
|
|
96
|
+
<script src="/builders/sessions.js?v=2026-08-28-kitpager"></script>
|
|
93
97
|
</body>
|
|
94
98
|
</html>
|
|
@@ -121,91 +121,146 @@
|
|
|
121
121
|
$('#sessions-scroll').hidden = true;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
// ---- paging
|
|
124
|
+
// ---- paging --------------------------------------------------------------
|
|
125
|
+
//
|
|
126
|
+
// This page used to hand-roll its own server-offset pager, and hall-ui/CLAUDE.md
|
|
127
|
+
// carried it as the ONE sanctioned exception to "no bespoke pager" (audited task
|
|
128
|
+
// 2179) — because the kit could only slice an array the browser already held, and
|
|
129
|
+
// this corpus cannot be held. Task 1003281 added the server-offset mode to the kit
|
|
130
|
+
// and left the migration as its own task; this is it. The exception is gone, so
|
|
131
|
+
// there is one pager contract in the hall again rather than one-plus-a-footnote.
|
|
132
|
+
//
|
|
133
|
+
// What the reader gains: a per-page control that remembers their choice, and a
|
|
134
|
+
// clamp — if the corpus shrinks under an open page, renderServerPage() says so and
|
|
135
|
+
// we re-ask, instead of showing an empty page under a range that claims rows.
|
|
136
|
+
//
|
|
137
|
+
// The one thing NOT taken from the kit is makeLedger: it stamps table+pager into a
|
|
138
|
+
// single mount, and the CARD is this page's scroll container (hall-ui/CLAUDE.md, the
|
|
139
|
+
// sideways-scroll rule), so a pager stamped into the table's mount would slide with
|
|
140
|
+
// the columns. Keeping it a separate sibling below the ledger is what holds it still.
|
|
141
|
+
// (This page used to carry a .ledger-table-wrap scrollport; task 1003470 removed it
|
|
142
|
+
// because the wrapper broke the shell's sticky th. The separate mount outlived it.)
|
|
143
|
+
// Table markup stays in sessions.html; the PAGER is the kit's.
|
|
125
144
|
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
// Guarded the way every other kit consumer guards it (profile.js): the kit is a
|
|
146
|
+
// separate <script>, so a cache miss or a load-order slip must degrade to a named
|
|
147
|
+
// failure rather than an uncaught throw at parse time that also kills load().
|
|
148
|
+
const kit = window.OTBKit;
|
|
149
|
+
const PAGER_PREFIX = 'sessions';
|
|
150
|
+
// 50 was this page's fixed page size before the per-page control existed; keeping it
|
|
151
|
+
// as the default means a returning reader's first page looks exactly as it did.
|
|
152
|
+
const pager = kit ? kit.makePager(PAGER_PREFIX, 'otb.sessions.perPage', { defaultPerPage: 50 }) : null;
|
|
153
|
+
// `page`/`perPage` here are the last values that actually PAINTED. The kit moves the
|
|
154
|
+
// pager first and calls back second, so a failed fetch has to put it back to these —
|
|
155
|
+
// otherwise the bar goes on claiming a page whose rows never arrived. Keeping both
|
|
156
|
+
// (not just the page) means a failed per-page change rolls back cleanly too.
|
|
157
|
+
const state = { total: 0, shown: 0, page: 1, perPage: pager?.perPage ?? 0 };
|
|
128
158
|
let loading = false;
|
|
159
|
+
// Two clicks can be in flight at once; only the newest may paint (the profile
|
|
160
|
+
// activity-log idiom, task 1003281).
|
|
161
|
+
let inFlight = 0;
|
|
129
162
|
|
|
163
|
+
// Paint the kit's bar, then apply the one thing the kit deliberately does not model:
|
|
164
|
+
// a fetch is in flight, so nothing may be pressed again until it lands. Only the
|
|
165
|
+
// DISABLING is ours — re-enabling the arrows is left to the kit, which knows whether
|
|
166
|
+
// there is a page on either side; re-enabling them here would light up "Prev" on
|
|
167
|
+
// page 1. Returns the kit's clamped flag: true = the page outran a shrunken corpus.
|
|
130
168
|
function renderPager() {
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
169
|
+
const clamped = kit.renderServerPage(pager, state.total, state.shown);
|
|
170
|
+
const perPage = $(`#${PAGER_PREFIX}-per-page`);
|
|
171
|
+
if (perPage) perPage.disabled = loading;
|
|
172
|
+
if (loading) {
|
|
173
|
+
for (const id of ['prev', 'next']) {
|
|
174
|
+
const el = $(`#${PAGER_PREFIX}-${id}`);
|
|
175
|
+
if (el) el.disabled = true;
|
|
176
|
+
}
|
|
137
177
|
}
|
|
138
|
-
|
|
139
|
-
const page = Math.floor(state.offset / PAGE_SIZE) + 1;
|
|
140
|
-
const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
141
|
-
const from = state.offset + 1;
|
|
142
|
-
const to = Math.min(state.offset + PAGE_SIZE, total);
|
|
143
|
-
$('#pager-pos').textContent = `Page ${fmtInt(page)} of ${fmtInt(pages)} · ${fmtInt(from)}–${fmtInt(to)} of ${fmtInt(total)}`;
|
|
144
|
-
$('#pager-prev').disabled = loading || state.offset <= 0;
|
|
145
|
-
$('#pager-next').disabled = loading || state.offset + PAGE_SIZE >= total;
|
|
178
|
+
return clamped;
|
|
146
179
|
}
|
|
147
180
|
|
|
181
|
+
// Paint one page. Returns true when the pager clamped — the corpus shrank under an
|
|
182
|
+
// open page, so the caller must re-ask rather than leave a range that lies.
|
|
148
183
|
function showData(data) {
|
|
149
184
|
const sessions = Array.isArray(data.sessions) ? data.sessions : [];
|
|
150
185
|
const totals = data.totals || { total: data.total || sessions.length };
|
|
151
186
|
state.total = num(totals.total);
|
|
187
|
+
state.shown = sessions.length;
|
|
152
188
|
$('#gate-scroll').hidden = true;
|
|
153
189
|
$('#stats-scroll').hidden = false;
|
|
154
190
|
$('#sessions-scroll').hidden = false;
|
|
155
191
|
renderStats(totals);
|
|
156
192
|
renderTable(sessions);
|
|
157
|
-
renderPager();
|
|
193
|
+
const clamped = renderPager();
|
|
158
194
|
$('#ledger-sub').textContent =
|
|
159
195
|
`${fmtInt(totals.total)} session${num(totals.total) === 1 ? '' : 's'} across ${fmtInt(totals.builders)} builder${num(totals.builders) === 1 ? '' : 's'}.`;
|
|
160
196
|
$('#ledger-foot').textContent =
|
|
161
197
|
'Most recent first. Each load of this page is itself recorded in the audit log.';
|
|
198
|
+
return clamped;
|
|
162
199
|
}
|
|
163
200
|
|
|
164
201
|
// ---- load ----------------------------------------------------------------
|
|
165
202
|
|
|
166
|
-
// Fetch
|
|
167
|
-
|
|
203
|
+
// Fetch the page the pager currently points at. Returns true on success.
|
|
204
|
+
//
|
|
205
|
+
// `pager.page` has ALREADY moved by the time this runs — wirePager advances it and
|
|
206
|
+
// then calls back — so a failure restores `state.page`/`state.perPage`, the last
|
|
207
|
+
// values that painted. That roll-back is what the old hand-roll did with
|
|
208
|
+
// `state.offset`, and it is the one piece of it worth carrying over.
|
|
209
|
+
async function fetchPage({ reclamps = 0 } = {}) {
|
|
210
|
+
const seq = ++inFlight;
|
|
168
211
|
loading = true;
|
|
169
212
|
renderPager();
|
|
170
213
|
try {
|
|
171
214
|
const res = await api.request(
|
|
172
215
|
'GET',
|
|
173
|
-
`${API}/sessions/search?order=recent&limit=${
|
|
216
|
+
`${API}/sessions/search?order=recent&limit=${pager.perPage}&offset=${kit.pageOffset(pager)}`
|
|
174
217
|
);
|
|
218
|
+
// A newer click already went out; painting this answer would show a page nobody
|
|
219
|
+
// is on any more.
|
|
220
|
+
if (seq !== inFlight) return true;
|
|
175
221
|
if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return false; }
|
|
176
222
|
if (res.status === 403) {
|
|
177
223
|
showSealed('You do not have access to the session log.');
|
|
178
224
|
return false;
|
|
179
225
|
}
|
|
180
226
|
if (!res.ok) throw new Error(`search → ${res.status}`);
|
|
181
|
-
const
|
|
182
|
-
|
|
227
|
+
const clamped = showData(res.data);
|
|
228
|
+
// The corpus shrank under this page (sessions pruned between loads):
|
|
229
|
+
// renderServerPage pulled the page back into range, so go get the rows that page
|
|
230
|
+
// actually holds rather than leaving a range that disagrees with the table.
|
|
231
|
+
// One re-ask, never a loop: a corpus shrinking on every round-trip would
|
|
232
|
+
// otherwise spin here, and a stale range for one paint is the smaller harm.
|
|
233
|
+
if (clamped && reclamps < 1) { loading = false; return fetchPage({ reclamps: reclamps + 1 }); }
|
|
234
|
+
state.page = pager.page;
|
|
235
|
+
state.perPage = pager.perPage;
|
|
183
236
|
return true;
|
|
184
237
|
} catch (err) {
|
|
238
|
+
if (seq !== inFlight) return true;
|
|
185
239
|
console.error('[sessions] failed to load corpus', err);
|
|
240
|
+
pager.page = state.page;
|
|
241
|
+
pager.perPage = state.perPage;
|
|
186
242
|
$('#ledger-sub').textContent = 'The session log could not be loaded. Try again.';
|
|
187
243
|
toast('Could not load sessions.');
|
|
188
244
|
return false;
|
|
189
245
|
} finally {
|
|
190
|
-
|
|
191
|
-
|
|
246
|
+
if (seq === inFlight) {
|
|
247
|
+
loading = false;
|
|
248
|
+
renderPager();
|
|
249
|
+
}
|
|
192
250
|
}
|
|
193
251
|
}
|
|
194
252
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
function wirePager() {
|
|
207
|
-
$('#pager-prev').addEventListener('click', () => goTo(state.offset - PAGE_SIZE));
|
|
208
|
-
$('#pager-next').addEventListener('click', () => goTo(state.offset + PAGE_SIZE));
|
|
253
|
+
function mountPager() {
|
|
254
|
+
const mount = $('#sessions-pager-mount');
|
|
255
|
+
if (!mount) return;
|
|
256
|
+
mount.innerHTML = kit.pagerBarHtml(PAGER_PREFIX, { label: 'sessions' });
|
|
257
|
+
// The kit has already updated `pager` by the time this fires; go get the rows that
|
|
258
|
+
// match it. Scrolling up only on success keeps a failed page from yanking the
|
|
259
|
+
// reader away from the table they were still looking at.
|
|
260
|
+
kit.wirePager(pager, async () => {
|
|
261
|
+
const ok = await fetchPage();
|
|
262
|
+
if (ok) window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
263
|
+
});
|
|
209
264
|
}
|
|
210
265
|
|
|
211
266
|
async function load() {
|
|
@@ -234,8 +289,14 @@
|
|
|
234
289
|
return;
|
|
235
290
|
}
|
|
236
291
|
|
|
292
|
+
if (!kit) {
|
|
293
|
+
console.error('[sessions] OTBKit did not load — the pager cannot be built');
|
|
294
|
+
$('#ledger-sub').textContent = 'The session log could not be loaded. Try again.';
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
237
298
|
// 2. Fetch the first page (Archon-gated + audited server-side).
|
|
238
|
-
|
|
299
|
+
mountPager();
|
|
239
300
|
await fetchPage();
|
|
240
301
|
}
|
|
241
302
|
|
|
@@ -271,11 +271,16 @@
|
|
|
271
271
|
// triage path: the STORED hint (idea.suggested_goal_id, written by
|
|
272
272
|
// inbox.captureIdea precisely so triage inherits the guess) wins; the R07
|
|
273
273
|
// suggester only fills in when there is no hint. Returns option HTML.
|
|
274
|
-
|
|
274
|
+
// `allowHint` scopes the stored hint to the version it was filed against. A
|
|
275
|
+
// goal belongs to exactly ONE version and POST /tasks 400s `bad_goal_id` on a
|
|
276
|
+
// goal from another one, so carrying the hint across a version change offered
|
|
277
|
+
// — pre-selected — the single option guaranteed to fail (found by
|
|
278
|
+
// tests/task_detail_ui.mjs, task 1003516).
|
|
279
|
+
async function goalOptionsFor(versionId, allowHint) {
|
|
275
280
|
// Number() so a junk stored value cannot reach the DOM as value="NaN" and
|
|
276
281
|
// then be POSTed as a goal id.
|
|
277
282
|
var raw = Number(idea.suggested_goal_id);
|
|
278
|
-
var hint = Number.isFinite(raw) && raw > 0 ? raw : null;
|
|
283
|
+
var hint = allowHint && Number.isFinite(raw) && raw > 0 ? raw : null;
|
|
279
284
|
var suggestions = [];
|
|
280
285
|
// The suggester is a convenience, so an outage degrades to "no suggestions"
|
|
281
286
|
// and must not stop a triage verdict — the hint and the opt-out below are
|
|
@@ -348,7 +353,7 @@
|
|
|
348
353
|
// Fetched for the PREFERRED version; the change listener below re-fetches,
|
|
349
354
|
// because goals belong to a version and showing another version's goals
|
|
350
355
|
// would offer a home the server would reject.
|
|
351
|
-
var goalOptions = await goalOptionsFor(preferred);
|
|
356
|
+
var goalOptions = await goalOptionsFor(preferred, true);
|
|
352
357
|
var priOptions = [1, 2, 3, 4, 5].map(function (p) {
|
|
353
358
|
return '<option value="' + p + '"' + (p === pri ? ' selected' : '') + '>P' + p + '</option>';
|
|
354
359
|
}).join('');
|
|
@@ -382,7 +387,7 @@
|
|
|
382
387
|
var mine = ++goalFetch;
|
|
383
388
|
gsel.disabled = true;
|
|
384
389
|
gsel.innerHTML = '<option value="">loading goals…</option>';
|
|
385
|
-
var html = await goalOptionsFor(vsel.value);
|
|
390
|
+
var html = await goalOptionsFor(vsel.value, vsel.value === preferred);
|
|
386
391
|
if (mine !== goalFetch) return;
|
|
387
392
|
gsel.innerHTML = html;
|
|
388
393
|
gsel.disabled = false;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.506",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.506",
|
|
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.19.
|
|
3
|
+
"version": "1.19.506",
|
|
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/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.506'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/hall_audit.mjs
CHANGED
|
@@ -198,6 +198,44 @@ test('the sidebar scrolls its nav list, not the whole column', () => {
|
|
|
198
198
|
|
|
199
199
|
// ---- 6. every hall list is paged through the kit (task 1002446) -----------
|
|
200
200
|
|
|
201
|
+
test('the kit is the ONLY thing in the hall that emits pager markup', () => {
|
|
202
|
+
// hall-ui/CLAUDE.md's "no bespoke pager" rule had one sanctioned exception for two
|
|
203
|
+
// releases — sessions.js, which paged server-side by offset, a thing the kit could
|
|
204
|
+
// not express (audited task 2179). Task 1003281 added `makeLedger({ serverPager })`
|
|
205
|
+
// and left the migration as its own task; it has now landed, so the hall is back to
|
|
206
|
+
// ONE pager contract with no footnote.
|
|
207
|
+
//
|
|
208
|
+
// A rule that only lives in prose is a rule that comes back as an eleventh pager.
|
|
209
|
+
// This is the machine-checkable half: the kit STAMPS every bar at runtime, so any
|
|
210
|
+
// .pager markup written by hand — in a page's HTML or in another script's template —
|
|
211
|
+
// is by construction a second implementation. Adding an exception here is where the
|
|
212
|
+
// conversation happens, exactly like the budgets above.
|
|
213
|
+
const OWNER = 'hall-kit.js';
|
|
214
|
+
const offenders = [];
|
|
215
|
+
for (const f of fs.readdirSync(HALL)) {
|
|
216
|
+
if (!/\.(html|js)$/.test(f) || f === OWNER) continue;
|
|
217
|
+
const src = read(f);
|
|
218
|
+
// `class="pager…"` in a template string or in static markup. The kit's own callers
|
|
219
|
+
// pass a PREFIX and get the markup back; they never write the class themselves.
|
|
220
|
+
if (/class="pager[\s"_-]/.test(src)) offenders.push(f);
|
|
221
|
+
}
|
|
222
|
+
assert.deepEqual(offenders, [],
|
|
223
|
+
`${offenders.join(', ')} writes pager markup by hand — call OTBKit.pagerBarHtml(prefix) `
|
|
224
|
+
+ '(or makeLedger, which stamps it for you) instead of a second copy.');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('the session log pages on the kit — the last hand-roll is gone', () => {
|
|
228
|
+
// The specific exception this rule carried, pinned by name so its removal cannot be
|
|
229
|
+
// quietly undone. Behaviour is exercised for real in tests/sessions_kit_pager.mjs;
|
|
230
|
+
// this is the structural half.
|
|
231
|
+
const js = read('sessions.js');
|
|
232
|
+
for (const fn of ['makePager', 'pagerBarHtml', 'wirePager', 'pageOffset', 'renderServerPage']) {
|
|
233
|
+
assert.match(js, new RegExp(`kit\\.${fn}\\(`), `sessions.js must page through the kit's ${fn}`);
|
|
234
|
+
}
|
|
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('sessions.html'), /hall-kit\.js/, 'and the page has to load the kit');
|
|
237
|
+
});
|
|
238
|
+
|
|
201
239
|
test('the peer-recognition feed is paged through OTBKit, not by hand', () => {
|
|
202
240
|
assert.match(read('hall-widgets.js'), /pagerBarHtml\('acclaim'/, 'the acclaim card stamps the kit pager bar');
|
|
203
241
|
const js = read('builders.js');
|
|
@@ -294,7 +294,10 @@ await test('DEGRADES: the hall re-fetches goals when the version changes, newest
|
|
|
294
294
|
// Goals belong to a version; keeping stale options would offer a home the
|
|
295
295
|
// server would reject.
|
|
296
296
|
assert.match(HALL, /vsel\.addEventListener\('change'/);
|
|
297
|
-
|
|
297
|
+
// The second argument (task 1003516) scopes the filed goal hint to the version
|
|
298
|
+
// it was filed against — matched loosely so this pins the re-fetch, not the
|
|
299
|
+
// argument list.
|
|
300
|
+
assert.match(HALL, /await goalOptionsFor\(vsel\.value/);
|
|
298
301
|
});
|
|
299
302
|
|
|
300
303
|
await test('the CLI uses the GENERATED typed client method, not the raw escape hatch', () => {
|