@bongos/core 1.19.469 → 1.19.471
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 +54 -34
- package/clients/bongos-client/index.d.ts +2 -2
- package/docs/adr/0226-one-stale-timer-reporting-degrades-enforcement-refuses.md +121 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +9 -5
- package/docs/copy-inventory.md +40 -43
- package/docs/copy-registry.json +155 -182
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/atlas.css +96 -88
- package/modules/hall-ui/public/atlas.html +43 -20
- package/modules/hall-ui/public/atlas.js +118 -20
- package/modules/hall-ui/public/atlas.states.json +52 -1
- package/modules/hall-ui/public/modules.css +67 -86
- package/modules/hall-ui/public/modules.html +46 -35
- package/modules/hall-ui/public/modules.js +195 -103
- package/modules/hall-ui/public/modules.states.json +48 -1
- package/modules/hall-ui/public/style.css +8 -8
- package/modules/hall-ui/public/task-lib.js +7 -3
- package/modules/lifecycle/db-ship.js +22 -11
- package/modules/sessions/pulse.js +15 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +6 -0
- package/scripts/gds/pulse.js +48 -24
- package/src/bongos/routes/builders.js +56 -24
- package/src/module-api.js +16 -1
- package/src/stale-timer.js +177 -0
- package/tests/hall_catalog_world.mjs +181 -0
- package/tests/module_api.mjs +1 -0
- package/tests/onboarding_observability.mjs +41 -1
- package/tests/stale_timer.mjs +230 -0
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
// public-builders/modules.js — the hall "Modules" tab (/builders/modules).
|
|
2
2
|
//
|
|
3
|
-
// ADR 0107 §9 (task 1776) + §2/§3 the browsable catalog (task 1001772)
|
|
3
|
+
// ADR 0107 §9 (task 1776) + §2/§3 the browsable catalog (task 1001772);
|
|
4
|
+
// redesigned onto the oversight family's row in task 1003472. Jobs:
|
|
4
5
|
// 1. BROWSE the catalog — every module the core ships that this instance can
|
|
5
|
-
// enable, each with its author + origin credit, filterable by
|
|
6
|
-
//
|
|
7
|
-
// OWN modules by `source` (which TREE it ships from — the core
|
|
8
|
-
// this repo), which GET /api/bongos/modules reads structurally
|
|
9
|
-
// loader's discovery roots. That is a different axis from
|
|
10
|
-
// (WHO built it, read off the manifest's origin) and both are
|
|
11
|
-
//
|
|
6
|
+
// enable, each with its author + origin credit, filterable by one search
|
|
7
|
+
// over name/author/origin/licence and a state lens. Split from the
|
|
8
|
+
// instance's OWN modules by `source` (which TREE it ships from — the core
|
|
9
|
+
// package or this repo), which GET /api/bongos/modules reads structurally
|
|
10
|
+
// off the loader's discovery roots. That is a different axis from
|
|
11
|
+
// `provenance` (WHO built it, read off the manifest's origin) and both are
|
|
12
|
+
// shown: the group says where it ships from, the facts say who to credit.
|
|
12
13
|
// 2. Enable/disable — a toggle over config/modules.json (POST /modules/:key/enable).
|
|
13
14
|
// Any signed-in builder sees the state; only metic+archon can flip it (the
|
|
14
15
|
// server enforces this — a lower-rank click just gets a 403 + a friendly
|
|
15
|
-
// toast, same as every other rank-gated write in this hall).
|
|
16
|
+
// toast, same as every other rank-gated write in this hall). A viewer below
|
|
17
|
+
// the floor sees the state as a pill rather than a dead control.
|
|
16
18
|
// 3. "Submit for review" front door — files one of this project's own modules
|
|
17
19
|
// into the upstreaming review queue (POST /modules/:key/submit, task 1770).
|
|
20
|
+
// The door sits on the module's own row.
|
|
18
21
|
//
|
|
19
22
|
// The rows come from the SAME builder as `bongos module list --catalog`
|
|
20
23
|
// (src/module-loader/catalog.js), so the two surfaces cannot drift.
|
|
@@ -27,31 +30,41 @@
|
|
|
27
30
|
// /lib/bongos-client.js. baseUrl:'' passes the full `${API}/...` URLs below
|
|
28
31
|
// through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
|
|
29
32
|
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
30
|
-
const { escapeHtml, $, toast } = window.OTB;
|
|
33
|
+
const { escapeHtml, $, toast, rankLabel } = window.OTB;
|
|
34
|
+
const kit = window.OTBKit;
|
|
31
35
|
|
|
32
36
|
let viewerRank = 'xenos';
|
|
33
37
|
let canToggle = false;
|
|
34
|
-
|
|
38
|
+
let submitLive = false;
|
|
39
|
+
// The last payload, held so the filter bar can re-render without a refetch.
|
|
35
40
|
let allModules = [];
|
|
41
|
+
// The filter bar's current values (search + state lens).
|
|
42
|
+
let query = '';
|
|
43
|
+
let stateLens = '';
|
|
44
|
+
// Which rows a builder has opened, by key, so a re-render (a toggle, a
|
|
45
|
+
// filter) does not fold the description they were reading.
|
|
46
|
+
const openKeys = new Set();
|
|
36
47
|
|
|
37
|
-
|
|
48
|
+
const floor = () => rankLabel('metic');
|
|
49
|
+
|
|
50
|
+
function provenancePill(m) {
|
|
38
51
|
return m.provenance === 'core'
|
|
39
|
-
? '<span class="
|
|
40
|
-
: '<span class="
|
|
52
|
+
? '<span class="fact-pill fact-pill--info">core-loaded</span>'
|
|
53
|
+
: '<span class="fact-pill">built here</span>';
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
// Author + origin credit (ADR 0107 §2/§3 — credit now, money later). Reads
|
|
44
57
|
// `credit`, not the raw manifest fields, so a core module whose manifest
|
|
45
|
-
// declares no author still shows who ships it rather than a blank
|
|
58
|
+
// declares no author still shows who ships it rather than a blank row; an
|
|
46
59
|
// INFERRED credit is marked so it never reads as something the author typed.
|
|
47
|
-
function
|
|
60
|
+
function attributionFacts(m) {
|
|
48
61
|
const credit = m.credit || { author: m.author, origin: m.origin, inferred: false };
|
|
49
62
|
const mark = credit.inferred ? '<abbr class="mod-inferred" title="Not declared in module.json — this is who ships the module.">*</abbr>' : '';
|
|
50
|
-
const
|
|
51
|
-
if (credit.author)
|
|
52
|
-
if (credit.origin && credit.origin !== credit.author)
|
|
53
|
-
if (m.license)
|
|
54
|
-
return
|
|
63
|
+
const facts = [];
|
|
64
|
+
if (credit.author) facts.push(`<span class="ov-fact">by <b>${escapeHtml(credit.author)}</b>${mark}</span>`);
|
|
65
|
+
if (credit.origin && credit.origin !== credit.author) facts.push(`<span class="ov-fact">from <b>${escapeHtml(credit.origin)}</b>${mark}</span>`);
|
|
66
|
+
if (m.license) facts.push(`<span class="fact-chip">${escapeHtml(m.license)}</span>`);
|
|
67
|
+
return facts.join('');
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
// Free-text browse over the fields a builder would search by. Mirrors
|
|
@@ -63,41 +76,77 @@
|
|
|
63
76
|
return [m.key, m.title, m.description, credit.author, credit.origin, m.license]
|
|
64
77
|
.some((f) => f && String(f).toLowerCase().includes(q));
|
|
65
78
|
}
|
|
79
|
+
function matchesLens(m) {
|
|
80
|
+
if (stateLens === 'enabled') return !!m.enabled;
|
|
81
|
+
if (stateLens === 'disabled') return !m.enabled;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
66
84
|
|
|
67
85
|
// Upkeep posture (ADR 0107 §7 / ADR 0166) — who keeps this module true as the
|
|
68
|
-
// core advances.
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
86
|
+
// core advances. Colour marks the EXCEPTION: an unowned entry is the thing the
|
|
87
|
+
// policy exists to catch, so it is the warn pill; a maintained one is a plain
|
|
88
|
+
// fact, since seventeen "core-maintained" pills would be the loudest thing on
|
|
89
|
+
// the page and none of them would be news.
|
|
90
|
+
function upkeepFact(m) {
|
|
72
91
|
const mt = m.maintenance;
|
|
73
|
-
if (!mt || !mt.status) return '<span class="
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
].filter(Boolean).join(' — ');
|
|
80
|
-
return `<span class="mod-chip" title="${escapeHtml(detail || mt.status)}">upkeep: ${escapeHtml(mt.status)}</span>`;
|
|
92
|
+
if (!mt || !mt.status) return '<span class="fact-pill fact-pill--warn" title="No maintenance declared — nobody is on the hook for this module as the core advances.">upkeep undeclared</span>';
|
|
93
|
+
const status = String(mt.status);
|
|
94
|
+
if (status === 'deprecated' || status === 'superseded' || mt.successor || mt.removeAfter) {
|
|
95
|
+
return `<span class="fact-pill fact-pill--warn">${escapeHtml(status)}</span>`;
|
|
96
|
+
}
|
|
97
|
+
return `<span class="ov-fact">upkeep <b>${escapeHtml(status)}</b></span>`;
|
|
81
98
|
}
|
|
82
99
|
|
|
83
|
-
|
|
100
|
+
// The lines the row's details disclosure opens onto: the facts the payload
|
|
101
|
+
// carries that the card never rendered — what the module contributes, the
|
|
102
|
+
// core it needs, who maintains it and what it hands off to.
|
|
103
|
+
function detailLines(m) {
|
|
104
|
+
const lines = [];
|
|
105
|
+
const line = (k, v) => { if (v) lines.push(`<li><span class="ov-key ov-key--faint">${escapeHtml(k)}</span><span class="ov-lines__v">${v}</span></li>`); };
|
|
106
|
+
line('key', `<span class="ov-mono">${escapeHtml(m.key)}</span>`);
|
|
107
|
+
if (m.coreVersion) line('needs core', `<span class="ov-mono">${escapeHtml(String(m.coreVersion))}</span>`);
|
|
108
|
+
const c = m.contributes || {};
|
|
109
|
+
for (const kind of Object.keys(c)) {
|
|
110
|
+
const items = Array.isArray(c[kind]) ? c[kind] : [];
|
|
111
|
+
if (items.length) line(kind, items.map((x) => `<span class="fact-chip">${escapeHtml(String(x))}</span>`).join(''));
|
|
112
|
+
}
|
|
113
|
+
const mt = m.maintenance || {};
|
|
114
|
+
if (m.maintainer) line('maintainer', escapeHtml(String(m.maintainer)));
|
|
115
|
+
if (mt.successor) line('superseded by', escapeHtml(String(mt.successor)));
|
|
116
|
+
if (mt.removeAfter) line('removable after core', `<span class="ov-mono">${escapeHtml(String(mt.removeAfter))}</span>`);
|
|
117
|
+
if (mt.note) line('note', escapeHtml(String(mt.note)));
|
|
118
|
+
if (m.default != null) line('ships', m.default ? 'default on' : 'default off');
|
|
119
|
+
return lines.length ? `<ul class="ov-lines">${lines.join('')}</ul>` : '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The right column: ONE control. The family's grant toggle when the viewer
|
|
123
|
+
// can flip it (enabled in ink, disabled faint, the accent on hover); the
|
|
124
|
+
// state as a pill when they cannot. A module built here also carries its
|
|
125
|
+
// submit door, since that door is about this module and no other.
|
|
126
|
+
function rightHtml(m) {
|
|
127
|
+
const on = !!m.enabled;
|
|
128
|
+
const control = canToggle
|
|
129
|
+
? `<button type="button" class="ov-toggle mod-toggle" aria-pressed="${on ? 'true' : 'false'}" data-key="${escapeHtml(m.key)}">${on ? 'enabled' : 'disabled'}</button>`
|
|
130
|
+
: `<span class="fact-pill ${on ? 'fact-pill--ok' : ''}" title="${escapeHtml(floor())} and above can enable or disable a module">${on ? 'enabled' : 'disabled'}</span>`;
|
|
131
|
+
const submit = sourceOf(m) === 'instance'
|
|
132
|
+
? `<button type="button" class="btn-ghost btn--sm mod-submit" data-submit-key="${escapeHtml(m.key)}"${submitLive ? '' : ' disabled title="Submit pipeline not live yet — tracked by task 1770"'}>submit for review</button>`
|
|
133
|
+
: '';
|
|
134
|
+
return control + submit;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function rowHTML(m) {
|
|
138
|
+
const desc = String(m.description || '').trim();
|
|
139
|
+
const lines = detailLines(m);
|
|
84
140
|
return `
|
|
85
|
-
<
|
|
86
|
-
<div class="
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
</
|
|
91
|
-
</div>
|
|
92
|
-
<p class="mod-card__desc">${escapeHtml(m.description || '')}</p>
|
|
93
|
-
<div class="mod-chips">${provenanceChip(m)}${attributionChips(m)}${maintenanceChip(m)}</div>
|
|
94
|
-
<div class="mod-card__actions">
|
|
95
|
-
<button type="button" class="mod-toggle" data-on="${m.enabled ? '1' : '0'}"
|
|
96
|
-
${canToggle ? '' : 'disabled title="Metic+ can enable or disable a module"'}>
|
|
97
|
-
${m.enabled ? 'Enabled' : 'Disabled'}
|
|
98
|
-
</button>
|
|
141
|
+
<li class="ov-row mod-row" data-key="${escapeHtml(m.key)}">
|
|
142
|
+
<div class="ov-row__main">
|
|
143
|
+
<span class="ov-row__title">${escapeHtml(m.title || m.key)}<span class="ov-mono">${escapeHtml(m.key)}</span></span>
|
|
144
|
+
${desc ? `<p class="ov-row__sub mod-row__desc">${escapeHtml(desc)}</p>` : ''}
|
|
145
|
+
<div class="ov-row__facts">${provenancePill(m)}${attributionFacts(m)}${upkeepFact(m)}</div>
|
|
146
|
+
${lines ? `<details class="mod-more"${openKeys.has(m.key) ? ' open' : ''}><summary aria-label="Details for ${escapeHtml(m.title || m.key)}"></summary>${lines}</details>` : ''}
|
|
99
147
|
</div>
|
|
100
|
-
|
|
148
|
+
<div class="ov-row__right mod-row__right">${rightHtml(m)}</div>
|
|
149
|
+
</li>`;
|
|
101
150
|
}
|
|
102
151
|
|
|
103
152
|
// `source` splits the page: 'core' = shipped by the core package (the
|
|
@@ -109,81 +158,126 @@
|
|
|
109
158
|
return m.provenance === 'core' ? 'core' : 'instance';
|
|
110
159
|
}
|
|
111
160
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const
|
|
161
|
+
// The figure band: the four numbers the page head used to carry as prose.
|
|
162
|
+
function figsHTML() {
|
|
163
|
+
const core = allModules.filter((m) => sourceOf(m) === 'core');
|
|
164
|
+
const own = allModules.filter((m) => sourceOf(m) === 'instance');
|
|
165
|
+
const enabled = allModules.filter((m) => m.enabled).length;
|
|
166
|
+
const undeclared = allModules.filter((m) => !m.maintenance || !m.maintenance.status).length;
|
|
167
|
+
const fig = (l, v, s) => `<div class="ov-fig"><span class="ov-fig__l">${l}</span><span class="ov-fig__v">${v}</span>${s ? `<span class="ov-fig__s">${s}</span>` : ''}</div>`;
|
|
168
|
+
// Short labels on purpose: the band is two columns at 390px and a label
|
|
169
|
+
// that wraps a phone's column is cut with an ellipsis.
|
|
170
|
+
return fig('modules', allModules.length, `${core.length} from the core`)
|
|
171
|
+
+ fig('enabled', enabled, `of ${allModules.length}`)
|
|
172
|
+
+ fig('built here', own.length, own.length ? 'can go upstream' : 'none on disk')
|
|
173
|
+
+ fig('no upkeep', undeclared, undeclared ? 'nobody on the hook' : 'every module owned');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderRows() {
|
|
177
|
+
const matching = allModules.filter((m) => matchesQuery(m, query) && matchesLens(m));
|
|
116
178
|
const core = matching.filter((m) => sourceOf(m) === 'core');
|
|
117
179
|
const own = matching.filter((m) => sourceOf(m) === 'instance');
|
|
180
|
+
const narrowed = !!(query || stateLens);
|
|
118
181
|
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
: `<
|
|
182
|
+
const coreList = $('#core-grid');
|
|
183
|
+
if (coreList) {
|
|
184
|
+
coreList.innerHTML = core.length ? core.map(rowHTML).join('')
|
|
185
|
+
: `<li>${kit.emptyStateHtml(narrowed ? 'No catalog module matches that search.' : 'The core ships no modules this instance can see.')}</li>`;
|
|
123
186
|
}
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
: `<
|
|
187
|
+
const ownList = $('#own-grid');
|
|
188
|
+
if (ownList) {
|
|
189
|
+
ownList.innerHTML = own.length ? own.map(rowHTML).join('')
|
|
190
|
+
: `<li>${kit.emptyStateHtml(narrowed ? 'None of this project\'s own modules match that search.' : 'None on disk — everything here ships with the core.')}</li>`;
|
|
128
191
|
}
|
|
192
|
+
const coreN = $('#core-n');
|
|
193
|
+
if (coreN) coreN.textContent = String(core.length);
|
|
194
|
+
const ownN = $('#own-n');
|
|
195
|
+
if (ownN) ownN.textContent = String(own.length);
|
|
129
196
|
|
|
130
|
-
|
|
197
|
+
// The count line speaks only while a filter narrows the list — the figure
|
|
198
|
+
// band under it already says how many there are.
|
|
199
|
+
const count = $('#mod-count');
|
|
131
200
|
if (count) {
|
|
132
|
-
|
|
133
|
-
|
|
201
|
+
const n = allModules.length;
|
|
202
|
+
count.textContent = narrowed
|
|
203
|
+
? `${matching.length} of ${n} ${n === 1 ? 'module' : 'modules'} match${query ? ` “${query}”` : ''}${stateLens ? ` · ${stateLens}` : ''}.`
|
|
134
204
|
: '';
|
|
135
205
|
}
|
|
206
|
+
}
|
|
136
207
|
|
|
137
|
-
|
|
208
|
+
// One listener on the panel outlives every re-render: the toggle, the submit
|
|
209
|
+
// door and the details disclosure all bubble to it.
|
|
210
|
+
let wired = false;
|
|
211
|
+
function wire() {
|
|
212
|
+
if (wired) return;
|
|
213
|
+
wired = true;
|
|
214
|
+
const panel = $('#core-scroll');
|
|
215
|
+
if (!panel) return;
|
|
216
|
+
panel.addEventListener('click', (ev) => {
|
|
217
|
+
const toggle = ev.target.closest('.mod-toggle');
|
|
218
|
+
if (toggle) { onToggle(toggle); return; }
|
|
219
|
+
const submit = ev.target.closest('.mod-submit');
|
|
220
|
+
if (submit) onSubmit(submit);
|
|
221
|
+
});
|
|
222
|
+
panel.addEventListener('toggle', (ev) => {
|
|
223
|
+
const d = ev.target;
|
|
224
|
+
if (!(d instanceof HTMLDetailsElement) || !d.classList.contains('mod-more')) return;
|
|
225
|
+
const row = d.closest('.mod-row');
|
|
226
|
+
const key = row && row.dataset.key;
|
|
227
|
+
if (!key) return;
|
|
228
|
+
if (d.open) openKeys.add(key); else openKeys.delete(key);
|
|
229
|
+
}, true);
|
|
138
230
|
}
|
|
139
231
|
|
|
140
232
|
function render(data) {
|
|
141
233
|
allModules = Array.isArray(data.modules) ? data.modules : [];
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
234
|
+
submitLive = !!data.submit_pipeline_live;
|
|
235
|
+
|
|
236
|
+
const figs = $('#mod-figs');
|
|
237
|
+
if (figs) figs.innerHTML = figsHTML();
|
|
145
238
|
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
239
|
+
const mount = $('#mod-filters');
|
|
240
|
+
if (mount && !mount.childElementCount) {
|
|
241
|
+
kit.makeFilterBar({
|
|
242
|
+
mount,
|
|
243
|
+
search: { id: 'mod-search', label: 'Search', placeholder: 'name, author, origin or licence' },
|
|
244
|
+
primary: [
|
|
245
|
+
{ id: 'mod-state', label: 'State', options: [
|
|
246
|
+
{ value: '', label: 'all' }, { value: 'enabled', label: 'enabled' }, { value: 'disabled', label: 'disabled' }] },
|
|
247
|
+
],
|
|
248
|
+
onChange: (vals) => {
|
|
249
|
+
query = String(vals['mod-search'] || '').trim().toLowerCase();
|
|
250
|
+
stateLens = String(vals['mod-state'] || '');
|
|
251
|
+
renderRows();
|
|
252
|
+
},
|
|
253
|
+
});
|
|
150
254
|
}
|
|
151
255
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// already ships with the core is, by definition, already in the catalog.
|
|
156
|
-
const submitStatus = $('#submit-status');
|
|
157
|
-
if (submitStatus) {
|
|
158
|
-
submitStatus.innerHTML = own.length
|
|
159
|
-
? own.map((m) => `
|
|
160
|
-
<span style="display:inline-flex;align-items:center;gap:8px;margin:4px 12px 4px 0;">
|
|
161
|
-
<button type="button" class="mod-submit" data-submit-key="${escapeHtml(m.key)}"
|
|
162
|
-
${data.submit_pipeline_live ? '' : 'disabled title="Submit pipeline not live yet — tracked by task 1770"'}>
|
|
163
|
-
Submit "${escapeHtml(m.key)}" for review
|
|
164
|
-
</button>
|
|
165
|
-
</span>`).join('')
|
|
166
|
-
: '<span class="mod-note">Nothing to submit — every module here already ships with the core.</span>';
|
|
167
|
-
document.querySelectorAll('[data-submit-key]').forEach((btn) => btn.addEventListener('click', onSubmit));
|
|
256
|
+
const lede = $('#submit-lede');
|
|
257
|
+
if (lede && !allModules.some((m) => sourceOf(m) === 'instance')) {
|
|
258
|
+
lede.textContent = 'Nothing to submit — every module here ships with the core.';
|
|
168
259
|
}
|
|
169
260
|
|
|
170
|
-
|
|
171
|
-
|
|
261
|
+
wire();
|
|
262
|
+
renderRows();
|
|
172
263
|
}
|
|
173
264
|
|
|
174
|
-
async function onToggle(
|
|
175
|
-
const
|
|
176
|
-
const card = btn.closest('.mod-card');
|
|
177
|
-
const key = card && card.dataset.key;
|
|
265
|
+
async function onToggle(btn) {
|
|
266
|
+
const key = btn.dataset.key;
|
|
178
267
|
if (!key) return;
|
|
179
|
-
const next = btn.
|
|
268
|
+
const next = btn.getAttribute('aria-pressed') !== 'true';
|
|
180
269
|
btn.disabled = true;
|
|
181
270
|
try {
|
|
182
271
|
const res = await api.request('POST', `${API}/modules/${encodeURIComponent(key)}/enable`, { body: { enabled: next } });
|
|
183
272
|
const body = res.data || {};
|
|
184
273
|
if (!res.ok) throw new Error(body.message || `enable → ${res.status}`);
|
|
185
|
-
|
|
186
|
-
|
|
274
|
+
const m = allModules.find((x) => x.key === key);
|
|
275
|
+
if (m) m.enabled = next;
|
|
276
|
+
btn.setAttribute('aria-pressed', next ? 'true' : 'false');
|
|
277
|
+
btn.textContent = next ? 'enabled' : 'disabled';
|
|
278
|
+
const figs = $('#mod-figs');
|
|
279
|
+
if (figs) figs.innerHTML = figsHTML();
|
|
280
|
+
renderRows();
|
|
187
281
|
toast(body.note || (next ? `${key} enabled.` : `${key} disabled.`), { duration: 3200 });
|
|
188
282
|
} catch (err) {
|
|
189
283
|
console.error('[modules] toggle failed', err);
|
|
@@ -193,8 +287,7 @@
|
|
|
193
287
|
}
|
|
194
288
|
}
|
|
195
289
|
|
|
196
|
-
async function onSubmit(
|
|
197
|
-
const btn = ev.currentTarget;
|
|
290
|
+
async function onSubmit(btn) {
|
|
198
291
|
const key = btn.dataset.submitKey;
|
|
199
292
|
try {
|
|
200
293
|
const res = await api.request('POST', `${API}/modules/${encodeURIComponent(key)}/submit`);
|
|
@@ -217,12 +310,11 @@
|
|
|
217
310
|
const res = await api.request('GET', `${API}/modules`);
|
|
218
311
|
if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return; }
|
|
219
312
|
if (!res.ok) throw new Error(`modules → ${res.status}`);
|
|
220
|
-
|
|
221
|
-
render(data);
|
|
313
|
+
render(res.data);
|
|
222
314
|
} catch (err) {
|
|
223
315
|
console.error('[modules] failed to load', err);
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
316
|
+
const list = $('#core-grid');
|
|
317
|
+
if (list) list.innerHTML = `<li>${kit.emptyStateHtml('Could not load modules. Return to the hall and try again.')}</li>`;
|
|
226
318
|
toast('Could not load modules.', { duration: 3200 });
|
|
227
319
|
}
|
|
228
320
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_": "Modules — the
|
|
2
|
+
"_": "Modules — the catalog as ONE panel of oversight rows (task 1003472): the kit filter bar, the figure band, two groups under heads. The empty state matters more than the populated one: a filter that matches nothing is the state a browse page spends real time in. Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once; the redesign added the `lens` and `details` states. Populated look: the hall-preview harness (scripts/hall-preview/fixtures/modules.json carries eighteen modules, one built by the project).",
|
|
3
3
|
"page": "/builders/modules",
|
|
4
4
|
"surface": "hall-ui",
|
|
5
5
|
"stub": {
|
|
@@ -46,6 +46,10 @@
|
|
|
46
46
|
"search-empty": {
|
|
47
47
|
"auth": true,
|
|
48
48
|
"actions": [
|
|
49
|
+
[
|
|
50
|
+
"wait",
|
|
51
|
+
600
|
|
52
|
+
],
|
|
49
53
|
[
|
|
50
54
|
"fill",
|
|
51
55
|
"#mod-search",
|
|
@@ -57,6 +61,49 @@
|
|
|
57
61
|
]
|
|
58
62
|
]
|
|
59
63
|
},
|
|
64
|
+
"lens": {
|
|
65
|
+
"auth": true,
|
|
66
|
+
"actions": [
|
|
67
|
+
[
|
|
68
|
+
"wait",
|
|
69
|
+
600
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"focus",
|
|
73
|
+
"#mod-state"
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
"press",
|
|
77
|
+
"ArrowDown"
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
"wait",
|
|
81
|
+
300
|
|
82
|
+
]
|
|
83
|
+
],
|
|
84
|
+
"expect": {
|
|
85
|
+
"visible": [
|
|
86
|
+
"#mod-state"
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"details": {
|
|
91
|
+
"auth": true,
|
|
92
|
+
"actions": [
|
|
93
|
+
[
|
|
94
|
+
"wait",
|
|
95
|
+
600
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
"clickIfVisible",
|
|
99
|
+
"#core-grid .mod-row:first-child .mod-more > summary"
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"wait",
|
|
103
|
+
300
|
|
104
|
+
]
|
|
105
|
+
]
|
|
106
|
+
},
|
|
60
107
|
"out": {
|
|
61
108
|
"auth": false
|
|
62
109
|
}
|
|
@@ -3057,18 +3057,18 @@ html.palette-open { overflow: hidden; }
|
|
|
3057
3057
|
The state colours are the hall's fixed verification vocabulary, never the
|
|
3058
3058
|
world's warm accent states (ADR 0196 §2): a status must not re-tint when an
|
|
3059
3059
|
instance re-brands. */
|
|
3060
|
-
/* The
|
|
3061
|
-
government, harbor, gate
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3060
|
+
/* The names below are the SAME pill, found by the task 1003307 audit:
|
|
3061
|
+
government, harbor, gate and watch each invented one because the shell had
|
|
3062
|
+
none to take (modules had one too; its rows take .fact-pill by name since
|
|
3063
|
+
task 1003472). They are aliased rather than renamed because renaming would
|
|
3064
|
+
mean editing the JS row builders, and a class name is not what was wrong —
|
|
3065
|
+
the duplicated, hand-rolled label voice was. Their own state modifiers stay
|
|
3066
|
+
in their own sheets. */
|
|
3066
3067
|
.fact-pill,
|
|
3067
3068
|
.gov-pill,
|
|
3068
3069
|
.harbor-pill,
|
|
3069
3070
|
.gate-pill,
|
|
3070
|
-
.badge
|
|
3071
|
-
.mod-chip {
|
|
3071
|
+
.badge {
|
|
3072
3072
|
display: inline-flex;
|
|
3073
3073
|
align-items: center;
|
|
3074
3074
|
min-height: 24px;
|
|
@@ -117,9 +117,13 @@
|
|
|
117
117
|
const msOf = (iso) => { const t = iso ? Date.parse(iso) : NaN; return Number.isNaN(t) ? 0 : t; };
|
|
118
118
|
const taskActivityMs = (t) => Math.max(msOf(t.updated_at), msOf(t.shipped_at));
|
|
119
119
|
|
|
120
|
-
// The staleness buckets both boards offer. A CLIENT-side vocabulary
|
|
121
|
-
// project's configured stale timer
|
|
122
|
-
//
|
|
120
|
+
// The staleness buckets both boards offer. A CLIENT-side vocabulary over TASKS,
|
|
121
|
+
// deliberately NOT the project's configured stale-claim timer — that one lives in
|
|
122
|
+
// src/stale-timer.js, counts hours rather than days, is about a CLAIM going silent,
|
|
123
|
+
// and is what the sweeper enforces and the Archon roster reports (task 1003476).
|
|
124
|
+
// These are lenses for a person browsing a board; that is the machine's rule. Two
|
|
125
|
+
// different questions, so two vocabularies — but only one of them may be a number
|
|
126
|
+
// typed twice, which is the bug 1003476 closed.
|
|
123
127
|
const STALE_CHOICES = [
|
|
124
128
|
{ value: '', label: '— any age —' },
|
|
125
129
|
{ value: '14', label: 'untouched 14+ days' },
|
|
@@ -562,21 +562,29 @@ async function getClaimPublishedSha(taskId) {
|
|
|
562
562
|
// time_to_first_ship_minutes (NULL until they've shipped)
|
|
563
563
|
// - claims: active_claim_count, stale_claim
|
|
564
564
|
//
|
|
565
|
-
// "stale_claim" mirrors the stale-claim-release sweeper definition
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
565
|
+
// "stale_claim" mirrors the stale-claim-release sweeper's definition: a
|
|
566
|
+
// still-open claim where either the last session_pulses row is older than
|
|
567
|
+
// the stale threshold, or there's no pulse row at all AND claimed_at is
|
|
568
|
+
// older than the threshold.
|
|
569
|
+
//
|
|
570
|
+
// THE THRESHOLD IS THE PROJECT'S, NOT THIS ROUTE'S (task 1003476, goal
|
|
571
|
+
// 1000072). It used to default to a hardcoded 24h "to match this task's
|
|
572
|
+
// spec" while the sweeper acted on GDS_STALE_CLAIM_HOURS (6h) — so for
|
|
573
|
+
// eighteen hours after a claim had been auto-released, tagged
|
|
574
|
+
// [auto-released:stale] and handed back to the queue, this roster still
|
|
575
|
+
// showed it healthy. The roster surfaces and the sweeper acts, but they
|
|
576
|
+
// must answer the same question the same way, so both now read
|
|
577
|
+
// src/stale-timer.js. An explicit `staleHours` still overrides, because
|
|
578
|
+
// "what would 48h show me?" is a lens a person points at the roster and
|
|
579
|
+
// not a reconfiguration of the project.
|
|
572
580
|
//
|
|
573
581
|
// Returns: array of builder roster rows, ordered by:
|
|
574
582
|
// 1. inactive builders sink to the bottom (status='inactive' last)
|
|
575
583
|
// 2. still-onboarding (first_ship_at IS NULL) above shipped builders
|
|
576
584
|
// 3. then by created_at DESC inside each group (newest first)
|
|
577
585
|
|
|
578
|
-
async function getBuildersRoster({ staleHours
|
|
579
|
-
const hours = Number(staleHours);
|
|
586
|
+
async function getBuildersRoster({ staleHours } = {}) {
|
|
587
|
+
const hours = Number(staleHours ?? api.resolveStaleClaimHours().hours);
|
|
580
588
|
if (!Number.isFinite(hours) || hours <= 0) {
|
|
581
589
|
throw new Error('getBuildersRoster: staleHours must be a positive number');
|
|
582
590
|
}
|
|
@@ -629,9 +637,12 @@ async function getBuildersRoster({ staleHours = 24 } = {}) {
|
|
|
629
637
|
// signals, so the Archon can pick the right one to release. Used by
|
|
630
638
|
// the management panel's per-row "release stale claim" action.
|
|
631
639
|
|
|
632
|
-
|
|
640
|
+
// The threshold defaults to the PROJECT's stale-claim timer, not to a number of its
|
|
641
|
+
// own (task 1003476) — a claim the roster called stale must still be stale one click
|
|
642
|
+
// later, in the view an Archon opens to release it.
|
|
643
|
+
async function getActiveClaimsForBuilderWithStale(builderId, { staleHours } = {}) {
|
|
633
644
|
const builder = Number(builderId);
|
|
634
|
-
const hours = Number(staleHours);
|
|
645
|
+
const hours = Number(staleHours ?? api.resolveStaleClaimHours().hours);
|
|
635
646
|
if (!Number.isFinite(builder)) {
|
|
636
647
|
throw new Error('getActiveClaimsForBuilderWithStale: builderId must be numeric');
|
|
637
648
|
}
|
|
@@ -39,7 +39,17 @@
|
|
|
39
39
|
// getStaleClaimsForBuilder().
|
|
40
40
|
// -------------------------------------------------------------------------
|
|
41
41
|
|
|
42
|
-
const
|
|
42
|
+
const api = require('../../src/module-api');
|
|
43
|
+
|
|
44
|
+
const { pool } = api;
|
|
45
|
+
|
|
46
|
+
// The project's stale-claim timer, in minutes (task 1003476, goal 1000072). These
|
|
47
|
+
// helpers used to default to a hardcoded 30 minutes, which was neither the sweeper's
|
|
48
|
+
// 6h nor the roster's 24h — a third answer to one question, reachable by any caller
|
|
49
|
+
// that omitted the argument. There is one answer now, and it is the project's.
|
|
50
|
+
// Resolved per call, never at require time: an env change must reach a long-running
|
|
51
|
+
// process on its next sweep, not on its next restart.
|
|
52
|
+
const projectThresholdMin = () => api.staleClaimMinutes(api.resolveStaleClaimHours().hours);
|
|
43
53
|
|
|
44
54
|
// Record a heartbeat for a claim. Idempotent UPSERT — the PK is
|
|
45
55
|
// claim_id so repeated pulses just bump last_pulse_at.
|
|
@@ -77,8 +87,8 @@ async function recordPulse(claimId, builderId) {
|
|
|
77
87
|
// This function is READ-ONLY by design (see open question #1 above);
|
|
78
88
|
// the write-side marker is deferred to a follow-up task that owns
|
|
79
89
|
// the migration.
|
|
80
|
-
async function sweepStaleClaims({ thresholdMin
|
|
81
|
-
const threshold = Number(thresholdMin);
|
|
90
|
+
async function sweepStaleClaims({ thresholdMin } = {}) {
|
|
91
|
+
const threshold = Number(thresholdMin ?? projectThresholdMin());
|
|
82
92
|
if (!Number.isFinite(threshold) || threshold <= 0) {
|
|
83
93
|
throw new Error('sweepStaleClaims: thresholdMin must be a positive number');
|
|
84
94
|
}
|
|
@@ -110,9 +120,9 @@ async function sweepStaleClaims({ thresholdMin = 30 } = {}) {
|
|
|
110
120
|
// Today, "stale" is computed the same way as sweepStaleClaims() —
|
|
111
121
|
// once a marker column exists (follow-up a), this can pivot to read
|
|
112
122
|
// claims.pulse_lost_at IS NOT NULL instead.
|
|
113
|
-
async function getStaleClaimsForBuilder(builderId, { thresholdMin
|
|
123
|
+
async function getStaleClaimsForBuilder(builderId, { thresholdMin } = {}) {
|
|
114
124
|
const builder = Number(builderId);
|
|
115
|
-
const threshold = Number(thresholdMin);
|
|
125
|
+
const threshold = Number(thresholdMin ?? projectThresholdMin());
|
|
116
126
|
if (!Number.isFinite(builder)) {
|
|
117
127
|
throw new Error('getStaleClaimsForBuilder: builderId must be numeric');
|
|
118
128
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.471",
|
|
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.471",
|
|
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.471",
|
|
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",
|