@bongos/core 1.19.878 → 1.19.879
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 +34 -79
- package/docs/copy-inventory.md +195 -283
- package/docs/copy-registry.json +254 -1141
- package/docs/file-map.md +2 -2
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/module.json +1 -1
- package/modules/hall-ui/public/builders.js +1 -13
- package/modules/hall-ui/public/dom-utils.js +4 -4
- package/modules/hall-ui/public/hall-render.js +2 -18
- package/modules/hall-ui/public/index.html +0 -3
- package/modules/hall-ui/public/panel.css +8 -9
- package/modules/hall-ui/public/settings.css +0 -5
- package/modules/hall-ui/public/settings.html +0 -29
- package/modules/hall-ui/public/settings.js +1 -26
- package/modules/hall-ui/public/shell.js +0 -1
- package/modules/hall-ui/public/style.css +0 -44
- package/modules/hall-ui/public/watch.html +0 -7
- package/modules/hall-ui/public/watch.js +1 -37
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/serve-internal.js +11 -11
- package/src/module-api.js +1 -1
- package/tests/government_page_gates.mjs +1 -1
- package/tests/hall_landing_boot.mjs +1 -1
- package/tests/hall_nav.mjs +1 -1
- package/tests/hall_oversight_world.mjs +5 -5
- package/tests/hall_page_gate_map.mjs +11 -2
- package/tests/hall_settings_world.mjs +21 -18
- package/modules/hall-ui/public/box-panel.js +0 -456
- package/modules/hall-ui/public/harbor.css +0 -9
- package/modules/hall-ui/public/harbor.html +0 -69
- package/modules/hall-ui/public/harbor.js +0 -367
- package/modules/hall-ui/public/harbor.states.json +0 -77
- package/modules/hall-ui/public/pair.css +0 -23
- package/modules/hall-ui/public/pair.html +0 -58
- package/modules/hall-ui/public/pair.js +0 -132
- package/modules/hall-ui/public/pair.states.json +0 -14
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
// public-builders/box-panel.js — the shared dev-box management panel.
|
|
2
|
-
//
|
|
3
|
-
// One self-serve control surface for a builder's OWN hosted dev box: request
|
|
4
|
-
// (provision), open the in-browser terminal, see connection details, and close
|
|
5
|
-
// the box. It is driven by LIVE SERVER STATE (GET /box/me + GET /box/terminal),
|
|
6
|
-
// repainted on every mount and kept fresh by a single self-rescheduling poll
|
|
7
|
-
// while anything is in flight (provisioning / waking / closing / tunnel warming
|
|
8
|
-
// up). The poll stops once the box settles.
|
|
9
|
-
//
|
|
10
|
-
// It is used in two places (#807 / ADR 0040 — moving box mgmt into Settings):
|
|
11
|
-
// * the #777 onboarding welcome panel, in browser-only mode → showSsh:false
|
|
12
|
-
// (online terminal only — a newcomer never sees raw SSH details);
|
|
13
|
-
// * the Settings page → showSsh:true (both the online terminal AND the SSH
|
|
14
|
-
// IP + command, so a builder can drive their box however they like).
|
|
15
|
-
//
|
|
16
|
-
// Behaviour is identical in both; the ONLY difference is whether the SSH rows
|
|
17
|
-
// are shown. The terminal credential is a secret returned only to the box's own
|
|
18
|
-
// builder by GET /box/terminal — shown to that builder, never logged.
|
|
19
|
-
//
|
|
20
|
-
// Usage:
|
|
21
|
-
// const panel = OTBBoxPanel.create({ api: '/api/bongos', showSsh: true, toast });
|
|
22
|
-
// panel.mount(containerEl); // injects markup, wires clicks, paints state
|
|
23
|
-
// panel.refresh(); // repaint from server (e.g. on un-minimise)
|
|
24
|
-
// panel.stop(); // stop polling (e.g. on dismiss / minimise)
|
|
25
|
-
//
|
|
26
|
-
// The container element should carry the `.setup__box` class for styling. mount
|
|
27
|
-
// is re-entrant: calling it again (e.g. after the host re-renders its DOM)
|
|
28
|
-
// re-binds to the new element and cancels any prior poll.
|
|
29
|
-
|
|
30
|
-
(function () {
|
|
31
|
-
'use strict';
|
|
32
|
-
|
|
33
|
-
// escapeHtml now comes from the shared window.OTB toolkit (dom-utils.js,
|
|
34
|
-
// loaded first on every page that mounts this panel). q / getJSON / postJSON
|
|
35
|
-
// and the injected toast stay local — they carry per-panel behaviour.
|
|
36
|
-
const { escapeHtml } = window.OTB;
|
|
37
|
-
|
|
38
|
-
// ttyd's HTTP-basic username is the fixed `otb` user (box-terminal.service:
|
|
39
|
-
// --credential "otb:<pass>"); the password is the long hex `credential`.
|
|
40
|
-
const TERMINAL_USER = 'otb';
|
|
41
|
-
|
|
42
|
-
// Terminal URL with the basic-auth userinfo embedded, so "Open online
|
|
43
|
-
// environment" lands signed-in (browsers honor user:pass@host for top-level
|
|
44
|
-
// navigation; ttyd auth is plain HTTP basic). The credential is already
|
|
45
|
-
// displayed in plaintext on this panel, so the href adds no new exposure.
|
|
46
|
-
function terminalAuthUrl(term) {
|
|
47
|
-
if (!term || !term.url) return null;
|
|
48
|
-
if (!term.credential) return term.url;
|
|
49
|
-
try {
|
|
50
|
-
const u = new URL(term.url);
|
|
51
|
-
u.username = TERMINAL_USER;
|
|
52
|
-
u.password = term.credential;
|
|
53
|
-
return u.toString();
|
|
54
|
-
} catch (_) {
|
|
55
|
-
return term.url;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function create(opts) {
|
|
60
|
-
opts = opts || {};
|
|
61
|
-
const API = opts.api || '/api/bongos';
|
|
62
|
-
// Generated typed client (task 1996 / R09), served as window.BongosClient by
|
|
63
|
-
// /lib/bongos-client.js. baseUrl:'' passes the full same-origin paths this file
|
|
64
|
-
// builds through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
|
|
65
|
-
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
66
|
-
const showSsh = !!opts.showSsh;
|
|
67
|
-
const toast = typeof opts.toast === 'function' ? opts.toast : function () {};
|
|
68
|
-
|
|
69
|
-
let root = null; // the mounted container element
|
|
70
|
-
let pollTimer = null; // self-rescheduling refresh timer
|
|
71
|
-
let termWindow = null; // handle to the opened terminal tab (to close it)
|
|
72
|
-
let bound = false; // whether we've attached our click listener
|
|
73
|
-
|
|
74
|
-
const AUTH_REDIRECT = 'redirecting to auth';
|
|
75
|
-
|
|
76
|
-
async function getJSON(path) {
|
|
77
|
-
const res = await api.request('GET', path);
|
|
78
|
-
if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); throw new Error(AUTH_REDIRECT); }
|
|
79
|
-
if (!res.ok) throw new Error(`${path} → ${res.status}`);
|
|
80
|
-
return res.data;
|
|
81
|
-
}
|
|
82
|
-
async function postJSON(path) {
|
|
83
|
-
const res = await api.request('POST', path);
|
|
84
|
-
if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); throw new Error(AUTH_REDIRECT); }
|
|
85
|
-
if (!res.ok) {
|
|
86
|
-
let detail = `${path} → ${res.status}`;
|
|
87
|
-
try { const j = res.data; detail = j.message || j.detail || j.error || detail; } catch (_) { /* keep default */ }
|
|
88
|
-
const err = new Error(detail); err.status = res.status; throw err;
|
|
89
|
-
}
|
|
90
|
-
return res.data;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ---- DOM helpers (scoped to `root`) ------------------------------------
|
|
94
|
-
const q = (sel) => (root ? root.querySelector(sel) : null);
|
|
95
|
-
// NOTE: the `.setup__box-out` (display:flex) and `.setup__btn` (display:
|
|
96
|
-
// inline-block) rules out-specify the UA `[hidden]{display:none}` rule, so
|
|
97
|
-
// toggling the `hidden` attribute alone leaves an empty bordered stub / a
|
|
98
|
-
// stale "Request dev box" button visible. Drive `display` directly to hide.
|
|
99
|
-
function setOut(html) { const out = q('[data-box-out]'); if (out) { out.hidden = false; out.style.display = ''; out.innerHTML = html; } }
|
|
100
|
-
function hideOut() { const out = q('[data-box-out]'); if (out) { out.hidden = true; out.style.display = 'none'; out.innerHTML = ''; } }
|
|
101
|
-
function setRequestBtn(visible, label) {
|
|
102
|
-
const btn = q('[data-box-request]');
|
|
103
|
-
if (!btn) return;
|
|
104
|
-
btn.hidden = !visible;
|
|
105
|
-
btn.style.display = visible ? '' : 'none';
|
|
106
|
-
if (visible) { btn.disabled = false; btn.textContent = label || 'Request dev box'; }
|
|
107
|
-
}
|
|
108
|
-
function enableOpenBtn(url) {
|
|
109
|
-
const openBtn = q('[data-box-open]');
|
|
110
|
-
if (!openBtn) return;
|
|
111
|
-
if (url) {
|
|
112
|
-
openBtn.setAttribute('href', url);
|
|
113
|
-
openBtn.setAttribute('target', '_blank');
|
|
114
|
-
openBtn.setAttribute('rel', 'noopener noreferrer');
|
|
115
|
-
openBtn.removeAttribute('aria-disabled');
|
|
116
|
-
} else {
|
|
117
|
-
openBtn.removeAttribute('href');
|
|
118
|
-
openBtn.removeAttribute('target');
|
|
119
|
-
openBtn.removeAttribute('rel');
|
|
120
|
-
openBtn.setAttribute('aria-disabled', 'true');
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function stopPoll() { if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } }
|
|
125
|
-
function schedulePoll(ms) { stopPoll(); pollTimer = setTimeout(() => { refresh(); }, ms || 5000); }
|
|
126
|
-
|
|
127
|
-
function closeTermWindow() {
|
|
128
|
-
if (termWindow && !termWindow.closed) {
|
|
129
|
-
try { termWindow.close(); } catch (_) { /* cross-origin or already gone */ }
|
|
130
|
-
}
|
|
131
|
-
termWindow = null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// One credential/detail row. The value itself is click-to-copy (values are
|
|
135
|
-
// long) — no separate button.
|
|
136
|
-
function credRow(label, value) {
|
|
137
|
-
return `<div class="setup__cred-row">
|
|
138
|
-
<span class="setup__cred-k">${escapeHtml(label)}</span>
|
|
139
|
-
<code class="setup__cred-v setup__copy" data-copy="${escapeHtml(value)}" tabindex="0" role="button" title="Click to copy ${escapeHtml(label.toLowerCase())}" aria-label="Copy ${escapeHtml(label.toLowerCase())}">${escapeHtml(value)}</code>
|
|
140
|
-
</div>`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// The default Close-box control. Clicking it ARMS a confirm step
|
|
144
|
-
// (armCloseConfirm) instead of closing immediately — task 1459: a fresh box
|
|
145
|
-
// gets a NEW SSH identity, so a newcomer who hits "Host denied" on reconnect
|
|
146
|
-
// tends to reflexively close + reopen again, which only rotates the identity
|
|
147
|
-
// once more and digs the hole deeper. The confirm names the real cure.
|
|
148
|
-
function closeSectionHtml() {
|
|
149
|
-
return '<button type="button" class="setup__btn setup__btn--danger" data-box-close>Close box</button>'
|
|
150
|
-
+ '<span class="setup__box-close-hint">Stops and deletes your box. Provision a fresh one anytime.</span>';
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// The armed confirm: the warning + a real Close + a Keep. The Desktop/"Host
|
|
154
|
-
// denied" line is phrased conditionally so browser-terminal-only builders
|
|
155
|
-
// (who never touch SSH host keys) naturally skip it.
|
|
156
|
-
function closeConfirmHtml() {
|
|
157
|
-
// .setup__box-close is a flex row; flex-basis:100% puts the warning on its
|
|
158
|
-
// own line so the two buttons wrap neatly beneath it.
|
|
159
|
-
return '<p class="setup__box-close-hint" style="flex-basis:100%;margin:0">Closing deletes this box. The fresh one you open next has a '
|
|
160
|
-
+ '<strong>new identity</strong> — so if Claude Desktop then says <strong>“Host denied”</strong> '
|
|
161
|
-
+ 'or asks for a password, fully quit and reopen Desktop (it re-pins your box automatically) or press '
|
|
162
|
-
+ '<strong>Repair</strong>. Closing and reopening again will <em>not</em> help — it only changes the identity once more.</p>'
|
|
163
|
-
+ '<button type="button" class="setup__btn setup__btn--danger" data-box-close-confirm>Close box</button> '
|
|
164
|
-
+ '<button type="button" class="setup__btn" data-box-close-cancel>Keep box</button>';
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Swap the Close-box control between its default and armed states in place.
|
|
168
|
-
// Both operate on the enclosing .setup__box-close container, so a stray
|
|
169
|
-
// server repaint (which re-renders the default) just resets to "not armed".
|
|
170
|
-
function armCloseConfirm(btn) {
|
|
171
|
-
const wrap = btn.closest('.setup__box-close');
|
|
172
|
-
if (wrap) wrap.innerHTML = closeConfirmHtml();
|
|
173
|
-
}
|
|
174
|
-
function disarmCloseConfirm(btn) {
|
|
175
|
-
const wrap = btn.closest('.setup__box-close');
|
|
176
|
-
if (wrap) wrap.innerHTML = closeSectionHtml();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Render the active-box panel. SSH rows (IP + connect command) only appear
|
|
180
|
-
// when showSsh is true AND the box has an IP — the onboarding panel hides
|
|
181
|
-
// them so a newcomer sees only the online terminal. The terminal rows show
|
|
182
|
-
// whenever the tunnel has published a URL. All users get a Close button.
|
|
183
|
-
function renderBoxActive(box, term, cost) {
|
|
184
|
-
const parts = ['<p class="setup__box-msg setup__box-msg--ok">Your dev box is ready.</p>'];
|
|
185
|
-
|
|
186
|
-
// idea 332 / task 1316 (ADR 0072): flag a box running old code. The server
|
|
187
|
-
// decides `stale` (boxes.computeCodeStaleness), and since task 1002698 it says
|
|
188
|
-
// so ONLY for reason 'behind' — a box that actually reported a commit far
|
|
189
|
-
// behind the deployed one. Silence ('never_reported') is an unknown and no
|
|
190
|
-
// longer flags, so this banner can't fire on a healthy box any more.
|
|
191
|
-
//
|
|
192
|
-
// Lead with the CHEAP, NON-DESTRUCTIVE fix. A 'behind' box is reporting, so its
|
|
193
|
-
// sync cron is alive and a pull usually clears this on its own; closing the box
|
|
194
|
-
// destroys uncommitted work and costs a fresh spin-up. The old copy named the
|
|
195
|
-
// rebuild as the ONLY remedy ("It can't update itself"), which sent builders
|
|
196
|
-
// straight to the destructive option — and while never_reported still flagged,
|
|
197
|
-
// sent them there in a loop it could never resolve.
|
|
198
|
-
if (box.code && box.code.stale) {
|
|
199
|
-
const days = Number(box.code.lag_ms) > 0
|
|
200
|
-
? Math.floor(Number(box.code.lag_ms) / 86400000) : 0;
|
|
201
|
-
parts.push(
|
|
202
|
-
'<p class="setup__box-msg setup__box-msg--warn">⚠ This box is running <strong>older code</strong>'
|
|
203
|
-
+ (days ? ` (about ${days} day${days === 1 ? '' : 's'} behind)` : '')
|
|
204
|
-
+ ', which can break art generation and reconnecting. '
|
|
205
|
-
+ 'Run <code>/builder-sync</code> in the terminal to pull the latest — that fixes it in place, '
|
|
206
|
-
+ 'and keeps your work. If it is still behind after that, use <strong>Close box</strong> below '
|
|
207
|
-
+ 'and provision a fresh one.</p>'
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (showSsh && box.ip) {
|
|
212
|
-
parts.push(credRow('IP', box.ip));
|
|
213
|
-
parts.push(credRow('SSH', `ssh root@${box.ip}`));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (term && term.url) {
|
|
217
|
-
if (showSsh && box.ip) parts.push('<hr class="setup__divider">');
|
|
218
|
-
parts.push(credRow('Terminal', term.url));
|
|
219
|
-
if (term.credential) {
|
|
220
|
-
parts.push(credRow('Username', TERMINAL_USER));
|
|
221
|
-
parts.push(credRow('Password', term.credential));
|
|
222
|
-
}
|
|
223
|
-
// #893: closing the terminal tab no longer kills the session (it runs in
|
|
224
|
-
// tmux on the box), and never shut the box down. Say both, so a builder
|
|
225
|
-
// neither fears losing work nor leaves the box billing thinking it's off.
|
|
226
|
-
parts.push('<p class="setup__box-close-hint">Closing the terminal tab keeps your session running — reopen to resume right where you left off. To shut the box down, use <strong>Close box</strong> below or type <code>close-box</code> in the terminal.</p>');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// #602: the builder's own running container-cost total. Tracked, not
|
|
230
|
-
// charged in the early stage (mirrors "shipped work isn't paid in real money
|
|
231
|
-
// yet") — say so plainly so the number doesn't read as a bill.
|
|
232
|
-
if (cost && Number(cost.total_usd) > 0) {
|
|
233
|
-
const f = (n) => `$${(Number(n) || 0).toFixed(2)}`;
|
|
234
|
-
parts.push('<hr class="setup__divider">');
|
|
235
|
-
parts.push(`<p class="setup__box-cost"><strong>What your box would cost:</strong> ${f(cost.total_usd)} so far `
|
|
236
|
-
+ `<span class="setup__box-cost-detail">(spin-up ${f(cost.spinup_usd)} · compute ${f(cost.compute_usd)} · storage ${f(cost.storage_usd)})</span><br>`
|
|
237
|
-
+ '<span class="setup__box-close-hint">Tracked for transparency — not charged in the early stage.</span></p>');
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
parts.push(`<div class="setup__box-close">${closeSectionHtml()}</div>`);
|
|
241
|
-
|
|
242
|
-
return parts.join('\n');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Paint from live server state. Idempotent + safe to call repeatedly. Never
|
|
246
|
-
// throws into the caller. Stops polling once the element is gone.
|
|
247
|
-
async function refresh() {
|
|
248
|
-
if (!root || !document.body.contains(root)) { stopPoll(); return; }
|
|
249
|
-
|
|
250
|
-
let me;
|
|
251
|
-
try { me = await getJSON(`${API}/box/me`); }
|
|
252
|
-
catch (err) { if (err && err.message === AUTH_REDIRECT) return; stopPoll(); return; }
|
|
253
|
-
const box = (me && me.box) || {};
|
|
254
|
-
const intent = me && me.open_intent;
|
|
255
|
-
const state = box.state || 'none';
|
|
256
|
-
|
|
257
|
-
// A close is in flight: the box stays 'active' for up to a minute while
|
|
258
|
-
// the control-plane runner deprovisions it, so this MUST be checked
|
|
259
|
-
// BEFORE the active branch — otherwise every poll repaints the full active
|
|
260
|
-
// panel and wipes the "Closing…" feedback. Close the terminal tab now.
|
|
261
|
-
if (intent && intent.action === 'deprovision') {
|
|
262
|
-
closeTermWindow();
|
|
263
|
-
setRequestBtn(false);
|
|
264
|
-
enableOpenBtn(null);
|
|
265
|
-
setOut('<p class="setup__box-msg">Closing your box… (a few seconds)</p>');
|
|
266
|
-
schedulePoll(4000);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (state === 'active') {
|
|
271
|
-
let term = null;
|
|
272
|
-
try { term = await getJSON(`${API}/box/terminal`); }
|
|
273
|
-
catch (err) { if (err && err.message === AUTH_REDIRECT) return; }
|
|
274
|
-
|
|
275
|
-
setRequestBtn(false);
|
|
276
|
-
enableOpenBtn(terminalAuthUrl(term));
|
|
277
|
-
|
|
278
|
-
if (term && term.url) {
|
|
279
|
-
setOut(renderBoxActive(box, term, me && me.cost));
|
|
280
|
-
stopPoll();
|
|
281
|
-
} else if (term && term.warming_up) {
|
|
282
|
-
setOut('<p class="setup__box-msg">Your box is up — waiting for the terminal to come online (a few seconds after boot)…</p>');
|
|
283
|
-
schedulePoll(5000);
|
|
284
|
-
} else {
|
|
285
|
-
// SSH path (or tunnel not yet published). With SSH shown, surface the
|
|
286
|
-
// details now; either way keep polling so the terminal URL appears
|
|
287
|
-
// when it publishes.
|
|
288
|
-
setOut(renderBoxActive(box, null, me && me.cost));
|
|
289
|
-
if (!box.ip) schedulePoll(5000);
|
|
290
|
-
else if (!showSsh) schedulePoll(5000); // online-only: still awaiting terminal
|
|
291
|
-
else stopPoll();
|
|
292
|
-
}
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
if (intent || state === 'provisioning' || state === 'waking') {
|
|
296
|
-
setRequestBtn(false);
|
|
297
|
-
enableOpenBtn(null);
|
|
298
|
-
setOut('<p class="setup__box-msg">Setting up your box… (about a minute)</p>');
|
|
299
|
-
schedulePoll(5000);
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
// No open intent and not active/provisioning/waking. Three cases:
|
|
303
|
-
// • parked → asleep after the idle sweep, filesystem preserved (task 1002726).
|
|
304
|
-
// Reachable again as of that task; offer a wake, and say the files are safe.
|
|
305
|
-
// • destroyed → the builder HAD a box that is now gone — they closed it, an
|
|
306
|
-
// operator deprovisioned it, or it passed the dormancy window after being
|
|
307
|
-
// warned. Either way the tunnel + DNS are torn down, so opening the old
|
|
308
|
-
// terminal/sandbox URL fails with a raw browser "can't find the server" and
|
|
309
|
-
// no explanation (idea #295). Don't leave them staring at a bare button.
|
|
310
|
-
// • none → never had a box (first run). Keep the clean first-run button.
|
|
311
|
-
closeTermWindow();
|
|
312
|
-
stopPoll();
|
|
313
|
-
enableOpenBtn(null);
|
|
314
|
-
// task 1002728 — PARKED is a real, reachable state again since task 1002726
|
|
315
|
-
// made the idle sweep park instead of destroy. Before that it could never
|
|
316
|
-
// happen, so there was no branch for it and a parked box fell through to
|
|
317
|
-
// the first-run "Request dev box" screen — which reads as "your box is
|
|
318
|
-
// gone" at the exact moment the box and its files are safe.
|
|
319
|
-
if (state === 'parked') {
|
|
320
|
-
setRequestBtn(true, 'Wake my box');
|
|
321
|
-
setOut('<p class="setup__box-msg">Your dev box is asleep — wake it below.</p>'
|
|
322
|
-
+ '<p class="setup__box-close-hint">It was put to sleep after a couple of hours idle so it stops '
|
|
323
|
-
+ 'costing money. <strong>Your files are kept</strong>, including anything you had not committed — '
|
|
324
|
-
+ 'waking it brings the box back as you left it, in about a minute. A sleeping box has no terminal '
|
|
325
|
-
+ 'or live-preview URL, so an old link will fail to load until it is awake.</p>'
|
|
326
|
-
+ '<p class="setup__box-close-hint">If it stays asleep for a couple of weeks we clean it up to save '
|
|
327
|
-
+ 'cost, but you will get a warning first with several days to wake it.</p>');
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
if (state === 'destroyed') {
|
|
331
|
-
setRequestBtn(true, 'Turn box back on');
|
|
332
|
-
setOut('<p class="setup__box-msg">Your dev box is stopped — turn it back on below.</p>'
|
|
333
|
-
+ '<p class="setup__box-close-hint">This happens when you close a box yourself, or after it has been '
|
|
334
|
-
+ 'asleep long enough to be cleaned up (you are warned first). A stopped box has no terminal or '
|
|
335
|
-
+ 'live-preview URL, so an old link will fail to load. Turning it back on takes about a minute, and '
|
|
336
|
-
+ 'gives you a <strong>fresh</strong> box — <strong>anything not committed or shipped on the old one '
|
|
337
|
-
+ 'is gone</strong>. (Going idle no longer does this: an idle box is put to sleep with your files '
|
|
338
|
-
+ 'intact.)</p>'
|
|
339
|
-
+ '<p class="setup__box-close-hint">The box you turn on has a <strong>new identity</strong>. If Claude '
|
|
340
|
-
+ 'Desktop says <strong>“Host denied”</strong> or asks for a password when reconnecting, fully quit '
|
|
341
|
-
+ 'and reopen Desktop (it re-pins your box automatically) or press <strong>Repair</strong> — do not keep turning '
|
|
342
|
-
+ 'the box off and on, which only changes the identity again.</p>');
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
setRequestBtn(true, 'Request dev box');
|
|
346
|
-
hideOut();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Request the box (provision), then let refresh paint from server state.
|
|
350
|
-
async function requestBox() {
|
|
351
|
-
const btn = q('[data-box-request]');
|
|
352
|
-
if (btn) { btn.disabled = true; btn.textContent = 'Setting up…'; }
|
|
353
|
-
try {
|
|
354
|
-
await postJSON(`${API}/box/ensure`);
|
|
355
|
-
} catch (err) {
|
|
356
|
-
if (err && err.message === AUTH_REDIRECT) return;
|
|
357
|
-
if (btn) { btn.disabled = false; btn.textContent = 'Request dev box'; }
|
|
358
|
-
setOut(`<p class="setup__box-msg setup__box-msg--err">${escapeHtml(err.message || 'Could not set up your box just now.')} You can also ask in Discord #support.</p>`);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
refresh();
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Enqueue a deprovision intent, then poll until the box is destroyed. Paints
|
|
365
|
-
// "Closing…" and closes the terminal tab IMMEDIATELY so the action feels
|
|
366
|
-
// instant — the box stays active for up to a minute while it tears down.
|
|
367
|
-
async function closeBox() {
|
|
368
|
-
stopPoll();
|
|
369
|
-
closeTermWindow();
|
|
370
|
-
setOut('<p class="setup__box-msg">Closing your box… (a few seconds)</p>');
|
|
371
|
-
enableOpenBtn(null);
|
|
372
|
-
setRequestBtn(false);
|
|
373
|
-
try {
|
|
374
|
-
await postJSON(`${API}/box/close`);
|
|
375
|
-
} catch (err) {
|
|
376
|
-
if (err && err.message === AUTH_REDIRECT) return;
|
|
377
|
-
setOut(`<p class="setup__box-msg setup__box-msg--err">${escapeHtml(err.message || 'Could not close your box just now.')} You can also ask in Discord #support.</p>`);
|
|
378
|
-
schedulePoll(1000);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
refresh();
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function onClick(e) {
|
|
385
|
-
if (e.target.closest('[data-box-request]')) { requestBox(); return; }
|
|
386
|
-
// Close box is a two-step confirm: first click arms (shows the reconnect
|
|
387
|
-
// warning), then Close box / Keep box. Check the armed buttons before the
|
|
388
|
-
// bare [data-box-close] so the arming click can't also fire a close.
|
|
389
|
-
if (e.target.closest('[data-box-close-confirm]')) { closeBox(); return; }
|
|
390
|
-
if (e.target.closest('[data-box-close-cancel]')) { disarmCloseConfirm(e.target.closest('[data-box-close-cancel]')); return; }
|
|
391
|
-
if (e.target.closest('[data-box-close]')) { armCloseConfirm(e.target.closest('[data-box-close]')); return; }
|
|
392
|
-
// Open the in-browser terminal via window.open (NOT a plain target=_blank
|
|
393
|
-
// nav) so we hold a handle and can close the tab when the box is torn
|
|
394
|
-
// down. The named target reuses the same tab on re-click.
|
|
395
|
-
const openBtnEl = e.target.closest('[data-box-open]');
|
|
396
|
-
if (openBtnEl) {
|
|
397
|
-
const url = openBtnEl.getAttribute('href');
|
|
398
|
-
if (url && openBtnEl.getAttribute('aria-disabled') !== 'true') {
|
|
399
|
-
e.preventDefault();
|
|
400
|
-
termWindow = window.open(url, 'otb-box-terminal');
|
|
401
|
-
if (termWindow) { try { termWindow.opener = null; } catch (_) {} }
|
|
402
|
-
}
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
const copyEl = e.target.closest('.setup__copy[data-copy]');
|
|
406
|
-
if (copyEl) {
|
|
407
|
-
const text = copyEl.getAttribute('data-copy') || '';
|
|
408
|
-
const done = () => {
|
|
409
|
-
copyEl.classList.add('setup__copy--copied');
|
|
410
|
-
setTimeout(() => { copyEl.classList.remove('setup__copy--copied'); }, 1200);
|
|
411
|
-
};
|
|
412
|
-
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
413
|
-
navigator.clipboard.writeText(text).then(done).catch(() => toast('Could not copy — select and copy by hand.'));
|
|
414
|
-
} else {
|
|
415
|
-
toast('Copy not supported here — select the value and copy by hand.');
|
|
416
|
-
}
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const MARKUP = `
|
|
422
|
-
<div class="setup__box-btns">
|
|
423
|
-
<button type="button" class="setup__btn setup__btn--box" data-box-request>Request dev box</button>
|
|
424
|
-
<a class="setup__btn setup__btn--open" data-box-open aria-disabled="true">Open online environment</a>
|
|
425
|
-
</div>
|
|
426
|
-
<div class="setup__box-out" data-box-out hidden></div>`;
|
|
427
|
-
|
|
428
|
-
function mount(el) {
|
|
429
|
-
stopPoll();
|
|
430
|
-
root = el || null;
|
|
431
|
-
if (!root) return;
|
|
432
|
-
root.innerHTML = MARKUP;
|
|
433
|
-
// The element is freshly re-created by the host on each render, so a new
|
|
434
|
-
// listener per mount does not stack on a live node. Guard anyway.
|
|
435
|
-
if (!root._otbBoxBound) {
|
|
436
|
-
root.addEventListener('click', onClick);
|
|
437
|
-
// Keyboard activation for the click-to-copy values (role="button").
|
|
438
|
-
root.addEventListener('keydown', (e) => {
|
|
439
|
-
if ((e.key === 'Enter' || e.key === ' ') && e.target.closest('.setup__copy[data-copy]')) {
|
|
440
|
-
e.preventDefault();
|
|
441
|
-
onClick(e);
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
root._otbBoxBound = true;
|
|
445
|
-
}
|
|
446
|
-
bound = true;
|
|
447
|
-
refresh();
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function stop() { stopPoll(); }
|
|
451
|
-
|
|
452
|
-
return { mount, refresh, stop };
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
window.OTBBoxPanel = { create };
|
|
456
|
-
})();
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/* harbor.css — Page-local styles for the Harbor (task 1003469 rebuild).
|
|
2
|
-
The harbor is one panel of oversight rows grouped by lifecycle, over the
|
|
3
|
-
shell's glass summary strip; everything structural is oversight.css. What is
|
|
4
|
-
the harbor's own is the one line a barred builder carries. The ?v= cache-bust
|
|
5
|
-
is stamped from this file's content hash at boot (src/asset-version.js). */
|
|
6
|
-
|
|
7
|
-
/* A barred builder: the reason under the row's title, in the danger ink and at
|
|
8
|
-
the body measure. Not a coloured bar — the world marks state with a pill. */
|
|
9
|
-
.harbor-barred { margin: 6px 0 0; font-size: 13px; line-height: 1.5; color: var(--danger); max-width: var(--measure); overflow-wrap: anywhere; }
|
|
@@ -1,69 +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>Harbor — {{worldName}}</title>
|
|
7
|
-
<meta name="description" content="Archon view of each builder's dev box, with controls to close a box or block a builder from opening one.">
|
|
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
|
-
<link rel="stylesheet" href="/builders/oversight.css">
|
|
13
|
-
<link rel="stylesheet" href="/builders/harbor.css">
|
|
14
|
-
</head>
|
|
15
|
-
<body data-page="harbor" data-page-title="Harbor">
|
|
16
|
-
|
|
17
|
-
<div class="app">
|
|
18
|
-
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
19
|
-
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
20
|
-
<div class="app-body">
|
|
21
|
-
<header class="app-topbar" id="app-topbar"></header>
|
|
22
|
-
<div class="app-content">
|
|
23
|
-
<!-- The board page head (task 1003307): the LABEL voice, not a display
|
|
24
|
-
line, because on a working surface the page's own name is the least
|
|
25
|
-
interesting thing on it. -->
|
|
26
|
-
<div class="page-head page-head--board">
|
|
27
|
-
<h1 class="page-head__h">Harbor</h1>
|
|
28
|
-
<p class="page-head__sub" id="harbor-sub">Loading dev boxes…</p>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<main id="hall-main">
|
|
32
|
-
<!-- task 1003469: the sealed state is a panel with a heading, not a bare
|
|
33
|
-
line on the ground. harbor.js writes the real floor into the lede
|
|
34
|
-
from the pack's rank label, so the copy cannot drift from ADR 0157. -->
|
|
35
|
-
<section class="scroll" id="harbor-sealed" aria-labelledby="harbor-sealed-h" hidden>
|
|
36
|
-
<h2 class="scroll__h" id="harbor-sealed-h">Restricted</h2>
|
|
37
|
-
<p class="scroll__lede" id="harbor-sealed-body">This page opens at a higher rank.</p>
|
|
38
|
-
</section>
|
|
39
|
-
|
|
40
|
-
<section id="harbor-body" hidden>
|
|
41
|
-
<!-- The glass summary strip (the shell's .stat-row), then ONE panel of
|
|
42
|
-
oversight rows grouped by lifecycle — the 280px card deck with a
|
|
43
|
-
coloured roof band per box is gone (task 1003469). -->
|
|
44
|
-
<div class="stat-row ov-summary" id="harbor-summary" aria-label="Fleet summary"></div>
|
|
45
|
-
|
|
46
|
-
<section class="scroll" aria-labelledby="harbor-dock-h">
|
|
47
|
-
<h2 class="scroll__h" id="harbor-dock-h">Dev boxes</h2>
|
|
48
|
-
<p class="scroll__lede">Every builder's box, running ones first. Close a box to reclaim it; block a builder to keep them from opening another.</p>
|
|
49
|
-
<div id="harbor-dock"><span class="kit-skel"></span></div>
|
|
50
|
-
</section>
|
|
51
|
-
</section>
|
|
52
|
-
</main>
|
|
53
|
-
|
|
54
|
-
<footer class="app-foot" id="app-foot"></footer>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<div id="harbor-modal-root"></div>
|
|
60
|
-
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
61
|
-
|
|
62
|
-
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
63
|
-
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
64
|
-
<script src="/builders/palette.js" defer></script>
|
|
65
|
-
<script src="/builders/dom-utils.js?v=2026-06-16-harbor"></script>
|
|
66
|
-
<script src="/builders/hall-kit.js"></script>
|
|
67
|
-
<script src="/builders/harbor.js?v=2026-09-02-harbor-rows"></script>
|
|
68
|
-
</body>
|
|
69
|
-
</html>
|