@bongos/core 1.19.878 → 1.19.880

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.
Files changed (42) hide show
  1. package/.bongos-core.json +43 -83
  2. package/docs/copy-inventory.md +368 -456
  3. package/docs/copy-registry.json +457 -1344
  4. package/docs/file-map.md +2 -2
  5. package/docs/module-api-changelog.md +4 -0
  6. package/modules/hall-ui/module.json +1 -1
  7. package/modules/hall-ui/public/builders.js +1 -13
  8. package/modules/hall-ui/public/dom-utils.js +22 -6
  9. package/modules/hall-ui/public/goals-render.js +8 -3
  10. package/modules/hall-ui/public/hall-render.js +2 -18
  11. package/modules/hall-ui/public/index.html +0 -3
  12. package/modules/hall-ui/public/panel.css +8 -9
  13. package/modules/hall-ui/public/settings.css +0 -5
  14. package/modules/hall-ui/public/settings.html +0 -29
  15. package/modules/hall-ui/public/settings.js +1 -26
  16. package/modules/hall-ui/public/shell.js +0 -1
  17. package/modules/hall-ui/public/style.css +0 -44
  18. package/modules/hall-ui/public/task.js +6 -2
  19. package/modules/hall-ui/public/watch.html +0 -7
  20. package/modules/hall-ui/public/watch.js +1 -37
  21. package/modules/hall-ui/public/work.js +4 -2
  22. package/package-lock.json +2 -2
  23. package/package.json +1 -1
  24. package/src/bongos/serve-internal.js +11 -11
  25. package/src/module-api.js +1 -1
  26. package/tests/government_page_gates.mjs +1 -1
  27. package/tests/hall_landing_boot.mjs +1 -1
  28. package/tests/hall_nav.mjs +1 -1
  29. package/tests/hall_oversight_world.mjs +5 -5
  30. package/tests/hall_page_gate_map.mjs +11 -2
  31. package/tests/hall_record_world.mjs +5 -0
  32. package/tests/hall_settings_world.mjs +21 -18
  33. package/tests/kind_axis_unset.mjs +195 -0
  34. package/modules/hall-ui/public/box-panel.js +0 -456
  35. package/modules/hall-ui/public/harbor.css +0 -9
  36. package/modules/hall-ui/public/harbor.html +0 -69
  37. package/modules/hall-ui/public/harbor.js +0 -367
  38. package/modules/hall-ui/public/harbor.states.json +0 -77
  39. package/modules/hall-ui/public/pair.css +0 -23
  40. package/modules/hall-ui/public/pair.html +0 -58
  41. package/modules/hall-ui/public/pair.js +0 -132
  42. package/modules/hall-ui/public/pair.states.json +0 -14
