@bongos/core 1.19.457 → 1.19.459
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 +126 -46
- package/.claude/skills/design/SKILL.md +10 -0
- package/docs/adr/0219-a-look-is-a-branding-pack-the-style-library.md +30 -0
- package/docs/adr/README.md +1 -0
- package/docs/branding-contract.md +1 -1
- package/docs/copy-inventory.md +160 -177
- package/docs/copy-registry.json +445 -606
- package/docs/file-map.md +2 -2
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/ui-look-before-you-ship.md +2 -0
- package/modules/hall-ui/public/gate.css +20 -88
- package/modules/hall-ui/public/gate.html +24 -12
- package/modules/hall-ui/public/gate.js +77 -60
- package/modules/hall-ui/public/gate.states.json +19 -1
- package/modules/hall-ui/public/government.css +96 -154
- package/modules/hall-ui/public/government.html +80 -52
- package/modules/hall-ui/public/government.js +227 -136
- package/modules/hall-ui/public/government.states.json +71 -1
- package/modules/hall-ui/public/harbor.css +9 -120
- package/modules/hall-ui/public/harbor.html +23 -11
- package/modules/hall-ui/public/harbor.js +109 -83
- package/modules/hall-ui/public/harbor.states.json +19 -1
- package/modules/hall-ui/public/oversight.css +296 -0
- package/modules/hall-ui/public/watch.css +56 -170
- package/modules/hall-ui/public/watch.html +48 -37
- package/modules/hall-ui/public/watch.js +363 -298
- package/modules/hall-ui/public/watch.states.json +48 -1
- package/modules/ui-design/kit/README.md +7 -5
- package/modules/ui-design/kit/check-mock.js +12 -5
- package/modules/ui-design/kit/contrast.js +13 -6
- package/modules/ui-design/kit/lib.js +39 -2
- package/modules/ui-design/kit/probe.js +4 -1
- package/modules/ui-design/kit/render.js +7 -4
- package/modules/ui-design/kit/serve.js +9 -2
- package/modules/ui-design/styles/README.md +54 -2
- package/modules/ui-design/styles/chrome-world/DESIGN.md +56 -0
- package/modules/ui-design/styles/chrome-world/mock.html +275 -0
- package/modules/ui-design/styles/chrome-world/mock.states.json +11 -0
- package/modules/ui-design/styles/chrome-world/pack.json +33 -0
- package/modules/ui-design/styles/expedition/DESIGN.md +141 -0
- package/modules/ui-design/styles/expedition/mock.html +285 -0
- package/modules/ui-design/styles/expedition/mock.states.json +11 -0
- package/modules/ui-design/styles/expedition/pack.json +33 -0
- package/modules/ui-design/styles/grove/DESIGN.md +148 -0
- package/modules/ui-design/styles/grove/mock.html +294 -0
- package/modules/ui-design/styles/grove/mock.states.json +11 -0
- package/modules/ui-design/styles/grove/pack.json +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/cosmos_token_contrast.mjs +9 -2
- package/tests/hall_oversight_world.mjs +118 -0
- package/tests/hall_tokens.mjs +15 -6
- package/tests/ui_design_styles.mjs +218 -0
- package/tests/world_tokens.mjs +9 -2
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
// "unavailable" message rather than blanking the whole page. Each section
|
|
20
20
|
// loads independently in its own try/catch so one failing endpoint cannot
|
|
21
21
|
// break the others.
|
|
22
|
+
//
|
|
23
|
+
// TASK 1003469 put the page in the chrome world's composition: every list is
|
|
24
|
+
// the kit's .ledger through makeLedger (eight of nine were a page-local table
|
|
25
|
+
// that duplicated it with a hand-rolled th voice), every figure row is the oversight
|
|
26
|
+
// family's .ov-figs, every sub-section is an .ov-group head, the access requests
|
|
27
|
+
// are oversight rows, and the five most recent sessions the page always fetched
|
|
28
|
+
// and threw away are now rendered under the tally.
|
|
22
29
|
|
|
23
30
|
(() => {
|
|
24
31
|
'use strict';
|
|
@@ -35,7 +42,7 @@
|
|
|
35
42
|
// empty; the 3200ms non-clearing toast). fmtDay / fmtDuration / fmtPct stay local.
|
|
36
43
|
const { escapeHtml, $, num, fmtInt, fmtCompact } = window.OTB;
|
|
37
44
|
// Shared component kit (hall-kit.js, task 2166): tabs, ledger, pager, empty
|
|
38
|
-
// states — adopted here by task 2173.
|
|
45
|
+
// states — adopted here by task 2173, and by every list since task 1003469.
|
|
39
46
|
const { makeTabs, makeLedger, makePager, emptyStateHtml } = window.OTBKit;
|
|
40
47
|
const fmtDate = (iso) => window.OTB.fmtDate(iso, { time: true });
|
|
41
48
|
const rankLabel = (rank) => window.OTB.rankLabel(rank, { empty: '' });
|
|
@@ -58,6 +65,17 @@
|
|
|
58
65
|
return `${Math.round(n * 100)}%`;
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
// seconds → "1h 31m" / "12m" / "45s". "—" for empty/bad input.
|
|
69
|
+
function fmtDuration(seconds) {
|
|
70
|
+
const s = Number(seconds);
|
|
71
|
+
if (!Number.isFinite(s) || s < 0) return '—';
|
|
72
|
+
const h = Math.floor(s / 3600);
|
|
73
|
+
const m = Math.floor((s % 3600) / 60);
|
|
74
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
75
|
+
if (m > 0) return `${m}m`;
|
|
76
|
+
return `${Math.round(s)}s`;
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
// ---- fetch helper ---------------------------------------------------------
|
|
62
80
|
|
|
63
81
|
// Fetch JSON. Throws { kind: '401'|'403' } on those statuses so each section
|
|
@@ -157,28 +175,55 @@
|
|
|
157
175
|
});
|
|
158
176
|
}
|
|
159
177
|
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
// delegate — the same dance as ONE listener on the section, for a list that
|
|
179
|
+
// pages: a row the kit re-renders on a page flip has no listener of its own,
|
|
180
|
+
// so the section listens once (per selector, guarded) and resolves the button
|
|
181
|
+
// from the click. `event` is 'click' unless a control says otherwise.
|
|
182
|
+
function delegate(root, selector, handler, errPrefix, event = 'click') {
|
|
183
|
+
const key = `wired:${event}:${selector}`;
|
|
184
|
+
if (root[key]) return;
|
|
185
|
+
root[key] = true;
|
|
186
|
+
root.addEventListener(event, async (e) => {
|
|
187
|
+
const el = e.target.closest(selector);
|
|
188
|
+
if (!el || !root.contains(el)) return;
|
|
189
|
+
el.disabled = true;
|
|
190
|
+
try {
|
|
191
|
+
await handler(el);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err && err.kind === '401') return;
|
|
194
|
+
el.disabled = false;
|
|
195
|
+
toast(`${errPrefix}: ${err.message || err}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ledger — the kit's makeLedger over a fresh mount appended to `target`, so a
|
|
201
|
+
// section can compose heads and foot lines around the table and still page
|
|
202
|
+
// through the kit. Every list on this page is a kit ledger since task 1003469;
|
|
203
|
+
// the ones that can grow carry a pager, whose bar hides itself when the list
|
|
204
|
+
// fits one page.
|
|
205
|
+
function ledger(target, { columns, rowHtml, pager = null, emptyMsg = 'Nothing here yet.' }) {
|
|
206
|
+
const mount = document.createElement('div');
|
|
207
|
+
target.appendChild(mount);
|
|
208
|
+
return makeLedger({ mount, columns, rowHtml, pager, emptyMsg });
|
|
172
209
|
}
|
|
173
210
|
|
|
174
|
-
//
|
|
175
|
-
|
|
211
|
+
// A group head inside a panel (oversight.css .ov-group): a run of rows under a
|
|
212
|
+
// name in the label voice. Returns the section; append the ledger to it.
|
|
213
|
+
function group(target, name, n) {
|
|
214
|
+
const sec = document.createElement('section');
|
|
215
|
+
sec.className = 'ov-group';
|
|
216
|
+
sec.innerHTML = `<div class="ov-group__head"><span class="ov-group__name">${escapeHtml(name)}</span>${n === undefined ? '' : `<span class="ov-group__n">${fmtInt(n)}</span>`}</div>`;
|
|
217
|
+
target.appendChild(sec);
|
|
218
|
+
return sec;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const foot = (html) => `<p class="ov-foot">${html}</p>`;
|
|
222
|
+
|
|
223
|
+
// The figure band from [{ num, label }] — the oversight family's .ov-figs.
|
|
224
|
+
function figs(cards) {
|
|
176
225
|
return cards
|
|
177
|
-
.map(
|
|
178
|
-
(c) =>
|
|
179
|
-
`<div class="watch-stat"><div class="watch-stat__num">${escapeHtml(c.num)}</div>` +
|
|
180
|
-
`<div class="watch-stat__label">${escapeHtml(c.label)}</div></div>`
|
|
181
|
-
)
|
|
226
|
+
.map((c) => `<div class="ov-fig"><span class="ov-fig__l">${escapeHtml(c.label)}</span><span class="ov-fig__v">${escapeHtml(c.num)}</span></div>`)
|
|
182
227
|
.join('');
|
|
183
228
|
}
|
|
184
229
|
|
|
@@ -192,6 +237,9 @@
|
|
|
192
237
|
return `<span class="badge ${cls}">${escapeHtml(sev || '—')}</span>`;
|
|
193
238
|
}
|
|
194
239
|
|
|
240
|
+
const ghost = (attrs, label, danger = false) =>
|
|
241
|
+
`<button type="button" class="btn-ghost${danger ? ' btn-ghost--danger' : ''}" ${attrs}>${label}</button>`;
|
|
242
|
+
|
|
195
243
|
// =========================================================================
|
|
196
244
|
// 1. GATE — access requests (pending)
|
|
197
245
|
// =========================================================================
|
|
@@ -210,13 +258,13 @@
|
|
|
210
258
|
? 'This project is open — anyone who signs in is admitted, so an invite is rarely needed.'
|
|
211
259
|
: 'Admit someone before they ask — once invited, their sign-in gets in.';
|
|
212
260
|
return (
|
|
213
|
-
'<form class="
|
|
214
|
-
'<
|
|
215
|
-
'<
|
|
261
|
+
'<form class="ov-form" id="gate-invite" novalidate>' +
|
|
262
|
+
'<div class="ov-field">' +
|
|
263
|
+
'<label class="ov-field__l" for="gate-invite-login">Invite by GitHub username</label>' +
|
|
216
264
|
'<input type="text" id="gate-invite-login" name="github_login" autocomplete="off" placeholder="e.g. octocat" maxlength="40">' +
|
|
217
|
-
'<button type="submit" class="watch-btn">Invite</button>' +
|
|
218
265
|
'</div>' +
|
|
219
|
-
|
|
266
|
+
'<button type="submit" class="btn-accent">Invite</button>' +
|
|
267
|
+
`<p class="ov-form__why">${escapeHtml(why)}</p>` +
|
|
220
268
|
'</form>'
|
|
221
269
|
);
|
|
222
270
|
}
|
|
@@ -243,6 +291,8 @@
|
|
|
243
291
|
});
|
|
244
292
|
}
|
|
245
293
|
|
|
294
|
+
// One request, one oversight row: who, their note as the sub line, when they
|
|
295
|
+
// asked as the fact, and the two decisions on the right.
|
|
246
296
|
function renderGate(data, target) {
|
|
247
297
|
const requests = Array.isArray(data.requests) ? data.requests : [];
|
|
248
298
|
setAttention('att-access', requests.length); // tile count = table content
|
|
@@ -251,29 +301,27 @@
|
|
|
251
301
|
wireInviteForm(target);
|
|
252
302
|
return;
|
|
253
303
|
}
|
|
254
|
-
target.innerHTML = inviteFormHtml() + requests
|
|
304
|
+
target.innerHTML = inviteFormHtml() + '<ul class="ov-rows">' + requests
|
|
255
305
|
.map((r) => {
|
|
256
306
|
const who = r.display_name
|
|
257
|
-
? `${escapeHtml(r.display_name)}
|
|
258
|
-
: `<span class="
|
|
259
|
-
const note = r.note
|
|
260
|
-
? `<div class="watch-req__note">“${escapeHtml(r.note)}”</div>`
|
|
261
|
-
: '';
|
|
307
|
+
? `${escapeHtml(r.display_name)}<span class="ov-mono">@${escapeHtml(r.github_login)}</span>`
|
|
308
|
+
: `<span class="ov-mono">@${escapeHtml(r.github_login)}</span>`;
|
|
309
|
+
const note = r.note ? `<p class="ov-row__sub">“${escapeHtml(r.note)}”</p>` : '';
|
|
262
310
|
return (
|
|
263
|
-
`<
|
|
264
|
-
'<div>' +
|
|
265
|
-
`<
|
|
266
|
-
`<div class="watch-req__meta">Asked ${escapeHtml(fmtDate(r.created_at))}</div>` +
|
|
311
|
+
`<li class="ov-row" data-req-id="${escapeHtml(r.id)}">` +
|
|
312
|
+
'<div class="ov-row__main">' +
|
|
313
|
+
`<span class="ov-row__title">${who}</span>` +
|
|
267
314
|
note +
|
|
315
|
+
`<div class="ov-row__facts"><span class="ov-row__when">Asked ${escapeHtml(fmtDate(r.created_at))}</span></div>` +
|
|
268
316
|
'</div>' +
|
|
269
|
-
'<div class="
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
'</div>' +
|
|
273
|
-
'</
|
|
317
|
+
'<div class="ov-row__right"><div class="ov-row__actions">' +
|
|
318
|
+
ghost(`data-gate-action="invited" data-id="${escapeHtml(r.id)}"`, 'Invite') +
|
|
319
|
+
ghost(`data-gate-action="dismissed" data-id="${escapeHtml(r.id)}"`, 'Dismiss', true) +
|
|
320
|
+
'</div></div>' +
|
|
321
|
+
'</li>'
|
|
274
322
|
);
|
|
275
323
|
})
|
|
276
|
-
.join('');
|
|
324
|
+
.join('') + '</ul>';
|
|
277
325
|
|
|
278
326
|
wireActions(target, '[data-gate-action]', async (btn) => {
|
|
279
327
|
const id = btn.getAttribute('data-id');
|
|
@@ -305,38 +353,33 @@
|
|
|
305
353
|
const reports = Array.isArray(data.reports) ? data.reports : [];
|
|
306
354
|
const statuses = Array.isArray(data.statuses) ? data.statuses : [];
|
|
307
355
|
setAttention('att-security', reports.filter(OPEN_REPORT).length);
|
|
356
|
+
target.innerHTML = '';
|
|
308
357
|
if (!reports.length) {
|
|
309
358
|
target.innerHTML = emptyStateHtml('No vulnerability reports filed.');
|
|
310
359
|
return;
|
|
311
360
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
target
|
|
334
|
-
renderTable(['Severity', 'Title', 'Target', 'Status', 'Action'], rows) +
|
|
335
|
-
(statuses.length
|
|
336
|
-
? `<p class="watch-note">Lifecycle: ${escapeHtml(statuses.join(' → '))}</p>`
|
|
337
|
-
: '');
|
|
338
|
-
|
|
339
|
-
wireActions(target, '[data-rep-action]', async (btn) => {
|
|
361
|
+
ledger(target, {
|
|
362
|
+
columns: ['Severity', 'Title', 'Target', 'Status', 'Action'],
|
|
363
|
+
pager: makePager('watch-reports', 'otb.watch.reportsPerPage'),
|
|
364
|
+
// The status-update actions are POSTs to dedicated verbs:
|
|
365
|
+
// /security/reports/:id/confirm | /dispute | /fix
|
|
366
|
+
rowHtml: (r) => (
|
|
367
|
+
'<tr>' +
|
|
368
|
+
`<td>${sevBadge(r.severity)}</td>` +
|
|
369
|
+
`<td>${escapeHtml(r.title)}</td>` +
|
|
370
|
+
`<td><span class="ov-mono">${escapeHtml(r.target)}</span></td>` +
|
|
371
|
+
`<td><span class="badge badge--status">${escapeHtml(r.status)}</span></td>` +
|
|
372
|
+
'<td><div class="watch-actions">' +
|
|
373
|
+
ghost(`data-rep-action="confirm" data-id="${escapeHtml(r.id)}"`, 'Confirm') +
|
|
374
|
+
ghost(`data-rep-action="dispute" data-id="${escapeHtml(r.id)}"`, 'Dispute') +
|
|
375
|
+
ghost(`data-rep-action="fix" data-id="${escapeHtml(r.id)}"`, 'Fixed') +
|
|
376
|
+
'</div></td>' +
|
|
377
|
+
'</tr>'
|
|
378
|
+
),
|
|
379
|
+
}).render(reports);
|
|
380
|
+
if (statuses.length) target.insertAdjacentHTML('beforeend', foot(`Lifecycle: ${escapeHtml(statuses.join(' → '))}`));
|
|
381
|
+
|
|
382
|
+
delegate(target, '[data-rep-action]', async (btn) => {
|
|
340
383
|
const id = btn.getAttribute('data-id');
|
|
341
384
|
const action = btn.getAttribute('data-rep-action');
|
|
342
385
|
await postJson(`/security/reports/${encodeURIComponent(id)}/${action}`, {});
|
|
@@ -363,41 +406,45 @@
|
|
|
363
406
|
// reports, so we never inject untrusted HTML.
|
|
364
407
|
function renderEngagementDocs(data, target) {
|
|
365
408
|
const docs = Array.isArray(data.docs) ? data.docs : [];
|
|
409
|
+
target.innerHTML = '';
|
|
366
410
|
if (!docs.length) {
|
|
367
411
|
target.innerHTML = emptyStateHtml('No engagement reports filed. Upload one via POST /api/bongos/security/docs.');
|
|
368
412
|
return;
|
|
369
413
|
}
|
|
370
|
-
|
|
371
|
-
|
|
414
|
+
ledger(target, {
|
|
415
|
+
columns: ['Title', 'Slug', 'Updated', { label: 'Size', class: 'num' }, ''],
|
|
416
|
+
pager: makePager('watch-docs', 'otb.watch.docsPerPage'),
|
|
417
|
+
rowHtml: (d) => {
|
|
372
418
|
const kb = d.bytes != null ? `${Math.max(1, Math.round(Number(d.bytes) / 1024))} KB` : '—';
|
|
373
419
|
return (
|
|
374
420
|
'<tr>' +
|
|
375
421
|
`<td>${escapeHtml(d.title)}</td>` +
|
|
376
|
-
`<td><span class="
|
|
422
|
+
`<td><span class="ov-mono">${escapeHtml(d.slug)}</span></td>` +
|
|
377
423
|
`<td>${escapeHtml(fmtDate(d.updated_at))}</td>` +
|
|
378
|
-
`<td>${escapeHtml(kb)}</td>` +
|
|
379
|
-
`<td><div class="watch-actions"
|
|
424
|
+
`<td class="num">${escapeHtml(kb)}</td>` +
|
|
425
|
+
`<td><div class="watch-actions">${ghost(`data-doc-open="${escapeHtml(d.slug)}"`, 'Read')}</div></td>` +
|
|
380
426
|
'</tr>'
|
|
381
427
|
);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
target.
|
|
385
|
-
renderTable(['Title', 'Slug', 'Updated', 'Size', ''], rows) +
|
|
386
|
-
'<div id="engagement-doc-view"></div>';
|
|
428
|
+
},
|
|
429
|
+
}).render(docs);
|
|
430
|
+
target.insertAdjacentHTML('beforeend', '<div id="engagement-doc-view"></div>');
|
|
387
431
|
|
|
388
432
|
// Bespoke handler: opening a doc renders its body into the mounted view (no
|
|
389
|
-
// toast, no reload), so this is NOT a
|
|
390
|
-
target
|
|
391
|
-
|
|
433
|
+
// toast, no reload), so this is NOT a delegate() site — the button stays live.
|
|
434
|
+
if (!target['wired:docs']) {
|
|
435
|
+
target['wired:docs'] = true;
|
|
436
|
+
target.addEventListener('click', async (e) => {
|
|
437
|
+
const btn = e.target.closest('[data-doc-open]');
|
|
438
|
+
if (!btn || !target.contains(btn)) return;
|
|
392
439
|
const slug = btn.getAttribute('data-doc-open');
|
|
393
440
|
const view = $('#engagement-doc-view');
|
|
394
|
-
if (view) view.innerHTML = '
|
|
441
|
+
if (view) view.innerHTML = foot('Reading…');
|
|
395
442
|
try {
|
|
396
443
|
const res = await fetchJson(`/security/docs/${encodeURIComponent(slug)}`);
|
|
397
444
|
const doc = res.doc || {};
|
|
398
445
|
if (view) {
|
|
399
446
|
view.innerHTML =
|
|
400
|
-
`<h4 class="watch-
|
|
447
|
+
`<h4 class="watch-doc__h">${escapeHtml(doc.title || slug)}</h4>` +
|
|
401
448
|
`<pre class="watch-doc">${escapeHtml(doc.content_md || '')}</pre>`;
|
|
402
449
|
}
|
|
403
450
|
} catch (err) {
|
|
@@ -405,7 +452,7 @@
|
|
|
405
452
|
if (view) view.innerHTML = `<p class="watch-unavailable">Could not read the report: ${escapeHtml((err && err.message) || String(err))}</p>`;
|
|
406
453
|
}
|
|
407
454
|
});
|
|
408
|
-
}
|
|
455
|
+
}
|
|
409
456
|
}
|
|
410
457
|
|
|
411
458
|
function loadEngagementDocs() {
|
|
@@ -419,33 +466,33 @@
|
|
|
419
466
|
// 2b. Override requests (grader-bypass queue).
|
|
420
467
|
function renderOverrides(data, target) {
|
|
421
468
|
const requests = Array.isArray(data.requests) ? data.requests : [];
|
|
469
|
+
target.innerHTML = '';
|
|
422
470
|
if (!requests.length) {
|
|
423
471
|
target.innerHTML = emptyStateHtml('No overrides sought.');
|
|
424
472
|
return;
|
|
425
473
|
}
|
|
426
|
-
|
|
427
|
-
|
|
474
|
+
ledger(target, {
|
|
475
|
+
columns: ['Task', 'Requester', 'Rationale', { label: 'Reward', class: 'num' }, 'Asked', 'Decide'],
|
|
476
|
+
pager: makePager('watch-overrides', 'otb.watch.overridesPerPage'),
|
|
477
|
+
rowHtml: (r) => {
|
|
428
478
|
const who = r.requester_display_name || r.requester_login || ('#' + r.requested_by_builder_id);
|
|
429
|
-
const actions =
|
|
430
|
-
'<div class="watch-actions">' +
|
|
431
|
-
`<button type="button" class="watch-btn" data-ovr-action="approve" data-id="${escapeHtml(r.id)}">Approve</button>` +
|
|
432
|
-
`<button type="button" class="watch-btn watch-btn--danger" data-ovr-action="deny" data-id="${escapeHtml(r.id)}">Deny</button>` +
|
|
433
|
-
'</div>';
|
|
434
479
|
return (
|
|
435
480
|
'<tr>' +
|
|
436
|
-
`<td><span class="
|
|
481
|
+
`<td><span class="ov-mono">#${escapeHtml(r.task_id)}</span><br>${escapeHtml(r.task_title || '')}</td>` +
|
|
437
482
|
`<td>${escapeHtml(who)}</td>` +
|
|
438
483
|
`<td>${escapeHtml(r.rationale || '—')}</td>` +
|
|
439
484
|
`<td class="num">${fmtInt(r.credits_reward)}</td>` +
|
|
440
485
|
`<td>${escapeHtml(fmtDate(r.created_at))}</td>` +
|
|
441
|
-
|
|
486
|
+
'<td><div class="watch-actions">' +
|
|
487
|
+
ghost(`data-ovr-action="approve" data-id="${escapeHtml(r.id)}"`, 'Approve') +
|
|
488
|
+
ghost(`data-ovr-action="deny" data-id="${escapeHtml(r.id)}"`, 'Deny', true) +
|
|
489
|
+
'</div></td>' +
|
|
442
490
|
'</tr>'
|
|
443
491
|
);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
target.innerHTML = renderTable(['Task', 'Requester', 'Rationale', 'Reward', 'Asked', 'Decide'], rows);
|
|
492
|
+
},
|
|
493
|
+
}).render(requests);
|
|
447
494
|
|
|
448
|
-
|
|
495
|
+
delegate(target, '[data-ovr-action]', async (btn) => {
|
|
449
496
|
const id = btn.getAttribute('data-id');
|
|
450
497
|
const decision = btn.getAttribute('data-ovr-action');
|
|
451
498
|
await postJson(`/override-requests/${encodeURIComponent(id)}/decide`, { decision, note: '' });
|
|
@@ -462,37 +509,35 @@
|
|
|
462
509
|
});
|
|
463
510
|
}
|
|
464
511
|
|
|
465
|
-
// 2c. Audit log (recent 50) — a read-only long table
|
|
466
|
-
//
|
|
512
|
+
// 2c. Audit log (recent 50) — a read-only long table on the kit ledger + pager
|
|
513
|
+
// (task 2173). Rebuilt per load; the pager pref persists.
|
|
467
514
|
function renderAudit(data, target) {
|
|
468
515
|
const rows = Array.isArray(data.rows) ? data.rows : [];
|
|
516
|
+
target.innerHTML = '';
|
|
469
517
|
if (!rows.length) {
|
|
470
518
|
target.innerHTML = emptyStateHtml('No audit entries yet.');
|
|
471
519
|
return;
|
|
472
520
|
}
|
|
473
|
-
|
|
474
|
-
const ledger = makeLedger({
|
|
475
|
-
mount: target,
|
|
521
|
+
ledger(target, {
|
|
476
522
|
columns: ['When', 'Builder', 'Method', 'Route', { label: 'Status', class: 'num' }],
|
|
523
|
+
pager: makePager('watch-audit', 'otb.watch.auditPerPage'),
|
|
477
524
|
rowHtml: (r) => {
|
|
478
525
|
const status = num(r.response_status);
|
|
479
526
|
const warn = status >= 400 ? ' warn' : '';
|
|
480
527
|
return (
|
|
481
528
|
'<tr>' +
|
|
482
529
|
`<td>${escapeHtml(fmtDate(r.recorded_at))}</td>` +
|
|
483
|
-
`<td><span class="
|
|
484
|
-
(r.surface ? ` <span class="
|
|
530
|
+
`<td><span class="ov-mono">#${escapeHtml(r.builder_id ?? '—')}</span>` +
|
|
531
|
+
(r.surface ? ` <span class="ov-mono">(${escapeHtml(r.surface)})</span>` : '') +
|
|
485
532
|
'</td>' +
|
|
486
533
|
`<td><span class="badge badge--status">${escapeHtml(r.method)}</span></td>` +
|
|
487
|
-
`<td><span class="
|
|
534
|
+
`<td><span class="ov-mono">${escapeHtml(r.route)}</span></td>` +
|
|
488
535
|
`<td class="num${warn}">${escapeHtml(r.response_status ?? '—')}</td>` +
|
|
489
536
|
'</tr>'
|
|
490
537
|
);
|
|
491
538
|
},
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
ledger.render(rows);
|
|
495
|
-
target.insertAdjacentHTML('beforeend', '<p class="watch-note">Newest first. The fifty most recent write operations.</p>');
|
|
539
|
+
}).render(rows);
|
|
540
|
+
target.insertAdjacentHTML('beforeend', foot('Newest first. The fifty most recent write operations.'));
|
|
496
541
|
}
|
|
497
542
|
|
|
498
543
|
function loadAudit() {
|
|
@@ -513,97 +558,106 @@
|
|
|
513
558
|
const RANK_OPTIONS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
514
559
|
|
|
515
560
|
function renderRoster(data, target) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
)
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
})
|
|
561
|
+
const builders = Array.isArray(data.builders) ? data.builders : [];
|
|
562
|
+
setAttention('att-stale', builders.filter((b) => b.stale_claim).length);
|
|
563
|
+
target.innerHTML = '';
|
|
564
|
+
if (!builders.length) {
|
|
565
|
+
target.innerHTML = emptyStateHtml('No builders on the roster.');
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
ledger(target, {
|
|
569
|
+
columns: [
|
|
570
|
+
'Builder', 'Rank',
|
|
571
|
+
{ label: (window.OTBBranding?.currencyLabel?.({ caps: true }) ?? 'Credits'), class: 'num' },
|
|
572
|
+
{ label: 'Karma', class: 'num' }, { label: 'Active claims', class: 'num' },
|
|
573
|
+
'Standing', 'Action',
|
|
574
|
+
],
|
|
575
|
+
pager: makePager('watch-roster', 'otb.watch.rosterPerPage'),
|
|
576
|
+
rowHtml: (b) => {
|
|
577
|
+
const opts = RANK_OPTIONS.slice();
|
|
578
|
+
if (b.rank && !opts.includes(b.rank)) opts.unshift(b.rank);
|
|
579
|
+
const rankSelect =
|
|
580
|
+
`<select data-roster-rank data-id="${escapeHtml(b.id)}" data-current="${escapeHtml(b.rank || '')}" aria-label="Rank">` +
|
|
581
|
+
opts.map((rk) => `<option value="${escapeHtml(rk)}"${rk === b.rank ? ' selected' : ''}>${escapeHtml(rankLabel(rk))}</option>`).join('') +
|
|
582
|
+
'</select>';
|
|
583
|
+
const isInactive = String(b.status) === 'inactive';
|
|
584
|
+
const statusBtn = isInactive
|
|
585
|
+
? ghost(`data-roster-status="active" data-id="${escapeHtml(b.id)}"`, 'Reactivate')
|
|
586
|
+
: ghost(`data-roster-status="inactive" data-id="${escapeHtml(b.id)}"`, 'Offboard', true);
|
|
587
|
+
const claims = num(b.active_claim_count);
|
|
588
|
+
const staleFlag = b.stale_claim ? ' <span class="badge badge--sev-medium">stale</span>' : '';
|
|
589
|
+
// task 1467: the roster name links to the builder's public profile page.
|
|
590
|
+
const rosterName = escapeHtml(b.display_name || b.github_login || ('#' + b.id));
|
|
591
|
+
const rosterNameCell = b.id != null
|
|
592
|
+
? `<a class="lb-name" href="/profile?id=${encodeURIComponent(b.id)}">${rosterName}</a>`
|
|
593
|
+
: rosterName;
|
|
594
|
+
return (
|
|
595
|
+
'<tr>' +
|
|
596
|
+
`<td>${rosterNameCell}` +
|
|
597
|
+
(b.github_login ? ` <span class="ov-mono">@${escapeHtml(b.github_login)}</span>` : '') +
|
|
598
|
+
'</td>' +
|
|
599
|
+
`<td>${rankSelect}</td>` +
|
|
600
|
+
`<td class="num">${fmtInt(b.total_credits)}</td>` +
|
|
601
|
+
`<td class="num">${fmtInt(b.karma)}</td>` +
|
|
602
|
+
`<td class="num${b.stale_claim ? ' warn' : ''}">${fmtInt(claims)}${staleFlag}</td>` +
|
|
603
|
+
`<td><span class="badge badge--status">${escapeHtml(b.status || '—')}</span></td>` +
|
|
604
|
+
`<td><div class="watch-actions">${statusBtn}</div></td>` +
|
|
605
|
+
'</tr>'
|
|
606
|
+
);
|
|
607
|
+
},
|
|
608
|
+
}).render(builders);
|
|
609
|
+
|
|
610
|
+
// Rank change via the <select>. Bespoke: a 'change' listener that rolls the
|
|
611
|
+
// select back to its previous value on failure, delegated so a page flip
|
|
612
|
+
// cannot strand a row without one.
|
|
613
|
+
if (!target['wired:rank']) {
|
|
614
|
+
target['wired:rank'] = true;
|
|
615
|
+
target.addEventListener('change', async (e) => {
|
|
616
|
+
const sel = e.target.closest('[data-roster-rank]');
|
|
617
|
+
if (!sel || !target.contains(sel)) return;
|
|
618
|
+
const id = sel.getAttribute('data-id');
|
|
619
|
+
const rank = sel.value;
|
|
620
|
+
const prev = sel.getAttribute('data-current');
|
|
621
|
+
if (rank === prev) return;
|
|
622
|
+
sel.disabled = true;
|
|
623
|
+
try {
|
|
624
|
+
await patchJson(`/builders/${encodeURIComponent(id)}/rank`, { rank });
|
|
625
|
+
toast(`Rank set to ${rankLabel(rank)}.`);
|
|
626
|
+
await loadRoster();
|
|
627
|
+
} catch (err) {
|
|
628
|
+
if (err && err.kind === '401') return;
|
|
629
|
+
sel.value = prev; // roll back the select on failure
|
|
630
|
+
sel.disabled = false;
|
|
631
|
+
toast(`Could not change rank: ${err.message || err}`);
|
|
632
|
+
}
|
|
583
633
|
});
|
|
634
|
+
}
|
|
584
635
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
636
|
+
// Status change (offboard / reactivate). Bespoke: it gathers an offboarding
|
|
637
|
+
// reason via window.prompt BEFORE disabling, so not delegate().
|
|
638
|
+
if (!target['wired:status']) {
|
|
639
|
+
target['wired:status'] = true;
|
|
640
|
+
target.addEventListener('click', async (e) => {
|
|
641
|
+
const btn = e.target.closest('[data-roster-status]');
|
|
642
|
+
if (!btn || !target.contains(btn)) return;
|
|
643
|
+
const id = btn.getAttribute('data-id');
|
|
644
|
+
const status = btn.getAttribute('data-roster-status');
|
|
645
|
+
let reason = null;
|
|
646
|
+
if (status === 'inactive') {
|
|
647
|
+
reason = window.prompt('Reason for offboarding (optional):', '') || null;
|
|
648
|
+
}
|
|
649
|
+
btn.disabled = true;
|
|
650
|
+
try {
|
|
651
|
+
await patchJson(`/builders/${encodeURIComponent(id)}/status`, { status, reason });
|
|
652
|
+
toast(status === 'inactive' ? 'Builder offboarded.' : 'Builder reactivated.');
|
|
653
|
+
await loadRoster();
|
|
654
|
+
} catch (err) {
|
|
655
|
+
if (err && err.kind === '401') return;
|
|
656
|
+
btn.disabled = false;
|
|
657
|
+
toast(`Could not change standing: ${err.message || err}`);
|
|
658
|
+
}
|
|
606
659
|
});
|
|
660
|
+
}
|
|
607
661
|
}
|
|
608
662
|
|
|
609
663
|
function loadRoster() {
|
|
@@ -617,29 +671,28 @@
|
|
|
617
671
|
// ---- Box costs (#602): per-builder container-cost totals + recent ledger ----
|
|
618
672
|
function renderBoxCosts(data, target) {
|
|
619
673
|
const totals = Array.isArray(data.totals_by_builder) ? data.totals_by_builder : [];
|
|
620
|
-
const
|
|
674
|
+
const recent = Array.isArray(data.ledger) ? data.ledger : [];
|
|
621
675
|
const usd = (n) => `$${(Number(n) || 0).toFixed(2)}`;
|
|
676
|
+
target.innerHTML = '';
|
|
622
677
|
if (!totals.length) {
|
|
623
678
|
target.innerHTML = emptyStateHtml('No dev-box costs accrued yet.');
|
|
624
679
|
return;
|
|
625
680
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
.
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
681
|
+
ledger(group(target, 'Totals by builder', totals.length), {
|
|
682
|
+
columns: ['Builder', { label: 'Total', class: 'num' }],
|
|
683
|
+
rowHtml: (tb) => `<tr><td>${escapeHtml(tb.builder_login || ('#' + (tb.builder_id ?? '?')))}</td><td class="num">${usd(tb.total_usd)}</td></tr>`,
|
|
684
|
+
}).render(totals);
|
|
685
|
+
ledger(group(target, 'Recent ledger', recent.length), {
|
|
686
|
+
columns: ['When', 'Builder', 'Kind', { label: 'Amount', class: 'num' }, 'Detail'],
|
|
687
|
+
pager: makePager('watch-boxcosts', 'otb.watch.boxCostsPerPage'),
|
|
688
|
+
emptyMsg: 'No ledger rows yet.',
|
|
689
|
+
rowHtml: (r) => `<tr><td>${escapeHtml(String(r.recorded_at || '').slice(0, 16).replace('T', ' '))}</td>` +
|
|
632
690
|
`<td>${escapeHtml(r.builder_login || ('#' + (r.builder_id ?? '?')))}</td>` +
|
|
633
|
-
`<td><span class="
|
|
691
|
+
`<td><span class="ov-mono">${escapeHtml((r.source || '').replace('box-', ''))}</span></td>` +
|
|
634
692
|
`<td class="num">${usd(r.amount_usd)}</td>` +
|
|
635
|
-
`<td>${escapeHtml(r.description || '')}</td></tr
|
|
636
|
-
|
|
637
|
-
target.
|
|
638
|
-
'<h3 class="watch-subh">Totals by builder</h3>' +
|
|
639
|
-
renderTable(['Builder', 'Total'], totalRows) +
|
|
640
|
-
'<h3 class="watch-subh">Recent ledger</h3>' +
|
|
641
|
-
renderTable(['When', 'Builder', 'Kind', 'Amount', 'Detail'], recentRows) +
|
|
642
|
-
'<p class="watch-note">Container cost is tracked for oversight — not charged in the early stage.</p>';
|
|
693
|
+
`<td>${escapeHtml(r.description || '')}</td></tr>`,
|
|
694
|
+
}).render(recent);
|
|
695
|
+
target.insertAdjacentHTML('beforeend', foot('Container cost is tracked for oversight — not charged in the early stage.'));
|
|
643
696
|
}
|
|
644
697
|
|
|
645
698
|
function loadBoxCosts() {
|
|
@@ -662,20 +715,23 @@
|
|
|
662
715
|
try {
|
|
663
716
|
const data = await fetchJson('/grades/by-builder?days=30');
|
|
664
717
|
const builders = Array.isArray(data.builders) ? data.builders : [];
|
|
718
|
+
target.innerHTML = '';
|
|
665
719
|
if (!builders.length) {
|
|
666
720
|
target.innerHTML = emptyStateHtml('No reviews recorded in this window.');
|
|
667
721
|
return;
|
|
668
722
|
}
|
|
669
723
|
const sorted = builders.slice().sort((a, b) => num(b.n) - num(a.n));
|
|
670
|
-
|
|
671
|
-
|
|
724
|
+
ledger(target, {
|
|
725
|
+
columns: ['Builder', { label: 'Graded', class: 'num' }, { label: 'Avg score', class: 'num' }, { label: 'Pass rate', class: 'num' }, 'Last graded'],
|
|
726
|
+
pager: makePager('watch-marks', 'otb.watch.marksPerPage'),
|
|
727
|
+
rowHtml: (b) => {
|
|
672
728
|
const avg = (b.avg_score === null || b.avg_score === undefined || b.avg_score === '')
|
|
673
729
|
? '—'
|
|
674
730
|
: escapeHtml(String(b.avg_score));
|
|
675
731
|
return (
|
|
676
732
|
'<tr>' +
|
|
677
733
|
`<td>${escapeHtml(b.display_name || b.github_login || ('#' + b.builder_id))}` +
|
|
678
|
-
(b.github_login ? ` <span class="
|
|
734
|
+
(b.github_login ? ` <span class="ov-mono">@${escapeHtml(b.github_login)}</span>` : '') +
|
|
679
735
|
'</td>' +
|
|
680
736
|
`<td class="num">${fmtInt(b.n)}</td>` +
|
|
681
737
|
`<td class="num">${avg}</td>` +
|
|
@@ -683,11 +739,9 @@
|
|
|
683
739
|
`<td>${escapeHtml(fmtDate(b.last_graded_at))}</td>` +
|
|
684
740
|
'</tr>'
|
|
685
741
|
);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
target.
|
|
689
|
-
renderTable(['Builder', 'Graded', 'Avg score', 'Pass rate', 'Last graded'], rows) +
|
|
690
|
-
(data.window_days ? `<p class="watch-note">Window: last ${escapeHtml(data.window_days)} days.</p>` : '');
|
|
742
|
+
},
|
|
743
|
+
}).render(sorted);
|
|
744
|
+
if (data.window_days) target.insertAdjacentHTML('beforeend', foot(`Window: last ${escapeHtml(data.window_days)} days.`));
|
|
691
745
|
} catch (err) {
|
|
692
746
|
if (err && err.kind === '401') return;
|
|
693
747
|
if (err && err.kind === '403') { renderUnavailable(target, 'Archons only. Review data is not visible at your rank.'); return; }
|
|
@@ -712,13 +766,13 @@
|
|
|
712
766
|
const meanAttempts = at && at.graded_tasks > 0
|
|
713
767
|
? Math.round((at.total_attempts / at.graded_tasks) * 10) / 10
|
|
714
768
|
: null;
|
|
715
|
-
stats.innerHTML =
|
|
716
|
-
{ num: (rolling.avg_score ?? '—') === '—' ? '—' :
|
|
769
|
+
stats.innerHTML = figs([
|
|
770
|
+
{ num: (rolling.avg_score ?? '—') === '—' ? '—' : String(rolling.avg_score), label: 'Avg score' },
|
|
717
771
|
{ num: fmtPct(passRate), label: 'Pass rate' },
|
|
718
772
|
{ num: fmtInt(n), label: 'Sample size' },
|
|
719
|
-
{ num: data.threshold !== undefined && data.threshold !== null ?
|
|
773
|
+
{ num: data.threshold !== undefined && data.threshold !== null ? String(data.threshold) : '—', label: 'Threshold' },
|
|
720
774
|
...(at ? [
|
|
721
|
-
{ num: meanAttempts === null ? '—' :
|
|
775
|
+
{ num: meanAttempts === null ? '—' : String(meanAttempts), label: 'Attempts/task' },
|
|
722
776
|
{ num: at.remediation_rate === null ? '—' : fmtPct(at.remediation_rate), label: 'Remediation' },
|
|
723
777
|
] : []),
|
|
724
778
|
]);
|
|
@@ -745,7 +799,7 @@
|
|
|
745
799
|
})
|
|
746
800
|
.join('') +
|
|
747
801
|
'</div>' +
|
|
748
|
-
'
|
|
802
|
+
foot('Per day: ships graded · avg score.');
|
|
749
803
|
}
|
|
750
804
|
|
|
751
805
|
function loadMasonMarks() {
|
|
@@ -762,26 +816,25 @@
|
|
|
762
816
|
|
|
763
817
|
function renderSurveyor(data, stats) {
|
|
764
818
|
const body = $('#surveyor-body');
|
|
765
|
-
stats.innerHTML =
|
|
766
|
-
{ num: data.overall_median !== undefined && data.overall_median !== null ?
|
|
819
|
+
stats.innerHTML = figs([
|
|
820
|
+
{ num: data.overall_median !== undefined && data.overall_median !== null ? String(data.overall_median) : '—', label: 'Overall median ratio' },
|
|
767
821
|
{ num: fmtInt(data.sample_size), label: 'Sample size' },
|
|
768
822
|
]);
|
|
769
823
|
const byVersion = Array.isArray(data.by_version) ? data.by_version : [];
|
|
824
|
+
body.innerHTML = '';
|
|
770
825
|
if (!byVersion.length) {
|
|
771
826
|
body.innerHTML = emptyStateHtml('No drift recorded by version yet.');
|
|
772
827
|
return;
|
|
773
828
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
.join('');
|
|
784
|
-
body.innerHTML = renderTable(['Version', 'Median ratio', 'Tasks'], rows);
|
|
829
|
+
ledger(body, {
|
|
830
|
+
columns: ['Version', { label: 'Median ratio', class: 'num' }, { label: 'Tasks', class: 'num' }],
|
|
831
|
+
rowHtml: (v) =>
|
|
832
|
+
'<tr>' +
|
|
833
|
+
`<td><span class="ov-mono">${escapeHtml(v.version_id)}</span></td>` +
|
|
834
|
+
`<td class="num">${v.median_ratio !== undefined && v.median_ratio !== null ? escapeHtml(String(v.median_ratio)) : '—'}</td>` +
|
|
835
|
+
`<td class="num">${fmtInt(v.task_count)}</td>` +
|
|
836
|
+
'</tr>',
|
|
837
|
+
}).render(byVersion);
|
|
785
838
|
}
|
|
786
839
|
|
|
787
840
|
function loadSurveyor() {
|
|
@@ -796,7 +849,7 @@
|
|
|
796
849
|
// 7. REPO HEALTH — codebase shape + weight
|
|
797
850
|
// =========================================================================
|
|
798
851
|
|
|
799
|
-
// Known scalar fields we render as their own
|
|
852
|
+
// Known scalar fields we render as their own figures if present. Anything
|
|
800
853
|
// else numeric on `latest` is rendered defensively (label-cased key).
|
|
801
854
|
function labelize(key) {
|
|
802
855
|
return String(key)
|
|
@@ -805,60 +858,45 @@
|
|
|
805
858
|
}
|
|
806
859
|
|
|
807
860
|
function renderLedger(data, stats) {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
stats.innerHTML = statCards(cards);
|
|
826
|
-
|
|
827
|
-
// loc_by_dir as a small list.
|
|
828
|
-
const locByDir = latest.loc_by_dir && typeof latest.loc_by_dir === 'object' ? latest.loc_by_dir : {};
|
|
829
|
-
const dirEntries = Object.entries(locByDir).sort((a, b) => num(b[1]) - num(a[1]));
|
|
830
|
-
if (dirEntries.length) {
|
|
831
|
-
locEl.innerHTML =
|
|
832
|
-
'<h3 class="watch-sub-h">Lines by directory</h3><ul class="watch-list">' +
|
|
833
|
-
dirEntries
|
|
834
|
-
.map(
|
|
835
|
-
([dir, loc]) =>
|
|
836
|
-
`<li><span class="k watch-mono">${escapeHtml(dir)}</span><span class="v">${fmtInt(loc)}</span></li>`
|
|
837
|
-
)
|
|
838
|
-
.join('') +
|
|
839
|
-
'</ul>';
|
|
840
|
-
} else {
|
|
841
|
-
locEl.innerHTML = '';
|
|
861
|
+
const locEl = $('#ledger-loc');
|
|
862
|
+
const filesEl = $('#ledger-files');
|
|
863
|
+
const latest = data.latest || {};
|
|
864
|
+
|
|
865
|
+
// Core figures, plus any extra scalar fields (dead exports, stale branches,
|
|
866
|
+
// etc.) that the snapshot happens to carry — render defensively.
|
|
867
|
+
const cards = [
|
|
868
|
+
{ num: fmtCompact(latest.total_loc), label: 'Total lines' },
|
|
869
|
+
{ num: fmtDate(latest.snapshot_date), label: 'Snapshot' },
|
|
870
|
+
];
|
|
871
|
+
const SKIP = new Set(['total_loc', 'snapshot_date', 'loc_by_dir', 'largest_files']);
|
|
872
|
+
for (const [k, v] of Object.entries(latest)) {
|
|
873
|
+
if (SKIP.has(k)) continue;
|
|
874
|
+
if (typeof v === 'number') {
|
|
875
|
+
cards.push({ num: fmtInt(v), label: labelize(k) });
|
|
842
876
|
}
|
|
877
|
+
}
|
|
878
|
+
stats.innerHTML = figs(cards);
|
|
879
|
+
|
|
880
|
+
// loc_by_dir as a two-column ledger under its own head.
|
|
881
|
+
const locByDir = latest.loc_by_dir && typeof latest.loc_by_dir === 'object' ? latest.loc_by_dir : {};
|
|
882
|
+
const dirEntries = Object.entries(locByDir).sort((a, b) => num(b[1]) - num(a[1]));
|
|
883
|
+
locEl.innerHTML = '';
|
|
884
|
+
if (dirEntries.length) {
|
|
885
|
+
ledger(group(locEl, 'Lines by directory', dirEntries.length), {
|
|
886
|
+
columns: ['Directory', { label: 'Lines', class: 'num' }],
|
|
887
|
+
rowHtml: ([dir, loc]) => `<tr><td><span class="ov-mono">${escapeHtml(dir)}</span></td><td class="num">${fmtInt(loc)}</td></tr>`,
|
|
888
|
+
}).render(dirEntries);
|
|
889
|
+
}
|
|
843
890
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
'</tr>'
|
|
854
|
-
)
|
|
855
|
-
.join('');
|
|
856
|
-
filesEl.innerHTML =
|
|
857
|
-
'<h3 class="watch-sub-h">Largest files</h3>' +
|
|
858
|
-
renderTable(['File', 'Lines'], rows);
|
|
859
|
-
} else {
|
|
860
|
-
filesEl.innerHTML = '';
|
|
861
|
-
}
|
|
891
|
+
// Top ~10 largest files.
|
|
892
|
+
const largest = Array.isArray(latest.largest_files) ? latest.largest_files.slice(0, 10) : [];
|
|
893
|
+
filesEl.innerHTML = '';
|
|
894
|
+
if (largest.length) {
|
|
895
|
+
ledger(group(filesEl, 'Largest files', largest.length), {
|
|
896
|
+
columns: ['File', { label: 'Lines', class: 'num' }],
|
|
897
|
+
rowHtml: (f) => `<tr><td><span class="ov-mono">${escapeHtml(f.path)}</span></td><td class="num">${fmtInt(f.lines)}</td></tr>`,
|
|
898
|
+
}).render(largest);
|
|
899
|
+
}
|
|
862
900
|
}
|
|
863
901
|
|
|
864
902
|
function loadLedger() {
|
|
@@ -870,18 +908,45 @@
|
|
|
870
908
|
}
|
|
871
909
|
|
|
872
910
|
// =========================================================================
|
|
873
|
-
// 8. SESSIONS — session tally +
|
|
911
|
+
// 8. SESSIONS — session tally + the latest five
|
|
874
912
|
// =========================================================================
|
|
875
913
|
|
|
914
|
+
// The tally as figures, then the five most recent sessions the request has
|
|
915
|
+
// always carried (limit=5) and the page used to drop on the floor (task
|
|
916
|
+
// 1003469): builder, tasks, what it earned, tokens, how long, when.
|
|
876
917
|
function renderSessions(data, stats) {
|
|
877
918
|
const t = data.totals || {};
|
|
878
919
|
setAttention('att-sessions', t.total);
|
|
879
|
-
stats.innerHTML =
|
|
920
|
+
stats.innerHTML = figs([
|
|
880
921
|
{ num: fmtInt(t.total), label: 'Sessions' },
|
|
881
922
|
{ num: fmtInt(t.builders), label: 'Builders' },
|
|
882
923
|
{ num: fmtCompact(t.tokens), label: 'Tokens' },
|
|
883
924
|
{ num: fmtInt(t.tool_errors), label: 'Tool errors' },
|
|
884
925
|
]);
|
|
926
|
+
const recentEl = $('#sessions-recent');
|
|
927
|
+
if (!recentEl) return;
|
|
928
|
+
const sessions = Array.isArray(data.sessions) ? data.sessions : [];
|
|
929
|
+
recentEl.innerHTML = '';
|
|
930
|
+
if (!sessions.length) return;
|
|
931
|
+
ledger(group(recentEl, 'Latest sessions', sessions.length), {
|
|
932
|
+
columns: ['Builder', 'Tasks', { label: 'Earned', class: 'num' }, { label: 'Tokens', class: 'num' }, { label: 'Duration', class: 'num' }, 'Uploaded'],
|
|
933
|
+
rowHtml: (s) => {
|
|
934
|
+
const who = s.github_login ? `@${escapeHtml(s.github_login)}` : `#${escapeHtml(s.builder_id ?? '?')}`;
|
|
935
|
+
const tasks = Array.isArray(s.task_ids) && s.task_ids.length
|
|
936
|
+
? s.task_ids.map((id) => `<a href="/#/task/${encodeURIComponent(id)}">task ${escapeHtml(String(id))}</a>`).join(', ')
|
|
937
|
+
: '—';
|
|
938
|
+
return (
|
|
939
|
+
'<tr>' +
|
|
940
|
+
`<td>${s.builder_id != null ? `<a class="lb-name" href="/profile?id=${encodeURIComponent(s.builder_id)}">${who}</a>` : who}</td>` +
|
|
941
|
+
`<td>${tasks}</td>` +
|
|
942
|
+
`<td class="num">${fmtInt(s.drachmae_earned)}</td>` +
|
|
943
|
+
`<td class="num">${fmtCompact(s.total_tokens)}</td>` +
|
|
944
|
+
`<td class="num">${escapeHtml(fmtDuration(s.duration_seconds))}</td>` +
|
|
945
|
+
`<td>${escapeHtml(fmtDate(s.uploaded_at))}</td>` +
|
|
946
|
+
'</tr>'
|
|
947
|
+
);
|
|
948
|
+
},
|
|
949
|
+
}).render(sessions);
|
|
885
950
|
}
|
|
886
951
|
|
|
887
952
|
function loadSessions() {
|
|
@@ -897,7 +962,7 @@
|
|
|
897
962
|
// =========================================================================
|
|
898
963
|
|
|
899
964
|
function showSealed(message) {
|
|
900
|
-
$('#watch-sub').textContent = '
|
|
965
|
+
$('#watch-sub').textContent = 'Restricted.';
|
|
901
966
|
const gate = $('#sealed-scroll');
|
|
902
967
|
if (message) $('#sealed-body').textContent = message;
|
|
903
968
|
gate.hidden = false;
|
|
@@ -951,12 +1016,12 @@
|
|
|
951
1016
|
// it reads (roster, security reports, audit log, sessions, box costs) delegated to
|
|
952
1017
|
// metic with it. Cosmetic — requireGovernmentPage + the route gates are the wall.
|
|
953
1018
|
if (!builder || (builder.rank !== 'metic' && builder.rank !== 'archon')) {
|
|
954
|
-
showSealed(
|
|
1019
|
+
showSealed(`Available from the ${rankLabel('metic')} rank up. Your current rank does not grant access.`);
|
|
955
1020
|
return;
|
|
956
1021
|
}
|
|
957
1022
|
|
|
958
1023
|
revealSections();
|
|
959
|
-
$('#watch-sub').textContent = 'System oversight
|
|
1024
|
+
$('#watch-sub').textContent = 'System oversight: people, operations and security.';
|
|
960
1025
|
|
|
961
1026
|
// Each section loads independently — one failure must not break the rest.
|
|
962
1027
|
loadGate();
|