@bongos/core 1.24.26 → 1.24.28
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 +164 -109
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +12 -0
- package/docs/copy-inventory.md +242 -223
- package/docs/copy-registry.json +559 -358
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +18 -6
- package/docs/page-readings.json +4399 -4315
- package/docs/recipes/ui-look-before-you-ship.md +1 -0
- package/modules/autonomy/fence.js +16 -1
- package/modules/government/protected-surfaces.json +2 -0
- package/modules/hall-ui/public/ai-studio.html +1 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/core-move-notice.js +9 -6
- package/modules/hall-ui/public/deploy-handoff.js +203 -0
- package/modules/hall-ui/public/deploy.html +4 -2
- package/modules/hall-ui/public/deploy.js +7 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/market-watch-item.html +1 -1
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch-settings.html +1 -1
- package/modules/hall-ui/public/market-watch.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/my-agents.css +72 -1
- package/modules/hall-ui/public/my-agents.html +31 -7
- package/modules/hall-ui/public/my-agents.js +63 -54
- package/modules/hall-ui/public/my-agents.states.json +18 -0
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-software-update.js +22 -8
- package/modules/hall-ui/public/settings.css +2 -132
- package/modules/hall-ui/public/settings.html +15 -53
- package/modules/hall-ui/public/shell.js +25 -73
- package/modules/hall-ui/public/speciality-tools.css +140 -0
- package/modules/hall-ui/public/{settings-specialities.js → speciality-tools.js} +83 -6
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +119 -39
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/update-pill.js +623 -0
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/my-agents.md +4 -2
- package/modules/hall-ui/records/panel-family.md +2 -0
- package/modules/hall-ui/records/update-pill.md +12 -0
- package/modules/lifecycle/github-push.js +1 -0
- package/modules/lifecycle/routes/tasks.js +2 -2
- package/modules/lifecycle/runner-self-guard.js +122 -0
- package/modules/lifecycle/runner-self.js +154 -0
- package/modules/public-landing/public/account.html +3 -3
- package/modules/public-landing/public/assets/cosmos.css +45 -0
- package/modules/public-landing/public/community.html +4 -4
- package/modules/public-landing/public/connections.html +1 -1
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/invitations.html +4 -4
- package/modules/public-landing/public/projects.html +26 -26
- package/modules/status-ui/public/index.html +2 -2
- package/modules/status-ui/public/index.states.json +12 -0
- package/modules/status-ui/public/status.js +10 -2
- package/modules/status-ui/public/style.css +57 -0
- package/modules/ui-design/kit/README.md +1 -1
- package/modules/ui-design/kit/serve.js +8 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/scripts/bongos/autobongos-loop.js +4 -0
- package/scripts/bongos/autobongos-run.js +21 -4
- package/scripts/bongos/autobongos-tweak-lane.js +5 -3
- package/scripts/bongos/ship-flow.js +20 -0
- package/scripts/hall-preview/server.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/autobongos_host_access.mjs +5 -3
- package/tests/autobongos_self_modification.mjs +420 -0
- package/tests/core_update_banner.mjs +6 -92
- package/tests/core_upgrade_owner_door.mjs +4 -56
- package/tests/deploy_handoff.mjs +206 -0
- package/tests/government_protected_surfaces.mjs +15 -0
- package/tests/hall_loading.mjs +3 -1
- package/tests/hall_my_agents.mjs +104 -2
- package/tests/helpers.mjs +161 -0
- package/tests/hub_connections_page.mjs +1 -1
- package/tests/hub_invitations_page.mjs +1 -1
- package/tests/loading_mark.mjs +203 -0
- package/tests/nav_permission_atoms.mjs +3 -2
- package/tests/software_update.mjs +6 -2
- package/tests/speciality_offer_ui.mjs +182 -149
- package/tests/speciality_walkthrough_ui.mjs +180 -161
- package/tests/update_pill.mjs +431 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
// update-pill.js — the hall's Update pill: a newer core, taken in place (task 1005041).
|
|
2
|
+
//
|
|
3
|
+
// The owner, 2026-11-03: upgrading Bongos from a project's hall was "way too obstructing and
|
|
4
|
+
// not obvious what to do", and it should feel like the Claude app, which just tells you to
|
|
5
|
+
// restart. Before this, a hosted project's owner met a full-width banner, went to Settings,
|
|
6
|
+
// pressed Update, landed on the hub's deploy door on another site, pressed Check, then
|
|
7
|
+
// Confirm, then waited there. Now it is one control in the top bar, in four faces:
|
|
8
|
+
// ready "Update". Pressing it opens a small card: what the update brings, how long it
|
|
9
|
+
// takes, that it puts itself back if it goes wrong, and one Update now button.
|
|
10
|
+
// running "Updating…", and the card says where it has got to.
|
|
11
|
+
// done "Reload". The Claude moment: the new version is running, this page is not.
|
|
12
|
+
// held the newest version was tried here and rolled back; the card says so plainly.
|
|
13
|
+
//
|
|
14
|
+
// TWO STEPS, STILL (ADR 0293 D4). Opening the card is step one: it names the version that
|
|
15
|
+
// would go in. Update now is step two and sends THAT version. `offered` is read off the
|
|
16
|
+
// reading the card was drawn from, so a release that lands while the card is open cannot
|
|
17
|
+
// change what the press does.
|
|
18
|
+
//
|
|
19
|
+
// WHERE THE ACT HAPPENS IS UNCHANGED (ADR 0293 D1, ADR 0339 D3). The server names this
|
|
20
|
+
// project's door (GET /software-update `door`), and doorPlan() reads it:
|
|
21
|
+
// local this hall runs the door (the platform): it calls its own core-upgrade route.
|
|
22
|
+
// hub a hosted project: the act stays on the hub. A small hub window opens on the door
|
|
23
|
+
// with &handoff=1, this page tells it the version over postMessage, and the hub
|
|
24
|
+
// posts the move with the owner's OWN hub session (deploy-handoff.js). This
|
|
25
|
+
// project's server gains no capability; the only thing that crosses is a version.
|
|
26
|
+
// ask a hosted project the hub's operator runs: "Ask for this update" (task 1004595).
|
|
27
|
+
// cli a self-hosted instance: no button; the card names the command.
|
|
28
|
+
//
|
|
29
|
+
// PROGRESS IS READ, NEVER GUESSED (deploy.js's rule). The page watches its own public
|
|
30
|
+
// /version: still the old version is "starting", no answer is "restarting", the new one is
|
|
31
|
+
// done. A rollback is the server's upgrade hold naming the version (GET /core-update `held`).
|
|
32
|
+
// The run is remembered in this browser, so it follows the owner from page to page and a
|
|
33
|
+
// reload in the middle picks it up again.
|
|
34
|
+
//
|
|
35
|
+
// Its own file, loaded by shell.js only when there is something to say (shell.js is at its
|
|
36
|
+
// 1,500-line ratchet), and by settings.html, whose Update button runs the same flow.
|
|
37
|
+
// Node-require-safe (github-chip.js's tail), so the tests run the pure half without a page.
|
|
38
|
+
(function (root) {
|
|
39
|
+
'use strict';
|
|
40
|
+
|
|
41
|
+
var RUN_KEY = 'bongos.update.run';
|
|
42
|
+
var MSG = 'bongos-update'; // the tag deploy-handoff.js answers to; both sides check it
|
|
43
|
+
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
44
|
+
// A hub door as the server words it (core-update.js HUB_UPGRADE_URL_RE's shape): an
|
|
45
|
+
// origin and /deploy?project=<slug>, nothing else. Anything else draws no button.
|
|
46
|
+
var HUB_DOOR_RE = /^(https?:\/\/[^\s"'<>/]+)\/deploy\?project=[A-Za-z0-9._%-]+$/;
|
|
47
|
+
var SETTINGS_HREF = '/settings#software-update';
|
|
48
|
+
var HIGHLIGHTS = 3;
|
|
49
|
+
var LINE_MAX = 120;
|
|
50
|
+
// The runner drains on a two-minute tick, then installs, migrates, restarts and checks
|
|
51
|
+
// health. Past SLOW_MS the card says it is taking longer than usual; past GIVE_UP_MS a
|
|
52
|
+
// remembered run is forgotten (the Settings history still has it).
|
|
53
|
+
var SLOW_MS = 8 * 60 * 1000;
|
|
54
|
+
var GIVE_UP_MS = 45 * 60 * 1000;
|
|
55
|
+
// How long a closed hub window is given to show that the update started anyway, for the
|
|
56
|
+
// case where the window could not talk back (its own one-press fallback was used).
|
|
57
|
+
var UNHEARD_MS = 4 * 60 * 1000;
|
|
58
|
+
var HOLD_EVERY = 5; // the hold is asked for on every fifth look, /version on every one
|
|
59
|
+
function num(v, dflt) { return typeof v === 'number' && isFinite(v) ? v : dflt; }
|
|
60
|
+
// A harness drives the loop without sleeping through it (deploy.js's __deployPollMs).
|
|
61
|
+
var POLL_MS = num(root && root.__updatePollMs, 4000);
|
|
62
|
+
|
|
63
|
+
// ---------- the pure half ----------
|
|
64
|
+
|
|
65
|
+
function isVersion(v) { return typeof v === 'string' && VERSION_RE.test(v); }
|
|
66
|
+
|
|
67
|
+
// Voice rule V4 (docs/voice.md): a release-note line is quoted ledger data, so its dashes
|
|
68
|
+
// become commas at render, the way settings-software-update.js does it.
|
|
69
|
+
function unDash(raw) {
|
|
70
|
+
return String(raw == null ? '' : raw).replace(/\s*[—–]\s*/g, ', ').replace(/,\s*,/g, ',').trim();
|
|
71
|
+
}
|
|
72
|
+
function clip(s) {
|
|
73
|
+
if (s.length <= LINE_MAX) return s;
|
|
74
|
+
var cut = s.slice(0, LINE_MAX - 1);
|
|
75
|
+
var space = cut.lastIndexOf(' ');
|
|
76
|
+
return (space > LINE_MAX / 2 ? cut.slice(0, space) : cut).replace(/[,;:.\s]+$/, '') + '…';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** What the update brings, from GET /software-update's `whats_new`: the first few lines,
|
|
80
|
+
* newest first, and how many there are. `more` is true when older versions were left out
|
|
81
|
+
* of the reading, so the count is a floor. */
|
|
82
|
+
function highlights(whatsNew, n) {
|
|
83
|
+
var w = whatsNew || {};
|
|
84
|
+
var versions = Array.isArray(w.versions) ? w.versions : [];
|
|
85
|
+
var lines = [];
|
|
86
|
+
var count = 0;
|
|
87
|
+
for (var i = 0; i < versions.length; i++) {
|
|
88
|
+
var entries = versions[i] && Array.isArray(versions[i].entries) ? versions[i].entries : [];
|
|
89
|
+
for (var j = 0; j < entries.length; j++) {
|
|
90
|
+
var t = unDash(entries[j] && entries[j].text);
|
|
91
|
+
if (!t) continue;
|
|
92
|
+
count++;
|
|
93
|
+
if (lines.length < n) lines.push(clip(t));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { lines: lines, count: count, more: !!w.more, read: w.ok !== false && versions.length > 0 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Where Update now goes, from GET /software-update. Same rules as the Settings panel:
|
|
100
|
+
* no button for a viewer the door would refuse, and only a known door shape is used. */
|
|
101
|
+
function doorPlan(sw) {
|
|
102
|
+
if (!sw || sw.can_update !== true) return { kind: 'none' };
|
|
103
|
+
var door = sw.door || {};
|
|
104
|
+
var ask = sw.ask || {};
|
|
105
|
+
if (door.kind === 'local' && door.url === '/deploy') return { kind: 'local' };
|
|
106
|
+
if (door.kind === 'hub') {
|
|
107
|
+
var m = HUB_DOOR_RE.exec(String(door.url || ''));
|
|
108
|
+
if (!m) return ask.available ? { kind: 'ask' } : { kind: 'none' };
|
|
109
|
+
return { kind: 'hub', url: door.url, origin: m[1] };
|
|
110
|
+
}
|
|
111
|
+
if (door.kind === 'cli') return ask.available ? { kind: 'ask' } : { kind: 'cli' };
|
|
112
|
+
return { kind: 'none' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The run this browser remembers: { to, from, at, via }, or null. Every touch is guarded,
|
|
116
|
+
* because storage throws in a private window and is absent in a vm. */
|
|
117
|
+
function readRun(storage, now) {
|
|
118
|
+
var run = null;
|
|
119
|
+
try { run = JSON.parse((storage && storage.getItem(RUN_KEY)) || 'null'); } catch (e) { run = null; }
|
|
120
|
+
if (!run || !isVersion(run.to) || !isFinite(run.at)) return null;
|
|
121
|
+
if (now - run.at > GIVE_UP_MS) { forgetRun(storage); return null; }
|
|
122
|
+
return run;
|
|
123
|
+
}
|
|
124
|
+
function keepRun(storage, run) {
|
|
125
|
+
try { if (storage) storage.setItem(RUN_KEY, JSON.stringify(run)); } catch (e) { /* this page still watches */ }
|
|
126
|
+
}
|
|
127
|
+
function forgetRun(storage) {
|
|
128
|
+
try { if (storage) storage.removeItem(RUN_KEY); } catch (e) { /* best-effort */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Which face the pill shows when a page loads, from GET /core-update and the remembered
|
|
132
|
+
* run. A run whose version this server already runs has landed (the page that loaded IS
|
|
133
|
+
* the new version), so there is nothing to say; the one-time what's-new note says it. */
|
|
134
|
+
function pillState(o) {
|
|
135
|
+
var u = (o && o.update) || {};
|
|
136
|
+
var run = o && o.run;
|
|
137
|
+
var held = o && o.held;
|
|
138
|
+
if (run) return u.running === run.to ? 'landed' : 'running';
|
|
139
|
+
if (held && isVersion(held.version) && held.version === u.latest) return 'held';
|
|
140
|
+
// "Behind" is said only from an answer (shell.js's old banner rule): a registry that did
|
|
141
|
+
// not answer is not "up to date", and it is not "update ready" either.
|
|
142
|
+
if (u.reading === 'ok' && u.behind && isVersion(u.latest)) return 'ready';
|
|
143
|
+
return 'none';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** One look folded into the run's stage. `look.version` is what /version said, or null
|
|
147
|
+
* when the server did not answer; `look.held` is the held version when it was asked. */
|
|
148
|
+
function stageOf(run, look, now) {
|
|
149
|
+
if (look.version === run.to) return 'done';
|
|
150
|
+
if (look.held && look.held === run.to) return 'failed';
|
|
151
|
+
var slow = now - run.at > SLOW_MS;
|
|
152
|
+
if (look.version === null || run.down) return slow ? 'slow' : 'restarting';
|
|
153
|
+
return slow ? 'slow' : 'starting';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------- the page half ----------
|
|
157
|
+
|
|
158
|
+
var state = {
|
|
159
|
+
api: null, update: null, held: null, sw: null, swAsked: null, swError: null, ended: null, note: null,
|
|
160
|
+
face: 'none', stage: null, run: null, error: null, pill: null, card: null,
|
|
161
|
+
popup: null, plan: null, offered: null, looks: 0, timer: null, popupTimer: null, listening: false,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function doc() { return root && root.document; }
|
|
165
|
+
function store() { try { return root && root.localStorage; } catch (e) { return null; } }
|
|
166
|
+
function now() { return Date.now(); }
|
|
167
|
+
function esc(s) { return root.OTB.escapeHtml(String(s == null ? '' : s)); }
|
|
168
|
+
function mono(v) { return '<span class="upd-ver">' + esc(v) + '</span>'; }
|
|
169
|
+
|
|
170
|
+
var DOT = '<span class="upd-pill__dot" aria-hidden="true"></span>';
|
|
171
|
+
var SPIN = '<span class="upd-pill__spin" aria-hidden="true"></span>';
|
|
172
|
+
var FACES = {
|
|
173
|
+
ready: { label: 'Update', title: 'A new version of Bongos is ready', lead: DOT },
|
|
174
|
+
held: { label: 'Update held', title: 'The newest version did not go in here', lead: DOT },
|
|
175
|
+
running: { label: 'Updating…', title: 'Bongos is updating', lead: SPIN },
|
|
176
|
+
done: { label: 'Reload', title: 'Bongos is updated. Reload to use it', lead: DOT },
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
function drawPill() {
|
|
180
|
+
var d = doc();
|
|
181
|
+
var face = FACES[state.face];
|
|
182
|
+
if (!d || !face) { if (state.pill) state.pill.remove(); state.pill = null; return; }
|
|
183
|
+
var side = d.querySelector && d.querySelector('#app-topbar .topbar-side');
|
|
184
|
+
if (!state.pill) {
|
|
185
|
+
if (!side) return;
|
|
186
|
+
var b = d.createElement('button');
|
|
187
|
+
b.type = 'button';
|
|
188
|
+
b.id = 'upd-pill';
|
|
189
|
+
b.className = 'upd-pill';
|
|
190
|
+
b.setAttribute('aria-haspopup', 'dialog');
|
|
191
|
+
b.setAttribute('aria-expanded', 'false');
|
|
192
|
+
b.setAttribute('aria-controls', 'upd-card');
|
|
193
|
+
b.addEventListener('click', function () {
|
|
194
|
+
if (state.face === 'done') { reload(); return; }
|
|
195
|
+
toggleCard();
|
|
196
|
+
});
|
|
197
|
+
side.insertBefore(b, side.firstChild);
|
|
198
|
+
state.pill = b;
|
|
199
|
+
}
|
|
200
|
+
state.pill.setAttribute('data-face', state.face);
|
|
201
|
+
state.pill.title = face.title;
|
|
202
|
+
state.pill.setAttribute('aria-label', face.title);
|
|
203
|
+
state.pill.innerHTML = face.lead + '<span class="upd-pill__label">' + face.label + '</span>';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function cardHtml(face) {
|
|
207
|
+
var u = state.update || {};
|
|
208
|
+
var close = '<button type="button" class="upd-card__close" id="upd-close" aria-label="Close">×</button>';
|
|
209
|
+
var run = state.run || state.ended;
|
|
210
|
+
var from = run ? run.from : u.running;
|
|
211
|
+
var to = run ? run.to : state.offered;
|
|
212
|
+
var versions = isVersion(from) && isVersion(to) ? '<p class="upd-card__ver">' + mono(from) + ' → ' + mono(to) + '</p>' : '';
|
|
213
|
+
var err = (state.error ? '<p class="upd-card__err" role="alert">' + esc(state.error) + '</p>' : '') +
|
|
214
|
+
(state.note ? '<p role="status">' + esc(state.note) + '</p>' : '');
|
|
215
|
+
|
|
216
|
+
if (face === 'held') {
|
|
217
|
+
var msg = (state.held && state.held.message) || ('Version ' + (state.held && state.held.version) + ' was tried on this project and rolled back. It is still on ' + (u.running || 'its current version') + ' and working.');
|
|
218
|
+
return close + '<h2 class="upd-card__h" id="upd-card-h">The newest version did not go in</h2>' +
|
|
219
|
+
'<p>' + esc(msg) + '</p><p class="upd-card__note">Nothing needs doing. The next version will be offered here when it is out.</p>' +
|
|
220
|
+
'<div class="upd-card__actions"><a class="upd-btn" href="' + SETTINGS_HREF + '">Details</a></div>';
|
|
221
|
+
}
|
|
222
|
+
if (face === 'running') {
|
|
223
|
+
var words = {
|
|
224
|
+
starting: 'Starting. The update begins within two minutes.',
|
|
225
|
+
restarting: 'Installing and restarting. This hall may stop answering for a moment.',
|
|
226
|
+
slow: 'This is taking longer than usual. It keeps going on its own.',
|
|
227
|
+
unheard: 'Checking that the update started…',
|
|
228
|
+
};
|
|
229
|
+
return close + '<h2 class="upd-card__h" id="upd-card-h">Updating Bongos</h2>' +
|
|
230
|
+
'<div class="upd-track" aria-hidden="true"><span></span></div>' +
|
|
231
|
+
'<p role="status">' + esc(words[state.stage] || words.starting) + '</p>' + versions +
|
|
232
|
+
'<p class="upd-card__note">You can keep working or close this card. The button up top says when it is done.</p>' +
|
|
233
|
+
(state.stage === 'slow' ? '<div class="upd-card__actions"><a class="upd-btn" href="' + SETTINGS_HREF + '">See where it got to</a></div>' : '');
|
|
234
|
+
}
|
|
235
|
+
if (face === 'done') {
|
|
236
|
+
return close + '<h2 class="upd-card__h" id="upd-card-h">Bongos is updated</h2>' +
|
|
237
|
+
'<p role="status">Reload to start using version ' + esc(to) + '.</p>' +
|
|
238
|
+
'<div class="upd-card__actions"><button type="button" class="upd-btn upd-btn--go" id="upd-reload">Reload</button></div>';
|
|
239
|
+
}
|
|
240
|
+
if (face === 'failed') {
|
|
241
|
+
return close + '<h2 class="upd-card__h" id="upd-card-h">The update did not go in</h2>' +
|
|
242
|
+
'<p role="status">The new version was not healthy, so this project went back to ' + esc(from) + '. It is working.</p>' +
|
|
243
|
+
'<div class="upd-card__actions"><a class="upd-btn" href="' + SETTINGS_HREF + '">Details</a></div>';
|
|
244
|
+
}
|
|
245
|
+
// ready
|
|
246
|
+
if (!state.sw) {
|
|
247
|
+
return close + '<h2 class="upd-card__h" id="upd-card-h">A new version of Bongos is ready</h2>' +
|
|
248
|
+
(state.swError ? '<p class="upd-card__err" role="alert">Could not read what it brings: ' + esc(state.swError) + '.</p>'
|
|
249
|
+
: '<p class="upd-card__note" role="status">Reading what it brings…</p>') + versions;
|
|
250
|
+
}
|
|
251
|
+
var h = highlights((state.sw.update || {}).whats_new, HIGHLIGHTS);
|
|
252
|
+
var brings = '';
|
|
253
|
+
if (h.lines.length) {
|
|
254
|
+
var lead = h.count > h.lines.length ? (h.more ? 'More than ' : '') + h.count + ' improvements, including:' : (h.count === 1 ? 'One improvement:' : h.count + ' improvements:');
|
|
255
|
+
brings = '<p>' + esc(lead) + '</p><ul class="upd-card__list">' + h.lines.map(function (l) { return '<li>' + esc(l) + '</li>'; }).join('') + '</ul>';
|
|
256
|
+
}
|
|
257
|
+
var plan = doorPlan(state.sw);
|
|
258
|
+
var action = '';
|
|
259
|
+
var note = '';
|
|
260
|
+
if (plan.kind === 'local' || plan.kind === 'hub') {
|
|
261
|
+
var host = '';
|
|
262
|
+
if (plan.kind === 'hub') { try { host = new URL(plan.origin).host; } catch (e) { host = ''; } }
|
|
263
|
+
note = '<p class="upd-card__note">Takes a few minutes. The project restarts once, and goes back to the version it runs now on its own if the new one is not healthy.' +
|
|
264
|
+
(host ? ' A small window from ' + esc(host) + ' opens for a moment to start it.' : '') + '</p>';
|
|
265
|
+
action = '<button type="button" class="upd-btn upd-btn--go" id="upd-go">Update now</button>';
|
|
266
|
+
} else if (plan.kind === 'ask') {
|
|
267
|
+
var ask = state.sw.ask || {};
|
|
268
|
+
var team = ask.hub_host ? 'the team at ' + ask.hub_host : 'the team that runs this project\'s server';
|
|
269
|
+
var asked = ask.requested && ask.requested.version === state.offered;
|
|
270
|
+
note = '<p class="upd-card__note">' + esc(asked ? 'You asked ' + team + ' for this update. This shows the new version once it is installed.'
|
|
271
|
+
: 'This project runs on a server that ' + team + ' looks after, so they install its updates.') + '</p>';
|
|
272
|
+
if (!asked) action = '<button type="button" class="upd-btn upd-btn--go" id="upd-ask">Ask for this update</button>';
|
|
273
|
+
} else if (plan.kind === 'cli') {
|
|
274
|
+
note = '<p class="upd-card__note">This project is updated on the server that runs it: <code>bongos upgrade</code> there takes this version, and puts the old one back if it fails.</p>';
|
|
275
|
+
} else {
|
|
276
|
+
note = '<p class="upd-card__note">Only the people allowed to update this project can start it.</p>';
|
|
277
|
+
}
|
|
278
|
+
return close + '<h2 class="upd-card__h" id="upd-card-h">A new version of Bongos is ready</h2>' + brings + note + versions + err +
|
|
279
|
+
'<div class="upd-card__actions">' + action + '<a class="upd-btn" href="' + SETTINGS_HREF + '">What\'s new</a></div>';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// The card's own face: the pill's, except a rollback seen on this page, which the card
|
|
283
|
+
// tells as what just happened while the pill settles on "Update held".
|
|
284
|
+
function cardFace() { return state.face === 'held' && state.stage === 'failed' ? 'failed' : state.face; }
|
|
285
|
+
|
|
286
|
+
function drawCard() {
|
|
287
|
+
if (!doc() || !state.card) return;
|
|
288
|
+
state.card.innerHTML = cardHtml(cardFace());
|
|
289
|
+
wireCard();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function wireCard() {
|
|
293
|
+
var c = state.card;
|
|
294
|
+
var on = function (id, fn) { var el = c.querySelector('#' + id); if (el) el.addEventListener('click', fn); };
|
|
295
|
+
on('upd-close', closeCard);
|
|
296
|
+
on('upd-go', function () { updateNow(); });
|
|
297
|
+
on('upd-ask', function (e) { askForIt(e.currentTarget); });
|
|
298
|
+
on('upd-reload', reload);
|
|
299
|
+
on('upd-open', function () { updateNow(); });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Under the pill, its left edge on the pill's, pulled in where that would run off the
|
|
303
|
+
// right of the window. On a phone the sheet's own rule (16px each side) wins.
|
|
304
|
+
function placeCard() {
|
|
305
|
+
if (!state.card || !state.pill || !state.pill.getBoundingClientRect) return;
|
|
306
|
+
var r = state.pill.getBoundingClientRect();
|
|
307
|
+
var s = state.card.style;
|
|
308
|
+
s.top = Math.round(r.bottom + 8) + 'px';
|
|
309
|
+
var vw = root.innerWidth || 0;
|
|
310
|
+
if (vw <= 640) { s.left = ''; s.right = ''; return; }
|
|
311
|
+
var width = Math.min(360, vw - 32);
|
|
312
|
+
s.left = Math.round(Math.max(16, Math.min(r.left, vw - width - 16))) + 'px';
|
|
313
|
+
s.right = 'auto';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function openCard() {
|
|
317
|
+
var d = doc();
|
|
318
|
+
if (!d) return;
|
|
319
|
+
if (!state.card) {
|
|
320
|
+
var c = d.createElement('div');
|
|
321
|
+
c.id = 'upd-card';
|
|
322
|
+
c.className = 'upd-card';
|
|
323
|
+
c.setAttribute('role', 'dialog');
|
|
324
|
+
c.setAttribute('aria-labelledby', 'upd-card-h');
|
|
325
|
+
d.body.appendChild(c);
|
|
326
|
+
state.card = c;
|
|
327
|
+
if (root.addEventListener) root.addEventListener('resize', function () { if (state.card && !state.card.hidden) placeCard(); });
|
|
328
|
+
d.addEventListener('keydown', function (e) { if (e.key === 'Escape' && state.card && !state.card.hidden) { closeCard(); if (state.pill) state.pill.focus(); } });
|
|
329
|
+
d.addEventListener('click', function (e) {
|
|
330
|
+
if (!state.card || state.card.hidden) return;
|
|
331
|
+
var t = e.target;
|
|
332
|
+
if (state.card.contains(t) || (state.pill && state.pill.contains(t))) return;
|
|
333
|
+
closeCard();
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
state.card.hidden = false;
|
|
337
|
+
if (state.pill) state.pill.setAttribute('aria-expanded', 'true');
|
|
338
|
+
placeCard();
|
|
339
|
+
drawCard();
|
|
340
|
+
var first = state.card.querySelector('.upd-btn--go') || state.card.querySelector('#upd-close');
|
|
341
|
+
if (first && first.focus) first.focus();
|
|
342
|
+
if (state.face === 'ready') readSoftwareUpdate();
|
|
343
|
+
}
|
|
344
|
+
function closeCard() {
|
|
345
|
+
if (state.card) state.card.hidden = true;
|
|
346
|
+
if (state.pill) state.pill.setAttribute('aria-expanded', 'false');
|
|
347
|
+
}
|
|
348
|
+
function toggleCard() { if (state.card && !state.card.hidden) closeCard(); else openCard(); }
|
|
349
|
+
|
|
350
|
+
function setFace(face) {
|
|
351
|
+
state.face = face;
|
|
352
|
+
drawPill();
|
|
353
|
+
if (state.card && !state.card.hidden) drawCard();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// The card's details, read on first open: an update's notes come out of the newer
|
|
357
|
+
// package's download, and most page loads never open the card.
|
|
358
|
+
function readSoftwareUpdate() {
|
|
359
|
+
if (state.sw || state.swAsked) return state.swAsked;
|
|
360
|
+
state.swError = null;
|
|
361
|
+
state.swAsked = state.api.request('GET', '/api/bongos/software-update').then(function (res) {
|
|
362
|
+
if (!res || !res.ok || !res.data) throw new Error((res && res.data && res.data.error && res.data.error.message) || 'HTTP ' + (res && res.status));
|
|
363
|
+
takeReading(res.data);
|
|
364
|
+
}).then(null, function (e) {
|
|
365
|
+
state.swAsked = null;
|
|
366
|
+
state.swError = String((e && e.message) || 'no answer').replace(/\.$/, '');
|
|
367
|
+
if (state.card && !state.card.hidden) drawCard();
|
|
368
|
+
});
|
|
369
|
+
return state.swAsked;
|
|
370
|
+
}
|
|
371
|
+
function takeReading(sw) {
|
|
372
|
+
state.sw = sw;
|
|
373
|
+
var u = sw && sw.update;
|
|
374
|
+
// The version this card offers is the Settings reading's own `offered` (the same channel
|
|
375
|
+
// rule the banner read applies), and only ever one the server named.
|
|
376
|
+
if (u && u.reading === 'ok' && isVersion(u.offered)) state.offered = u.offered;
|
|
377
|
+
if (state.card && !state.card.hidden) drawCard();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function fail(message) {
|
|
381
|
+
state.error = message;
|
|
382
|
+
if (state.face === 'running' && !state.run) state.face = 'ready';
|
|
383
|
+
setFace(state.face);
|
|
384
|
+
if (!state.card || state.card.hidden) openCard();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Step two. Synchronous up to window.open, which a browser allows only inside the press. */
|
|
388
|
+
function updateNow(sw) {
|
|
389
|
+
if (sw) takeReading(sw);
|
|
390
|
+
state.error = null;
|
|
391
|
+
state.note = null;
|
|
392
|
+
var to = state.offered;
|
|
393
|
+
var plan = doorPlan(state.sw);
|
|
394
|
+
if (!isVersion(to) || (plan.kind !== 'local' && plan.kind !== 'hub')) return false;
|
|
395
|
+
state.plan = plan;
|
|
396
|
+
if (plan.kind === 'hub') {
|
|
397
|
+
var w = root.open(plan.url + '&handoff=1', 'bongos-update', 'popup=yes,width=460,height=620');
|
|
398
|
+
if (!w) {
|
|
399
|
+
state.error = 'Your browser blocked the update window.';
|
|
400
|
+
if (!state.card || state.card.hidden) openCard(); else drawCard();
|
|
401
|
+
var go = state.card && state.card.querySelector('#upd-go');
|
|
402
|
+
if (go) { go.id = 'upd-open'; go.textContent = 'Open the update window'; wireCard(); }
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
state.popup = w;
|
|
406
|
+
listen();
|
|
407
|
+
state.stage = 'starting';
|
|
408
|
+
setFace('running');
|
|
409
|
+
watchPopup();
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
state.stage = 'starting';
|
|
413
|
+
setFace('running');
|
|
414
|
+
startLocal(to);
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// The platform's own hall runs the door itself: the same two calls deploy.js makes, with
|
|
419
|
+
// the version this card showed. A preview that names a different version (a release landed
|
|
420
|
+
// in between, or the door's own reading is a few minutes behind the registry's) is never
|
|
421
|
+
// sent: the card names the door's version instead, and the next press takes THAT one, as
|
|
422
|
+
// the hub's window does. Shown, then confirmed; never swapped under the owner.
|
|
423
|
+
function startLocal(to) {
|
|
424
|
+
var api = state.api;
|
|
425
|
+
api.request('GET', '/api/bongos/provisioning/control-plane').then(function (res) {
|
|
426
|
+
var p = res && res.ok && res.data && res.data.preview;
|
|
427
|
+
if (!p || !p.instance_id) throw new Error(errorOf(res, 'This platform has no update door yet.'));
|
|
428
|
+
if (p.recommended_version !== to) {
|
|
429
|
+
if (!isVersion(p.recommended_version)) throw new Error('This platform\'s update door has no version to offer just now.');
|
|
430
|
+
state.offered = p.recommended_version;
|
|
431
|
+
state.note = 'The update door offers version ' + p.recommended_version + ', not the one shown before. Update now takes ' + p.recommended_version + '.';
|
|
432
|
+
state.face = 'ready';
|
|
433
|
+
setFace('ready');
|
|
434
|
+
if (!state.card || state.card.hidden) openCard();
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
return api.request('POST', '/api/bongos/provisioning/instances/' + encodeURIComponent(p.instance_id) + '/core-upgrade', { body: { to: to } });
|
|
438
|
+
}).then(function (res) {
|
|
439
|
+
if (res === null) return;
|
|
440
|
+
if (!res || !res.ok) throw new Error(errorOf(res, 'The update could not be started.'));
|
|
441
|
+
begin(to);
|
|
442
|
+
}).then(null, function (e) { fail(String((e && e.message) || 'The update could not be started.')); });
|
|
443
|
+
}
|
|
444
|
+
// The server's own sentence when it wrote one; its refusals say what to do instead.
|
|
445
|
+
function errorOf(res, fallback) {
|
|
446
|
+
var err = (res && res.data && res.data.error) || {};
|
|
447
|
+
return err.message && err.message !== err.code ? err.message : (res && res.status ? fallback + ' (' + res.status + ')' : fallback);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ---------- the hub window ----------
|
|
451
|
+
|
|
452
|
+
function listen() {
|
|
453
|
+
if (state.listening || !root.addEventListener) return;
|
|
454
|
+
state.listening = true;
|
|
455
|
+
root.addEventListener('message', onMessage);
|
|
456
|
+
}
|
|
457
|
+
/** Only the window this page opened, on the hub's origin, speaking this protocol. */
|
|
458
|
+
function onMessage(e) {
|
|
459
|
+
var plan = state.plan;
|
|
460
|
+
if (!plan || plan.kind !== 'hub' || !state.popup || e.source !== state.popup || e.origin !== plan.origin) return;
|
|
461
|
+
var m = e.data;
|
|
462
|
+
if (!m || m.tag !== MSG) return;
|
|
463
|
+
if (m.step === 'ready') {
|
|
464
|
+
state.popup.postMessage({ tag: MSG, step: 'go', to: state.offered }, plan.origin);
|
|
465
|
+
} else if (m.step === 'started' && m.to === state.offered) {
|
|
466
|
+
begin(state.offered);
|
|
467
|
+
} else if (m.step === 'refused') {
|
|
468
|
+
stopPopupWatch();
|
|
469
|
+
fail(typeof m.message === 'string' && m.message ? m.message.slice(0, 400) : 'The update was refused.');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// A window closed before it said "started". If it could not talk back at all (a browser
|
|
473
|
+
// that cuts the link between the two windows), the owner may still have pressed its own
|
|
474
|
+
// button, so the page watches for a short while before saying it did not start.
|
|
475
|
+
function watchPopup() {
|
|
476
|
+
stopPopupWatch();
|
|
477
|
+
state.popupTimer = root.setInterval(function () {
|
|
478
|
+
var w = state.popup;
|
|
479
|
+
if (state.run) { stopPopupWatch(); return; }
|
|
480
|
+
if (w && !w.closed) return;
|
|
481
|
+
stopPopupWatch();
|
|
482
|
+
if (state.face !== 'running') return;
|
|
483
|
+
state.stage = 'unheard';
|
|
484
|
+
begin(state.offered, { unheard: true });
|
|
485
|
+
}, 700);
|
|
486
|
+
}
|
|
487
|
+
function stopPopupWatch() {
|
|
488
|
+
if (state.popupTimer) root.clearInterval(state.popupTimer);
|
|
489
|
+
state.popupTimer = null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ---------- watching it go in ----------
|
|
493
|
+
|
|
494
|
+
function begin(to, opts) {
|
|
495
|
+
var u = state.update || {};
|
|
496
|
+
stopPopupWatch();
|
|
497
|
+
var unheard = !!(opts && opts.unheard);
|
|
498
|
+
state.run = { to: to, from: u.running || null, at: now(), via: state.plan ? state.plan.kind : null, unheard: unheard };
|
|
499
|
+
if (!unheard) keepRun(store(), state.run);
|
|
500
|
+
state.stage = unheard ? 'unheard' : 'starting';
|
|
501
|
+
setFace('running');
|
|
502
|
+
schedule();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function schedule() {
|
|
506
|
+
if (state.timer) root.clearTimeout(state.timer);
|
|
507
|
+
state.timer = root.setTimeout(look, POLL_MS);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function look() {
|
|
511
|
+
state.timer = null;
|
|
512
|
+
var run = state.run;
|
|
513
|
+
if (!run) return;
|
|
514
|
+
state.looks++;
|
|
515
|
+
var askHold = state.looks % HOLD_EVERY === 0;
|
|
516
|
+
var version = root.fetch('/version', { cache: 'no-store', credentials: 'same-origin' }).then(function (r) {
|
|
517
|
+
return r && r.ok ? r.json().then(function (j) { return j && isVersion(j.coreVersion) ? j.coreVersion : null; }) : null;
|
|
518
|
+
}).then(null, function () { return null; });
|
|
519
|
+
var held = askHold ? state.api.request('GET', '/api/bongos/core-update').then(function (res) {
|
|
520
|
+
return res && res.status === 200 && res.data && res.data.held ? res.data.held.version : null;
|
|
521
|
+
}, function () { return null; }) : null;
|
|
522
|
+
Promise.all([version, held]).then(function (got) {
|
|
523
|
+
if (state.run !== run) return;
|
|
524
|
+
var v = got[0];
|
|
525
|
+
if (v === null) run.down = true;
|
|
526
|
+
var stage = stageOf(run, { version: v, held: got[1] }, now());
|
|
527
|
+
if (run.unheard) {
|
|
528
|
+
// Nothing heard from the window: the first sign of movement makes it a real run;
|
|
529
|
+
// no sign at all inside UNHEARD_MS means it did not start.
|
|
530
|
+
if (stage === 'starting' || stage === 'slow') {
|
|
531
|
+
if (now() - run.at > UNHEARD_MS) {
|
|
532
|
+
state.run = null;
|
|
533
|
+
fail('The update window closed before the update started.');
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
stage = 'unheard';
|
|
537
|
+
} else if (stage !== 'done' && stage !== 'failed') {
|
|
538
|
+
run.unheard = false;
|
|
539
|
+
keepRun(store(), run);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
state.stage = stage;
|
|
543
|
+
if (stage === 'done' || stage === 'failed') {
|
|
544
|
+
forgetRun(store());
|
|
545
|
+
state.run = null;
|
|
546
|
+
state.ended = { to: run.to, from: run.from };
|
|
547
|
+
if (stage === 'failed') {
|
|
548
|
+
// The hold now names this version, so the pill stays as "Update held" and the
|
|
549
|
+
// card says what happened, once, without being asked.
|
|
550
|
+
state.held = { version: run.to };
|
|
551
|
+
setFace('held');
|
|
552
|
+
if (!state.card || state.card.hidden) openCard();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
setFace('done');
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
setFace('running');
|
|
559
|
+
schedule();
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function reload() {
|
|
564
|
+
forgetRun(store());
|
|
565
|
+
if (root.location && root.location.reload) root.location.reload();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// "Ask for this update" (task 1004595): one press reaches the people who run the server.
|
|
569
|
+
function askForIt(btn) {
|
|
570
|
+
btn.disabled = true;
|
|
571
|
+
btn.textContent = 'Asking…';
|
|
572
|
+
state.api.request('POST', '/api/bongos/software-update/request', { body: {} }).then(function (res) {
|
|
573
|
+
var data = (res && res.data) || {};
|
|
574
|
+
var request = data.request || (data.error && data.error.details && data.error.details.request);
|
|
575
|
+
if (!((res.ok || res.status === 429) && request)) throw new Error(errorOf(res, 'Could not send the request.'));
|
|
576
|
+
state.sw.ask = Object.assign({}, state.sw.ask, { requested: request });
|
|
577
|
+
drawCard();
|
|
578
|
+
}).then(null, function (e) {
|
|
579
|
+
state.error = String((e && e.message) || 'Could not send the request.');
|
|
580
|
+
drawCard();
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** shell.js hands over GET /core-update's answer; the pill decides what, if anything, to show. */
|
|
585
|
+
function mount(data, opts) {
|
|
586
|
+
state.api = opts && opts.api;
|
|
587
|
+
if (!state.api || !root.document) return null;
|
|
588
|
+
var d = data || {};
|
|
589
|
+
state.update = d.update || null;
|
|
590
|
+
state.held = d.held || null;
|
|
591
|
+
if (state.update && isVersion(state.update.latest) && !state.offered) state.offered = state.update.latest;
|
|
592
|
+
var run = readRun(store(), now());
|
|
593
|
+
var face = pillState({ update: state.update, held: state.held, run: run });
|
|
594
|
+
if (face === 'landed') { forgetRun(store()); face = 'none'; }
|
|
595
|
+
if (face === 'running') {
|
|
596
|
+
state.run = run;
|
|
597
|
+
state.offered = run.to;
|
|
598
|
+
state.stage = 'starting';
|
|
599
|
+
}
|
|
600
|
+
state.face = face;
|
|
601
|
+
drawPill();
|
|
602
|
+
if (face === 'running') schedule();
|
|
603
|
+
return face;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
var api = {
|
|
607
|
+
mount: mount,
|
|
608
|
+
updateNow: function (sw, opts) {
|
|
609
|
+
if (opts && opts.api) state.api = opts.api;
|
|
610
|
+
if (!state.api) return false;
|
|
611
|
+
if (!state.update && sw && sw.update) state.update = { running: sw.update.running, latest: sw.update.offered, reading: sw.update.reading, behind: !!sw.update.offered };
|
|
612
|
+
if (!state.pill) { state.face = 'ready'; drawPill(); }
|
|
613
|
+
return updateNow(sw);
|
|
614
|
+
},
|
|
615
|
+
// the pure half, for the tests
|
|
616
|
+
highlights: highlights, doorPlan: doorPlan, pillState: pillState, stageOf: stageOf,
|
|
617
|
+
readRun: readRun, keepRun: keepRun, forgetRun: forgetRun, isVersion: isVersion,
|
|
618
|
+
RUN_KEY: RUN_KEY, MSG: MSG, SLOW_MS: SLOW_MS, GIVE_UP_MS: GIVE_UP_MS,
|
|
619
|
+
_state: state,
|
|
620
|
+
};
|
|
621
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
622
|
+
if (root && !root.OTBUpdatePill) root.OTBUpdatePill = api;
|
|
623
|
+
})(typeof window !== 'undefined' ? window : null);
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
<script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
|
|
169
169
|
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
170
170
|
<script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
|
|
171
|
-
<script src="/builders/shell.js?v=2026-
|
|
171
|
+
<script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
|
|
172
172
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
173
173
|
<script src="/builders/palette.js" defer></script>
|
|
174
174
|
<!-- shared component kit (window.OTBKit) — tabs, ledger, pager (task 2166). -->
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
<script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
|
|
175
175
|
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
176
176
|
<script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
|
|
177
|
-
<script src="/builders/shell.js?v=2026-
|
|
177
|
+
<script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
|
|
178
178
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
179
179
|
<script src="/builders/palette.js" defer></script>
|
|
180
180
|
<!-- task 1004727: the ONE karma vote control every task/idea list renders -->
|