@@ -1,367 +0,0 @@
1
- // public-builders/harbor.js — the Harbor (/builders/harbor).
2
- //
3
- // Metic+ dev-box management view (idea 257 / task 1163; Archon-only until ADR 0157
4
- // moved `box.fleet.manage` to the working rank). Every builder's dev box is one
5
- // oversight row in a panel, grouped by lifecycle (task 1003469); a Metic or Archon
6
- // can close any box and block a builder from opening new ones.
7
- //
8
- // Like the Watch page, this reads /api/bongos/me to learn the viewer's rank and
9
- // shows a restricted notice below the Metic floor — DEFENCE IN DEPTH only. The data +
10
- // mutation endpoints (/boxes, /boxes/:id/close, /boxes/:id/block) are ALSO gated
11
- // server-side on `box.fleet.manage` (ADR 0016), and the page itself is gated by
12
- // serve-internal.js requireGovernmentPage — whose own floor is `page.view.government`,
13
- // metic since ADR 0157. On a 401 it redirects into the web sign-in flow.
14
-
15
- (() => {
16
- 'use strict';
17
-
18
- const API = '/api/bongos';
19
- // Generated typed client (task 1996 / R09), served as window.BongosClient by
20
- // /lib/bongos-client.js. baseUrl:'' passes the full `${API}${path}` URLs below
21
- // through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
22
- const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
23
- const { escapeHtml, fmtInt } = window.OTB;
24
- const { emptyStateHtml } = window.OTBKit;
25
- const rankLabel = (rank) => window.OTB.rankLabel(rank, { empty: '' });
26
- const toast = (msg) => window.OTB.toast(msg);
27
-
28
- // States that hold a droplet/snapshot worth reclaiming — mirrors the server's
29
- // boxes.canTransition('deprovision', …). A box in one of these can be
30
- // closed; the rest (none/destroyed/provisioning) have nothing to deprovision.
31
- const CLOSEABLE = new Set(['active', 'parking', 'parked', 'waking', 'error']);
32
-
33
- // ---- fetch helpers (mirror watch.js) -------------------------------------
34
-
35
- async function fetchJson(path) {
36
- const res = await api.request('GET', `${API}${path}`);
37
- if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); throw { kind: '401' }; }
38
- if (res.status === 403) { throw { kind: '403' }; }
39
- if (!res.ok) throw new Error(`${path} → ${res.status}`);
40
- return res.data;
41
- }
42
-
43
- async function sendJson(method, path, body) {
44
- const res = await api.request(method, `${API}${path}`, { body: body || {} });
45
- if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); throw { kind: '401' }; }
46
- const data = res.data || {};
47
- if (!res.ok) throw new Error(data.message || data.error || `${path} → ${res.status}`);
48
- return data;
49
- }
50
-
51
- // ---- formatters ----------------------------------------------------------
52
-
53
- // ISO → "3h 12m" / "2d 4h" / "8m" elapsed-since. "—" for empty/bad input.
54
- function fmtAgo(iso) {
55
- if (!iso) return '—';
56
- const t = new Date(iso).getTime();
57
- if (!Number.isFinite(t)) return '—';
58
- let s = Math.max(0, Math.round((Date.now() - t) / 1000));
59
- const d = Math.floor(s / 86400); s -= d * 86400;
60
- const h = Math.floor(s / 3600); s -= h * 3600;
61
- const m = Math.floor(s / 60);
62
- if (d > 0) return `${d}d ${h}h`;
63
- if (h > 0) return `${h}h ${m}m`;
64
- return `${m}m`;
65
- }
66
-
67
- function fmtUsd(n) {
68
- const v = Number(n);
69
- if (!Number.isFinite(v)) return '$0.00';
70
- return `$${v.toFixed(2)}`;
71
- }
72
-
73
- // The lifecycle state as a hairline pill in the state's colour — the shell's
74
- // .fact-pill states, never a coloured band on the row (task 1003469 retired the
75
- // 4px "roof" strip each card wore).
76
- function statePill(state) {
77
- const s = state || 'none';
78
- const mod = s === 'active' ? 'ok'
79
- : (s === 'provisioning' || s === 'waking' || s === 'parking') ? 'warn'
80
- : s === 'error' ? 'danger'
81
- : 'quiet';
82
- return `<span class="fact-pill fact-pill--${mod}">${escapeHtml(s)}</span>`;
83
- }
84
-
85
- // ---- modal ---------------------------------------------------------------
86
-
87
- function closeModal() {
88
- const root = document.getElementById('harbor-modal-root');
89
- if (root) root.innerHTML = '';
90
- }
91
-
92
- // Build the confirm dialog on the shell's --z-modal layer: the oversight
93
- // family's .ov-modal (oversight.css). `bodyHtml` is optional extra HTML
94
- // (inputs); onConfirm receives the dialog so the caller can read them.
95
- function openModal({ title, bodyHtml, confirmLabel, onConfirm }) {
96
- const root = document.getElementById('harbor-modal-root');
97
- root.innerHTML = '';
98
- const bg = document.createElement('div');
99
- bg.className = 'ov-modal-bg';
100
- bg.innerHTML =
101
- `<div class="ov-modal" role="dialog" aria-modal="true" aria-labelledby="harbor-modal-h">
102
- <h2 class="scroll__h" id="harbor-modal-h">${escapeHtml(title)}</h2>
103
- ${bodyHtml}
104
- <div class="ov-modal__actions">
105
- <button type="button" class="btn-ghost" data-act="cancel">Cancel</button>
106
- <button type="button" class="btn-accent" data-act="confirm">${escapeHtml(confirmLabel)}</button>
107
- </div>
108
- </div>`;
109
- bg.addEventListener('click', (e) => { if (e.target === bg) closeModal(); });
110
- bg.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
111
- bg.querySelector('[data-act=cancel]').addEventListener('click', closeModal);
112
- bg.querySelector('[data-act=confirm]').addEventListener('click', () => onConfirm(bg));
113
- root.appendChild(bg);
114
- const first = bg.querySelector('textarea, input, [data-act=confirm]');
115
- if (first) first.focus();
116
- }
117
-
118
- // ---- actions -------------------------------------------------------------
119
-
120
- function confirmClose(box) {
121
- const who = box.builder.github_login || `#${box.builder.id}`;
122
- const verb = box.state === 'parked' ? 'destroy the snapshot of' : 'close';
123
- openModal({
124
- title: `Close ${who}'s box?`,
125
- bodyHtml: `<p>This will ${verb} <strong>${escapeHtml(who)}</strong>'s dev box (state: ${escapeHtml(box.state)}). Any uncommitted work on that box is lost. The box is destroyed; the builder can open a fresh one unless they are also blocked.</p>
126
- <p>A fresh box has a <strong>new SSH identity</strong>. If their Claude Desktop later says &ldquo;Host denied&rdquo; or asks for a password, tell them to fully restart Desktop (it re-pins) or press Repair — not to keep rebuilding, which only rotates the identity again.</p>`,
127
- confirmLabel: 'Close box',
128
- onConfirm: async () => {
129
- try {
130
- const r = await sendJson('POST', `/boxes/${box.builder.id}/close`, {});
131
- toast(r.message || 'Close queued.');
132
- closeModal();
133
- load();
134
- } catch (err) {
135
- if (err && err.kind === '401') return;
136
- toast(`Could not close: ${err.message || err}`);
137
- }
138
- },
139
- });
140
- }
141
-
142
- function confirmBlock(box) {
143
- const who = box.builder.github_login || `#${box.builder.id}`;
144
- const hasLiveBox = CLOSEABLE.has(box.state);
145
- openModal({
146
- title: `Bar ${who} from opening boxes?`,
147
- bodyHtml:
148
- `<p>${escapeHtml(who)} will not be able to open or wake a dev box until you unblock them. A box they already have running is left alone unless you also close it below.</p>
149
- <label for="harbor-block-reason">Reason (shown to the builder)</label>
150
- <textarea id="harbor-block-reason" rows="2" placeholder="e.g. runaway cost — paused pending review"></textarea>
151
- ${hasLiveBox ? `<label class="ov-modal__check"><input type="checkbox" id="harbor-block-close"> Also close the box they have running now (state: ${escapeHtml(box.state)})</label>` : ''}`,
152
- confirmLabel: 'Block builder',
153
- onConfirm: async (modal) => {
154
- const reason = (modal.querySelector('#harbor-block-reason') || {}).value || '';
155
- const closeEl = modal.querySelector('#harbor-block-close');
156
- try {
157
- const r = await sendJson('PATCH', `/boxes/${box.builder.id}/block`, {
158
- blocked: true,
159
- reason: reason.trim() || undefined,
160
- close_current: !!(closeEl && closeEl.checked),
161
- });
162
- toast(r.close_queued ? 'Builder blocked — running box will be closed.' : 'Builder blocked.');
163
- closeModal();
164
- load();
165
- } catch (err) {
166
- if (err && err.kind === '401') return;
167
- toast(`Could not block: ${err.message || err}`);
168
- }
169
- },
170
- });
171
- }
172
-
173
- function confirmUnblock(box) {
174
- const who = box.builder.github_login || `#${box.builder.id}`;
175
- openModal({
176
- title: `Lift the block on ${who}?`,
177
- bodyHtml: `<p>${escapeHtml(who)} will be able to open dev boxes again.</p>`,
178
- confirmLabel: 'Unblock',
179
- onConfirm: async () => {
180
- try {
181
- await sendJson('PATCH', `/boxes/${box.builder.id}/block`, { blocked: false });
182
- toast('Block lifted.');
183
- closeModal();
184
- load();
185
- } catch (err) {
186
- if (err && err.kind === '401') return;
187
- toast(`Could not unblock: ${err.message || err}`);
188
- }
189
- },
190
- });
191
- }
192
-
193
- // ---- render --------------------------------------------------------------
194
-
195
- // One box, one oversight row (oversight.css .ov-row): who and their rank, the
196
- // state pill, then the facts a fleet operator scans for — make, address, how
197
- // long it has been up, how long idle, what it has cost — the barred note when
198
- // there is one, and the two actions on the right.
199
- function renderRow(box, costByBuilder) {
200
- const blocked = !!(box.blocked && box.blocked.is);
201
- const who = escapeHtml(box.builder.display_name || box.builder.github_login || `#${box.builder.id}`);
202
- const login = box.builder.github_login ? `<span class="ov-mono">@${escapeHtml(box.builder.github_login)}</span>` : '';
203
- const rank = rankLabel(box.builder.rank);
204
- const cost = costByBuilder.get(String(box.builder.id));
205
-
206
- const facts = [statePill(box.state)];
207
- if (blocked) facts.push('<span class="fact-pill fact-pill--danger">blocked</span>');
208
- if (rank) facts.push(`<span class="fact-chip">${escapeHtml(rank)}</span>`);
209
- if (box.region || box.size_slug) {
210
- facts.push(`<span class="ov-fact">${escapeHtml([box.region, box.size_slug].filter(Boolean).join(' · '))}</span>`);
211
- }
212
- if (box.ip && box.state === 'active') facts.push(`<span class="ov-mono">${escapeHtml(box.ip)}</span>`);
213
- if (box.state === 'active') {
214
- facts.push(`<span class="ov-fact">up <b>${fmtAgo(box.provisioned_at)}</b></span>`);
215
- facts.push(`<span class="ov-fact">idle <b>${fmtAgo(box.last_activity_at)}</b>${box.claude_active ? ' · claude live' : ''}</span>`);
216
- } else if (box.state === 'parked') {
217
- facts.push(`<span class="ov-fact">parked <b>${fmtAgo(box.parked_at)}</b> ago</span>`);
218
- } else if (CLOSEABLE.has(box.state) || box.state === 'provisioning') {
219
- facts.push(`<span class="ov-fact">in this state <b>${fmtAgo(box.last_activity_at || box.provisioned_at)}</b></span>`);
220
- }
221
- facts.push(`<span class="ov-fact">cost <b>${cost != null ? fmtUsd(cost) : fmtUsd(box.compute_cost_usd)}</b></span>`);
222
-
223
- const barred = blocked
224
- ? `<p class="harbor-barred">Barred from opening boxes${box.blocked.reason ? `: ${escapeHtml(box.blocked.reason)}` : '.'}</p>`
225
- : '';
226
-
227
- const closeable = CLOSEABLE.has(box.state);
228
- const actions = [];
229
- if (closeable) {
230
- actions.push(`<button type="button" class="btn-ghost btn-ghost--danger" data-act="close">${box.state === 'parked' ? 'Destroy' : 'Close'}</button>`);
231
- }
232
- actions.push(blocked
233
- ? '<button type="button" class="btn-ghost" data-act="unblock">Unblock</button>'
234
- : '<button type="button" class="btn-ghost" data-act="block">Block</button>');
235
-
236
- const el = document.createElement('li');
237
- el.className = 'ov-row';
238
- el.innerHTML =
239
- `<div class="ov-row__main">
240
- <span class="ov-row__title">${who}${login}</span>
241
- <div class="ov-row__facts">${facts.join('')}</div>
242
- ${barred}
243
- </div>
244
- <div class="ov-row__right"><div class="ov-row__actions">${actions.join('')}</div></div>`;
245
-
246
- const closeBtn = el.querySelector('[data-act=close]');
247
- if (closeBtn) closeBtn.addEventListener('click', () => confirmClose(box));
248
- const blockBtn = el.querySelector('[data-act=block]');
249
- if (blockBtn) blockBtn.addEventListener('click', () => confirmBlock(box));
250
- const unblockBtn = el.querySelector('[data-act=unblock]');
251
- if (unblockBtn) unblockBtn.addEventListener('click', () => confirmUnblock(box));
252
- return el;
253
- }
254
-
255
- // The fleet reads in three runs — running, parked or in transition, no box —
256
- // so the operator's eye lands on the boxes that cost money first. Within a run
257
- // the API's order holds (most recently touched first). An empty run is not
258
- // drawn: a head over nothing is a promise the page cannot keep.
259
- const GROUPS = [
260
- { name: 'Running', test: (b) => b.state === 'active' },
261
- { name: 'Parked or in transition', test: (b) => CLOSEABLE.has(b.state) || b.state === 'provisioning' },
262
- { name: 'No box', test: () => true },
263
- ];
264
-
265
- function renderDock(boxes, costByBuilder) {
266
- const dock = document.getElementById('harbor-dock');
267
- dock.innerHTML = '';
268
- let left = boxes.slice();
269
- for (const g of GROUPS) {
270
- const mine = left.filter(g.test);
271
- left = left.filter((b) => !g.test(b));
272
- if (!mine.length) continue;
273
- const sec = document.createElement('section');
274
- sec.className = 'ov-group';
275
- sec.innerHTML = `<div class="ov-group__head"><span class="ov-group__name">${escapeHtml(g.name)}</span><span class="ov-group__n">${fmtInt(mine.length)}</span></div>`;
276
- const list = document.createElement('ul');
277
- list.className = 'ov-rows';
278
- for (const box of mine) list.appendChild(renderRow(box, costByBuilder));
279
- sec.appendChild(list);
280
- dock.appendChild(sec);
281
- }
282
- }
283
-
284
- // The glass strip above the panel: the shell's .stat-row, each tile a figure
285
- // with the one clause that makes it a fact rather than a number.
286
- function renderSummary(boxes, totalUsd) {
287
- const afloat = boxes.filter((b) => CLOSEABLE.has(b.state)).length;
288
- const active = boxes.filter((b) => b.state === 'active').length;
289
- const live = boxes.filter((b) => b.state === 'active' && b.claude_active).length;
290
- const blocked = boxes.filter((b) => b.blocked && b.blocked.is).length;
291
- const tiles = [
292
- ['Boxes live', fmtInt(afloat), `of ${fmtInt(boxes.length)} builder${boxes.length === 1 ? '' : 's'}`],
293
- ['Active now', fmtInt(active), `${fmtInt(live)} with claude live`],
294
- ['Blocked builders', fmtInt(blocked), blocked ? 'barred from opening a box' : 'nobody barred'],
295
- ['Total cost', fmtUsd(totalUsd), 'across every box'],
296
- ];
297
- document.getElementById('harbor-summary').innerHTML = tiles.map(([label, num, sub]) =>
298
- `<div class="stat-tile"><span class="stat-tile__label">${escapeHtml(label)}</span><span class="stat-tile__num">${num}</span><span class="stat-tile__sub">${escapeHtml(sub)}</span></div>`).join('');
299
- }
300
-
301
- async function load() {
302
- const dock = document.getElementById('harbor-dock');
303
- let boxesResp, ledger;
304
- try {
305
- [boxesResp, ledger] = await Promise.all([
306
- fetchJson('/boxes'),
307
- fetchJson('/boxes/cost-ledger').catch(() => ({ totals_by_builder: [] })),
308
- ]);
309
- } catch (err) {
310
- if (err && err.kind === '401') return;
311
- if (err && err.kind === '403') { showSealed(); return; }
312
- dock.innerHTML = emptyStateHtml('Could not load dev boxes just now.');
313
- return;
314
- }
315
-
316
- const boxes = boxesResp.boxes || [];
317
- const costByBuilder = new Map();
318
- let totalUsd = 0;
319
- for (const row of (ledger.totals_by_builder || [])) {
320
- const usd = Number(row.total_usd) || 0;
321
- costByBuilder.set(String(row.builder_id), usd);
322
- totalUsd += usd;
323
- }
324
-
325
- renderSummary(boxes, totalUsd);
326
-
327
- if (boxes.length === 0) {
328
- dock.innerHTML = emptyStateHtml('No dev boxes yet. No builder has opened one.');
329
- return;
330
- }
331
- renderDock(boxes, costByBuilder);
332
- }
333
-
334
- function showSealed() {
335
- document.getElementById('harbor-sealed').hidden = false;
336
- document.getElementById('harbor-body').hidden = true;
337
- document.getElementById('harbor-sub').textContent = 'Restricted.';
338
- // The floor in the pack's own words (ADR 0157: metic), never a rank spelled into the source.
339
- const body = document.getElementById('harbor-sealed-body');
340
- if (body) body.textContent = `Available from the ${rankLabel('metic')} rank up.`;
341
- }
342
-
343
- async function init() {
344
- // Confirm the viewer's rank for a friendly restricted notice; the server gates
345
- // the page + every endpoint regardless (this is cosmetic defence-in-depth).
346
- let me;
347
- try {
348
- me = await fetchJson('/me');
349
- } catch (err) {
350
- if (err && err.kind === '401') return; // redirected to sign-in
351
- if (err && err.kind === '403') { showSealed(); return; }
352
- me = null;
353
- }
354
- const rank = (me && (me.builder || me).rank || '').toLowerCase();
355
- // ADR 0157: Harbor sits at the `page.view.government` floor (metic), and the fleet
356
- // data it reads is `box.fleet.manage` (also metic now). Cosmetic —
357
- // requireGovernmentPage + the route gates are the wall; this just picks body vs
358
- // sealed notice.
359
- if (rank !== 'metic' && rank !== 'archon') { showSealed(); return; }
360
-
361
- document.getElementById('harbor-sub').textContent = 'Each builder’s dev box at a glance.';
362
- document.getElementById('harbor-body').hidden = false;
363
- await load();
364
- }
365
-
366
- init();
367
- })();
@@ -1,77 +0,0 @@
1
- {
2
- "_": "Harbor — the dev-box dock behind requireGovernmentPage. Its confirm modal is the reason this page needed the shell to grow a --z-modal layer (task 1003307), so the modal is rendered, not assumed. Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once.",
3
- "page": "/builders/harbor",
4
- "surface": "hall-ui",
5
- "stub": {
6
- "prefix": "/builders/"
7
- },
8
- "modeQuery": false,
9
- "_feeds": "The kit's own stub carries no feed for this page, so against it the page renders its EMPTY and SKELETON states — which is the point: those are states the floors must hold in. Against the hall-preview harness the same states render populated. Every declared 404 below is a fixture the harness does not carry, named rather than hidden.",
10
- "ignoreRequests": [
11
- "/api/(gds|bongos)/me$",
12
- "/api/(gds|bongos)/me/",
13
- "/api/(gds|bongos)/goals",
14
- "/api/(gds|bongos)/inbox/",
15
- "/api/(gds|bongos)/versions",
16
- "/api/(gds|bongos)/boxes",
17
- "/api/(gds|bongos)/provisioning"
18
- ],
19
- "ignoreConsole": [
20
- "/api/(gds|bongos)/",
21
- "\\b401\\b",
22
- "\\b403\\b",
23
- "\\b404\\b",
24
- "\\b501\\b",
25
- "Unauthorized",
26
- "Forbidden",
27
- "Not Found",
28
- "Not Implemented",
29
- "load failed"
30
- ],
31
- "states": {
32
- "dock": {
33
- "auth": true,
34
- "actions": [
35
- [
36
- "wait",
37
- 700
38
- ]
39
- ],
40
- "expect": {
41
- "visible": [
42
- "#harbor-body"
43
- ]
44
- }
45
- },
46
- "out": {
47
- "auth": false
48
- },
49
- "block": {
50
- "auth": true,
51
- "actions": [
52
- [
53
- "wait",
54
- 700
55
- ],
56
- [
57
- "clickIfVisible",
58
- "[data-act=block]"
59
- ],
60
- [
61
- "wait",
62
- 250
63
- ]
64
- ]
65
- }
66
- },
67
- "reducedMotion": {
68
- "auth": true,
69
- "actions": [
70
- [
71
- "wait",
72
- 600
73
- ]
74
- ]
75
- },
76
- "_states": "task 1003469: `block` opens the confirm modal on the first row that has a Block action. clickIfVisible, because the kit's own stub carries no /boxes fixture and renders the empty dock — the modal is measured against the harness, where the fixture fleet renders."
77
- }
@@ -1,23 +0,0 @@
1
- /* pair.css — what the Dev Box pairing page has and the rest of the panel family
2
- does not (task 1003308, goal 1000081; the archetype is panel.css). One panel,
3
- one device, one button. The device is a row of the family with the platform
4
- glyph as its figure; the approve button is the page's one accent door. */
5
-
6
- #pair-scroll { max-width: 640px; }
7
-
8
- .pair-device {
9
- display: flex;
10
- align-items: center;
11
- gap: var(--sp-sm);
12
- margin: var(--sp-sm) 0 var(--sp-md);
13
- padding: var(--sp-sm) 0;
14
- border-top: var(--line) solid var(--rule-soft);
15
- border-bottom: var(--line) solid var(--rule-soft);
16
- }
17
- .pair-device__glyph { width: 40px; font-size: 26px; line-height: 1; text-align: center; color: var(--ink-soft); flex: none; }
18
- .pair-device__name { font-size: 14.5px; font-weight: 500; color: var(--ink); }
19
- .pair-device__meta { margin-top: 2px; font-size: 13.5px; color: var(--ink-soft); }
20
- .pair-code { font-family: var(--font-mono); font-size: 12.5px; letter-spacing: .08em; color: var(--ink); }
21
- .pair-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-xs); }
22
- .pair-actions .pair-status { margin: 0; }
23
- .pair-note { margin: var(--sp-sm) 0 0; max-width: var(--measure); font-size: 13px; color: var(--ink-faint); }
@@ -1,58 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Approve your Dev Box app — {{worldName}}</title>
7
- <meta name="description" content="Approve the Dev Box desktop app's request to sign in as you.">
8
- <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
- <link rel="preconnect" href="https://fonts.googleapis.com">
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
- <!-- The PANEL archetype (task 1003308) before the page's own sheet. -->
13
- <link rel="stylesheet" href="/builders/panel.css">
14
- <link rel="stylesheet" href="/builders/pair.css">
15
- </head>
16
- <body data-page="pair" data-page-title="Device pairing">
17
-
18
- <div class="app">
19
- <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
20
- <div class="app-scrim" id="app-scrim" hidden></div>
21
- <div class="app-body">
22
- <header class="app-topbar" id="app-topbar"></header>
23
- <div class="app-content">
24
- <!-- The page's ONE h1 (one-header-main-h1), in the family's voice; the
25
- sub-line is pair.js's — the request, the approval, or the error. -->
26
- <div class="page-head page-head--room page-head--panel">
27
- <h1 id="pair-h1" class="page-head__h">pairing</h1>
28
- <p class="page-head__sub" id="pair-sub">Reading the request…</p>
29
- </div>
30
- <main id="hall-main">
31
- <section class="scroll" id="pair-scroll" aria-labelledby="pair-h">
32
- <h2 id="pair-h" class="scroll__h">A device is asking to act as you</h2>
33
- <div id="pair-body">
34
- <p class="scroll__lede">Loading…</p>
35
- </div>
36
- <p class="pair-note">
37
- Approve only if you just clicked “Connect” in the Dev Box app on your own
38
- machine. Approving signs that app in as you; the request expires on its own
39
- if you do nothing.
40
- </p>
41
- </section>
42
- </main>
43
- <footer class="app-foot" id="app-foot"></footer>
44
- </div>
45
- </div>
46
- </div>
47
-
48
- <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
49
-
50
- <!-- App shell (sidebar/topbar/footer) — dependency-free, loads FIRST. -->
51
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
52
- <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
53
- <script src="/builders/palette.js" defer></script>
54
- <!-- shared frontend toolkit (window.OTB) — loaded before pair.js, which pulls from it. -->
55
- <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
56
- <script src="/builders/pair.js?v=2026-06-14-domutils"></script>
57
- </body>
58
- </html>
@@ -1,132 +0,0 @@
1
- // public-builders/pair.js — the Dev Box desktop app's approve page (#904,
2
- // ADR 0044). The app opened this URL with ?code=<pair_code>; the signed-in
3
- // builder reviews what asked to connect and clicks ONE button. The server
4
- // gate (requireBuilderPage in server.js) has already bounced unauthenticated
5
- // visitors through web sign-in with this URL — code included — as ?return=.
6
- (() => {
7
- 'use strict';
8
-
9
- const API = '/api/bongos';
10
- // Generated typed client (task 1996 / R09), served as window.BongosClient by
11
- // /lib/bongos-client.js. baseUrl:'' passes the full `${API}/...` URLs below
12
- // through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
13
- const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
14
-
15
- // escapeHtml / $ now come from the shared window.OTB toolkit (dom-utils.js,
16
- // loaded first). The shared $ accepts an optional root arg — a superset of
17
- // this page's prior single-arg form, so call sites are unchanged.
18
- const { escapeHtml, $ } = window.OTB;
19
-
20
- function toAuth() {
21
- const here = window.location.pathname + window.location.search;
22
- window.location.assign(`${API}/auth/web/start?return=${encodeURIComponent(here)}`);
23
- }
24
-
25
- const PLATFORM_LABELS = {
26
- mac: 'macOS', windows: 'Windows', linux: 'Linux', unknown: 'an unknown platform',
27
- };
28
- const PLATFORM_GLYPHS = { mac: '⌘', windows: '⊞', linux: '🐧', unknown: '🖥' };
29
-
30
- function renderError(heading, detail) {
31
- $('#pair-sub').textContent = heading;
32
- $('#pair-body').innerHTML = `
33
- <p class="scroll__lede">${escapeHtml(detail)}</p>
34
- <p class="pair-status pair-status--err">Return to the Dev Box app and click “Connect” again for a fresh code.</p>
35
- `;
36
- }
37
-
38
- function renderApproved(login) {
39
- $('#pair-sub').textContent = 'Approved.';
40
- $('#pair-body').innerHTML = `
41
- <p class="scroll__lede">
42
- The Dev Box app now acts as <strong>@${escapeHtml(login)}</strong>.
43
- Return to the app — it has already noticed. You can close this tab.
44
- </p>
45
- <p class="pair-status pair-status--ok">✓ Approved</p>
46
- `;
47
- }
48
-
49
- function renderRequest(info, login) {
50
- const plat = PLATFORM_LABELS[info.platform] || PLATFORM_LABELS.unknown;
51
- const glyph = PLATFORM_GLYPHS[info.platform] || PLATFORM_GLYPHS.unknown;
52
- $('#pair-sub').textContent = 'One click, and the app can act as you.';
53
- $('#pair-body').innerHTML = `
54
- <p class="scroll__lede">
55
- <strong>${escapeHtml(info.device_name)}</strong> on ${escapeHtml(plat)}
56
- is asking to sign in as <strong>@${escapeHtml(login)}</strong> —
57
- to start and stop your dev box on your behalf.
58
- </p>
59
- <div class="pair-device">
60
- <span class="pair-device__glyph" aria-hidden="true">${glyph}</span>
61
- <div>
62
- <div class="pair-device__name">${escapeHtml(info.device_name)}</div>
63
- <div class="pair-device__meta">${escapeHtml(plat)} · code <span class="pair-code">${escapeHtml(info.pair_code)}</span></div>
64
- </div>
65
- </div>
66
- <div class="pair-actions">
67
- <button type="button" class="pair-approve" id="pair-approve-btn">Approve — sign the app in as me</button>
68
- <span class="pair-status" id="pair-status" role="status" aria-live="polite"></span>
69
- </div>
70
- `;
71
- $('#pair-approve-btn').addEventListener('click', () => approve(info.pair_code, login));
72
- }
73
-
74
- async function approve(code, login) {
75
- const btn = $('#pair-approve-btn');
76
- const status = $('#pair-status');
77
- btn.disabled = true;
78
- status.textContent = 'Approving…';
79
- try {
80
- const res = await api.request('POST', `${API}/auth/app-pair/approve`, { body: { pair_code: code } });
81
- if (res.status === 401) return toAuth();
82
- if (res.status === 404 || res.status === 410) {
83
- return renderError('Request expired.', 'This pairing code is unknown or has expired (codes last ten minutes).');
84
- }
85
- if (res.status === 409) {
86
- return renderError('Already approved.', 'This pairing was already approved once — a code works exactly one time.');
87
- }
88
- if (!res.ok) throw new Error(`approve failed (${res.status})`);
89
- renderApproved(login);
90
- } catch (err) {
91
- btn.disabled = false;
92
- status.textContent = 'Approval failed — try again.';
93
- status.className = 'pair-status pair-status--err';
94
- console.error('[pair]', err);
95
- }
96
- }
97
-
98
- async function load() {
99
- const code = new URLSearchParams(window.location.search).get('code');
100
- if (!code) {
101
- return renderError('No code in this link.', 'The link is missing its pairing code.');
102
- }
103
- let me;
104
- try {
105
- const meRes = await api.request('GET', `${API}/me`);
106
- if (meRes.status === 401) return toAuth();
107
- if (!meRes.ok) throw new Error(`/me ${meRes.status}`);
108
- me = meRes.data;
109
- } catch (err) {
110
- console.error('[pair]', err);
111
- return renderError('Could not reach the server.', 'Could not verify your identity. Reload to try again.');
112
- }
113
- try {
114
- const res = await api.request('GET', `${API}/auth/app-pair/info?code=${encodeURIComponent(code)}`);
115
- if (res.status === 401) return toAuth();
116
- if (res.status === 404) {
117
- return renderError('Request expired.', 'This pairing code is unknown or has expired (codes last ten minutes).');
118
- }
119
- if (!res.ok) throw new Error(`info ${res.status}`);
120
- const info = res.data;
121
- if (info.status !== 'pending') {
122
- return renderError('Already approved.', 'This pairing was already approved once — a code works exactly one time.');
123
- }
124
- renderRequest(info, me.builder.github_login);
125
- } catch (err) {
126
- console.error('[pair]', err);
127
- renderError('Could not reach the server.', 'Could not read the pairing request. Reload to try again.');
128
- }
129
- }
130
-
131
- load();
132
- })();