@bongos/core 1.19.952 → 1.19.954

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 (50) hide show
  1. package/.bongos-core.json +65 -55
  2. package/docs/adr/0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md +80 -0
  3. package/docs/adr/README.md +1 -0
  4. package/docs/copy-inventory.md +32 -24
  5. package/docs/copy-registry.json +100 -28
  6. package/docs/module-api-changelog.md +4 -0
  7. package/modules/hall-ui/public/atlas.html +1 -1
  8. package/modules/hall-ui/public/blockers.html +1 -1
  9. package/modules/hall-ui/public/board-room.html +1 -1
  10. package/modules/hall-ui/public/collab.html +1 -1
  11. package/modules/hall-ui/public/copy-desk.html +1 -1
  12. package/modules/hall-ui/public/deploy.html +3 -3
  13. package/modules/hall-ui/public/deploy.js +97 -5
  14. package/modules/hall-ui/public/diagrams.html +1 -1
  15. package/modules/hall-ui/public/drachmae.html +1 -1
  16. package/modules/hall-ui/public/gate.html +1 -1
  17. package/modules/hall-ui/public/goal-map.html +1 -1
  18. package/modules/hall-ui/public/goals.html +1 -1
  19. package/modules/hall-ui/public/government.html +1 -1
  20. package/modules/hall-ui/public/idea.html +1 -1
  21. package/modules/hall-ui/public/ideas.html +1 -1
  22. package/modules/hall-ui/public/index.html +1 -1
  23. package/modules/hall-ui/public/modules.html +1 -1
  24. package/modules/hall-ui/public/people.html +1 -1
  25. package/modules/hall-ui/public/primer.html +1 -1
  26. package/modules/hall-ui/public/profile.html +1 -1
  27. package/modules/hall-ui/public/project-settings.css +16 -0
  28. package/modules/hall-ui/public/project-settings.html +1 -1
  29. package/modules/hall-ui/public/ranks.html +1 -1
  30. package/modules/hall-ui/public/roadmap.html +1 -1
  31. package/modules/hall-ui/public/roster.html +1 -1
  32. package/modules/hall-ui/public/sessions.html +1 -1
  33. package/modules/hall-ui/public/settings.html +1 -1
  34. package/modules/hall-ui/public/shell.js +12 -3
  35. package/modules/hall-ui/public/sky.html +1 -1
  36. package/modules/hall-ui/public/studio.html +1 -1
  37. package/modules/hall-ui/public/style.css +6 -0
  38. package/modules/hall-ui/public/task.html +1 -1
  39. package/modules/hall-ui/public/thinking.html +1 -1
  40. package/modules/hall-ui/public/watch.html +1 -1
  41. package/modules/hall-ui/public/work.html +1 -1
  42. package/modules/provisioning/core-upgrade.js +35 -3
  43. package/modules/provisioning/migrations/provisioning_022_last_core_move_index.sql +25 -0
  44. package/package-lock.json +2 -2
  45. package/package.json +1 -1
  46. package/src/module-api.js +1 -1
  47. package/tests/core_update_banner.mjs +30 -2
  48. package/tests/core_upgrade_door.mjs +50 -0
  49. package/tests/deploy_page_projects.mjs +131 -7
  50. package/tests/nav_permission_atoms.mjs +1 -1
@@ -177,7 +177,7 @@
177
177
 
178
178
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
179
179
 
180
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
180
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
181
181
  <script src="/builders/palette.js" defer></script>
182
182
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
183
183
  <script src="/builders/hall-kit.js"></script>
@@ -8,7 +8,7 @@
8
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
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
11
+ <link rel="stylesheet" href="/builders/style.css?v=2026-09-22-movefeedback1">
12
12
  <link rel="stylesheet" href="/builders/oversight.css">
13
13
  <link rel="stylesheet" href="/builders/project-settings.css">
14
14
  </head>
@@ -55,10 +55,10 @@
55
55
 
56
56
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
57
57
 
58
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
58
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
59
59
  <script src="/builders/palette.js" defer></script>
60
60
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
61
61
  <script src="/builders/hall-kit.js"></script>
62
- <script src="/builders/deploy.js?v=2026-09-20-preflight2"></script>
62
+ <script src="/builders/deploy.js?v=2026-09-22-movefeedback1"></script>
63
63
  </body>
64
64
  </html>
@@ -63,6 +63,15 @@
63
63
  const num = (v, dflt) => (Number.isFinite(v) ? v : dflt);
64
64
  const POLL_MS = typeof window !== 'undefined' ? num(window.__deployPollMs, 2000) : 2000;
65
65
  const POLL_TRIES = typeof window !== 'undefined' ? num(window.__deployPollTries, 15) : 15;
66
+ // A MOVE NEEDS A LONGER LEASH THAN A PREVIEW (task 1004145). The intent waits for the
67
+ // runner's 2-minute tick, and then the upgrade itself installs, migrates, restarts and
68
+ // health-checks — so the preview's 30s ceiling would time out on every healthy deploy
69
+ // and report "still running" as if something were wrong. 5 minutes covers the worst
70
+ // case with the tick included.
71
+ const MOVE_TRIES = typeof window !== 'undefined' ? num(window.__deployMoveTries, 150) : 150;
72
+ // Boxes with a move in flight, and what each was asked to move TO — so the row can say
73
+ // which version it is waiting for, and the settled check knows what "worked" means.
74
+ const moving = new Map();
66
75
 
67
76
  async function fetchJson(path) {
68
77
  const res = await api.request('GET', `${API}${path}`);
@@ -109,12 +118,54 @@
109
118
  // Something is already running. The press is not offered at all — offering a button
110
119
  // that the server will refuse with 'intent_conflict' is a worse experience than saying
111
120
  // plainly that the box is busy.
121
+ // NOT "this page will notice when it does" any more (task 1004145). It said that and
122
+ // did not: only a move STARTED from this page polls, and even that stops at its bound.
123
+ // This branch is what an owner sees for someone else's press, or for their own after a
124
+ // reload, and neither is being watched — so it says what is true instead.
112
125
  function busyHtml(p) {
113
126
  const i = p.open_intent;
114
127
  return `
115
128
  <p class="ov-row__sub">A <strong>${escapeHtml(i.action)}</strong> is ${escapeHtml(i.state)} for this box${
116
129
  i.target_version ? ` (to core ${escapeHtml(i.target_version)})` : ''}.
117
- Nothing else can be queued until it settles; this page will notice when it does.</p>`;
130
+ Nothing else can be queued until it settles. Reload to see where it got to.</p>`;
131
+ }
132
+
133
+ // WHAT THE OWNER SEES WHILE IT RUNS. busyHtml already covered "something is running",
134
+ // but only when the server still reports an open intent — and it offered no version,
135
+ // no elapsed sense, and nothing when the intent had settled. This one is driven by the
136
+ // press itself, so it is on screen from the click rather than from the next poll.
137
+ function movingHtml(p) {
138
+ const to = moving.get(p.instance_id);
139
+ const state = (p.open_intent && p.open_intent.state) || 'queued';
140
+ return `
141
+ <p class="ov-row__sub dp-moving">Moving to core <strong>${escapeHtml(String(to))}</strong> — ${escapeHtml(state)}.
142
+ The project restarts as part of this, and puts itself back on
143
+ ${escapeHtml(String(p.served_version))} if the new core does not answer.
144
+ This page is watching and will say when it lands.</p>`;
145
+ }
146
+
147
+ // AND WHAT IT SEES AFTERWARDS. Three outcomes, and the failure is the one that had no
148
+ // surface at all before this: an errored intent is not an OPEN intent, so the payload
149
+ // after a rolled-back upgrade used to be identical to the payload before it.
150
+ function settledHtml(p) {
151
+ const m = p.last_move;
152
+ if (!m) return '';
153
+ if (m.state === 'error') {
154
+ return `
155
+ <div class="dp-settled dp-settled--failed">
156
+ <p class="ov-row__sub"><strong>The move to core ${escapeHtml(String(m.target_version))} did not land</strong>
157
+ — this project was put back on ${escapeHtml(String(p.served_version))} and is running.</p>
158
+ ${m.error ? `<pre class="dp-preflight__error">${escapeHtml(String(m.error))}</pre>` : ''}
159
+ </div>`;
160
+ }
161
+ if (m.state === 'done' && m.target_version === p.served_version) {
162
+ return `
163
+ <div class="dp-settled dp-settled--ok">
164
+ <p class="ov-row__sub"><strong>Now running core ${escapeHtml(String(m.target_version))}</strong>
165
+ — moved ${escapeHtml(ago(m.at))}, and the box confirmed it.</p>
166
+ </div>`;
167
+ }
168
+ return '';
118
169
  }
119
170
 
120
171
  function stepOneHtml(p) {
@@ -232,10 +283,15 @@
232
283
  function boxHtml(p, isSelf) {
233
284
  const step = steps.get(p.instance_id) || 'preview';
234
285
  let action;
235
- if (p.open_intent) action = busyHtml(p);
286
+ // The owner's own press wins over every other state: it is true from the click,
287
+ // before the server has an intent to report, and it must not be replaced by
288
+ // "nothing to deploy" the moment served_version catches up mid-poll.
289
+ if (moving.has(p.instance_id)) action = movingHtml(p);
290
+ else if (p.open_intent) action = busyHtml(p);
236
291
  else if (p.reading === 'no_answer') action = '';
237
292
  else if (!p.behind) action = `<p class="ov-row__sub">Nothing newer is published that this box can take. There is nothing to deploy.</p>`;
238
293
  else action = step === 'confirm' ? stepTwoHtml(p, isSelf) : stepOneHtml(p);
294
+ if (!moving.has(p.instance_id)) action += settledHtml(p);
239
295
  return `
240
296
  <li class="ov-row" id="dp-box-${escapeHtml(String(p.instance_id))}"><div class="ov-row__main">
241
297
  <span class="ov-row__title">${escapeHtml(String(p.slug))}${isSelf ? ' — the box this platform runs on' : ''}</span>
@@ -302,17 +358,53 @@
302
358
  async function move(p) {
303
359
  const btn = $(`dp-confirm-${p.instance_id}`);
304
360
  if (btn) { btn.disabled = true; btn.textContent = 'Queueing…'; }
361
+ const id = String(p.instance_id);
362
+ const to = p.recommended_version;
305
363
  try {
306
364
  // `p.recommended_version` — the version this owner was SHOWN for THIS box, not
307
365
  // whatever is newest at the moment of the click.
308
- const r = await sendJson('POST', `/provisioning/instances/${p.instance_id}/core-upgrade`, { to: p.recommended_version });
366
+ const r = await sendJson('POST', `/provisioning/instances/${p.instance_id}/core-upgrade`, { to });
309
367
  toast(r.message || 'Queued.');
310
- await refreshOne(p);
311
368
  } catch (e) {
312
369
  if (e && e.kind === '401') return;
313
370
  toast(e.message || 'Could not queue the move.');
314
- if (btn) { btn.disabled = false; btn.textContent = `Move to ${p.recommended_version}`; }
371
+ if (btn) { btn.disabled = false; btn.textContent = `Move to ${to}`; }
372
+ return; // nothing was queued, so there is nothing to watch
315
373
  }
374
+ // WATCH IT (task 1004145). busyHtml has always promised "this page will notice when
375
+ // it does", and until now it did not: move() refreshed ONCE, immediately — before
376
+ // the runner's tick had even fired — and then fell silent for the whole upgrade. The
377
+ // owner got a toast, a row that snapped back to its pre-press state, and nothing
378
+ // else, which is indistinguishable from the press having been lost.
379
+ moving.set(p.instance_id, to);
380
+ steps.set(p.instance_id, 'preview'); // the two-step is spent; the row now reports
381
+ repaint();
382
+ let settled = false;
383
+ for (let i = 0; i < MOVE_TRIES; i++) {
384
+ await new Promise((r) => setTimeout(r, POLL_MS));
385
+ if (moving.get(p.instance_id) !== to) return; // superseded or the row is gone
386
+ let now;
387
+ try {
388
+ // keepStep, for the reason the pre-flight poll documents: a refresh that resets
389
+ // the row would fight the state this loop is painting, every interval.
390
+ await refreshOne(p, { keepStep: true });
391
+ now = (view.self && String(view.self.instance_id) === id)
392
+ ? view.self
393
+ : view.boxes.find((b) => String(b.instance_id) === id);
394
+ } catch (e) { break; }
395
+ if (!now) break;
396
+ // Settled two ways, and BOTH need the intent to be closed: served_version can
397
+ // read the new core while the runner is still committing the pin, and calling
398
+ // that "done" would drop the row out of its in-flight state early.
399
+ if (!now.open_intent && now.served_version === to) { settled = true; break; }
400
+ if (!now.open_intent && now.last_move && now.last_move.state === 'error') { settled = true; break; }
401
+ }
402
+ moving.delete(p.instance_id);
403
+ repaint();
404
+ // A poll that ran out is NOT a failure, and must not be painted as one. The upgrade
405
+ // is still the runner's to finish; saying so beats going quiet, which is the exact
406
+ // complaint this task came from.
407
+ if (!settled) toast(`Still working on core ${to}. It keeps going without this page — reload to see where it got to.`);
316
408
  }
317
409
 
318
410
  // Re-read ONLY the box that just changed. The full load() fans out one probe per
@@ -97,7 +97,7 @@
97
97
 
98
98
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
99
99
  time so the chrome exists before the deferred page scripts execute. -->
100
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
100
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
101
101
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
102
102
  <script src="/builders/palette.js" defer></script>
103
103
  <script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
@@ -98,7 +98,7 @@
98
98
 
99
99
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
100
100
 
101
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
101
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
102
102
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
103
103
  <script src="/builders/palette.js" defer></script>
104
104
  <script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
@@ -76,7 +76,7 @@
76
76
  <div id="gate-modal-root"></div>
77
77
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
78
78
 
79
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
79
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
80
80
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
81
81
  <script src="/builders/palette.js" defer></script>
82
82
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -56,7 +56,7 @@
56
56
 
57
57
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
58
58
 
59
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
59
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
60
60
  <script src="/builders/palette.js" defer></script>
61
61
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
62
62
  <script src="/builders/hall-kit.js"></script>
@@ -223,7 +223,7 @@
223
223
 
224
224
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
225
225
 
226
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
226
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
227
227
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
228
228
  <script src="/builders/palette.js" defer></script>
229
229
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -134,7 +134,7 @@
134
134
 
135
135
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
136
136
 
137
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
137
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
138
138
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
139
139
  <script src="/builders/palette.js" defer></script>
140
140
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -52,7 +52,7 @@
52
52
 
53
53
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
54
54
 
55
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
55
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
56
56
  <script src="/builders/palette.js" defer></script>
57
57
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib" defer></script>
58
58
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref
@@ -199,7 +199,7 @@
199
199
 
200
200
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
201
201
 
202
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
202
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
203
203
  <script src="/builders/palette.js" defer></script>
204
204
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
205
205
  <!-- task 1003069 (R08) — the quick-filing form's routed result links a task
@@ -102,7 +102,7 @@
102
102
  <script src="/builders/brand-apply.js?v=2026-06-27-r75" defer></script>
103
103
  <!-- The app shell (sidebar + top bar + view router). Dependency-free; loaded
104
104
  BEFORE builders.js so the chrome ids exist when events are wired. -->
105
- <script src="/builders/shell.js?v=2026-09-20-room1" defer></script>
105
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1" defer></script>
106
106
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
107
107
  <script src="/builders/palette.js" defer></script>
108
108
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
@@ -80,7 +80,7 @@
80
80
 
81
81
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
82
82
 
83
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
83
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
84
84
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
85
85
  <script src="/builders/palette.js" defer></script>
86
86
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
@@ -55,7 +55,7 @@
55
55
 
56
56
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
57
57
  chrome exists before the page script runs. -->
58
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
58
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
59
59
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
60
60
  <script src="/builders/palette.js" defer></script>
61
61
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -115,7 +115,7 @@
115
115
 
116
116
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
117
117
 
118
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
118
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
119
119
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
120
120
  <script src="/builders/palette.js" defer></script>
121
121
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -187,7 +187,7 @@
187
187
 
188
188
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
189
189
  chrome exists before the page script runs. -->
190
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
190
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
191
191
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
192
192
  <script src="/builders/palette.js" defer></script>
193
193
  <!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
@@ -127,3 +127,19 @@
127
127
  white-space: pre-wrap;
128
128
  overflow-x: auto;
129
129
  }
130
+
131
+ /* A move in flight, and how it ended (task 1004145). The owner pressed Upgrade and got
132
+ a toast and then silence; these are the two states that silence was hiding. */
133
+ .dp-moving {
134
+ border-left: 3px solid var(--accent);
135
+ padding-left: 0.7rem;
136
+ }
137
+ .dp-settled { margin: 0.4rem 0 0; }
138
+ .dp-settled--ok {
139
+ border-left: 3px solid var(--ok);
140
+ padding-left: 0.7rem;
141
+ }
142
+ .dp-settled--failed {
143
+ border-left: 3px solid var(--danger);
144
+ padding-left: 0.7rem;
145
+ }
@@ -58,7 +58,7 @@
58
58
 
59
59
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
60
60
 
61
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
61
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
62
62
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
63
63
  <script src="/builders/palette.js" defer></script>
64
64
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -68,7 +68,7 @@
68
68
 
69
69
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
70
70
  chrome exists before the page script runs. -->
71
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
71
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
72
72
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
73
73
  <script src="/builders/palette.js" defer></script>
74
74
  <!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
@@ -41,7 +41,7 @@
41
41
 
42
42
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
43
43
 
44
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
44
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
45
45
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
46
46
  <script src="/builders/palette.js" defer></script>
47
47
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -50,7 +50,7 @@
50
50
 
51
51
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
52
52
  chrome exists before the page script runs. -->
53
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
53
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
54
54
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
55
55
  <script src="/builders/palette.js" defer></script>
56
56
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -99,7 +99,7 @@
99
99
 
100
100
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
101
101
  chrome exists before the page script runs. -->
102
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
102
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
103
103
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
104
104
  <script src="/builders/palette.js" defer></script>
105
105
  <!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
@@ -416,7 +416,7 @@
416
416
 
417
417
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
418
418
  chrome exists before the page scripts run. -->
419
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
419
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
420
420
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
421
421
  <script src="/builders/palette.js" defer></script>
422
422
  <!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
@@ -637,9 +637,18 @@
637
637
  // (probed: /api/bongos/provisioning/health is 404 on a tenant, 200 on the
638
638
  // platform) — so on those the banner informs and does not offer a button that
639
639
  // would 404. Task 1004046 is what gives those halls a real action.
640
- var action = moduleOn('provisioning')
641
- ? '<a class="core-update-notice__action" href="/deploy">Upgrade</a>'
642
- : '';
640
+ // NOT A LINK TO THE PAGE YOU ARE ALREADY ON (task 1004145). This reads as the action
641
+ // button — it is labelled with the verb and styled like one — but it is a plain
642
+ // navigation to /deploy. On /deploy itself that is a no-op, and the owner who
643
+ // pressed it there reasonably reported "I clicked update, but I didn't see anything
644
+ // happen": nothing had, and nothing had been asked for. The control is on that page
645
+ // already, so on it the banner informs and points, and does not pretend to act.
646
+ var onDeploy = window.location && String(window.location.pathname || '').replace(/\/+$/, '') === '/deploy';
647
+ var action = !moduleOn('provisioning')
648
+ ? ''
649
+ : (onDeploy
650
+ ? '<span class="core-update-notice__hint">Use the control below to take it.</span>'
651
+ : '<a class="core-update-notice__action" href="/deploy">Upgrade</a>');
643
652
  el.innerHTML =
644
653
  '<span>Core <strong>' + esc(update.latest) + '</strong> is available — this project runs ' +
645
654
  esc(update.running || 'an unknown version') + '.</span>' +
@@ -65,7 +65,7 @@
65
65
 
66
66
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
67
67
 
68
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
68
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
69
69
  <script src="/builders/palette.js" defer></script>
70
70
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
71
71
  <!-- The SAME shared vocabulary the room loads, at the same version: the server
@@ -66,7 +66,7 @@
66
66
 
67
67
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
68
68
 
69
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
69
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
70
70
  <script src="/builders/palette.js" defer></script>
71
71
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
72
72
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
@@ -897,6 +897,12 @@ main:focus { outline: none; }
897
897
  font-weight: 600;
898
898
  color: inherit;
899
899
  }
900
+ /* The banner on /deploy itself: it still announces the release, but points at the
901
+ control instead of linking to the page the reader is already on (task 1004145). */
902
+ .core-update-notice__hint {
903
+ margin-left: auto;
904
+ color: var(--ink-soft);
905
+ }
900
906
  .core-update-notice__close {
901
907
  min-width: 24px;
902
908
  min-height: 24px;
@@ -57,7 +57,7 @@
57
57
 
58
58
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
59
59
  time so the chrome exists before the deferred page scripts execute. -->
60
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
60
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
61
61
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
62
62
  <script src="/builders/palette.js" defer></script>
63
63
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
@@ -62,7 +62,7 @@
62
62
 
63
63
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
64
64
 
65
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
65
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
66
66
  <script src="/builders/palette.js" defer></script>
67
67
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
68
68
  <!-- The filter strip is the kit's tab strip (makeTabs) rather than a fifth
@@ -162,7 +162,7 @@
162
162
 
163
163
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
164
164
  chrome exists before the page script runs. -->
165
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
165
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
166
166
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
167
167
  <script src="/builders/palette.js" defer></script>
168
168
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
@@ -151,7 +151,7 @@
151
151
 
152
152
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
153
153
  time so the chrome exists before the deferred page scripts execute. -->
154
- <script src="/builders/shell.js?v=2026-09-20-room1"></script>
154
+ <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
155
155
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
156
156
  <script src="/builders/palette.js" defer></script>
157
157
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
@@ -239,7 +239,7 @@ function makeCoreUpgradeHandler({ provisioning, pool, parseId, validateOrRespond
239
239
  * empty one — a control that renders a cache as though it were live is how someone is
240
240
  * told their box is on a version it left an hour ago.
241
241
  */
242
- function previewFromSnapshot(inst, snap, openIntent) {
242
+ function previewFromSnapshot(inst, snap, openIntent, lastMove) {
243
243
  const served = (snap && snap.served_version) || null;
244
244
  const pf = (snap && snap.preflight) || null;
245
245
  const available = (snap && Array.isArray(snap.available) ? snap.available : []);
@@ -266,13 +266,45 @@ function previewFromSnapshot(inst, snap, openIntent) {
266
266
  preflight: pf && recommended && pf.target === recommended ? pf : null,
267
267
  preflight_at: pf && recommended && pf.target === recommended ? (snap.preflight_at || null) : null,
268
268
  open_intent: openIntent ? { action: openIntent.action, state: openIntent.state, target_version: openIntent.target_version || null } : null,
269
+ // THE LAST MOVE, WHATEVER BECAME OF IT (task 1004145).
270
+ //
271
+ // `open_intent` is `state IN ('pending','running')` — by the index's own definition.
272
+ // So an intent that reaches `error` stops being open, and until this field existed
273
+ // the payload after a FAILED upgrade was byte-identical to the payload before it:
274
+ // same served_version, open_intent null, no error anywhere. Intents 77/78/79 on
275
+ // 2026-09-20 all ended "failed and was rolled back" and the page could not say so;
276
+ // the owner's only signal was a number that never changed, which is indistinguishable
277
+ // from never having pressed the button. It is also what lets a SUCCESS be attributed:
278
+ // "the version is different now" and "what you asked for worked" are not the same
279
+ // sentence, and only one of them answers the press.
280
+ last_move: lastMove ? {
281
+ target_version: lastMove.target_version || null,
282
+ state: lastMove.state,
283
+ at: lastMove.resolved_at || lastMove.updated_at || null,
284
+ // Truncated because it reaches a DOM and upgrade.js's refusals can carry a whole
285
+ // `git status` (the dirty-tree one lists every file).
286
+ error: lastMove.last_error ? String(lastMove.last_error).slice(0, 600) : null,
287
+ } : null,
269
288
  };
270
289
  }
271
290
 
291
+ // The newest core-pin MOVE for this box, in any terminal or live state. Deliberately
292
+ // not `getOpenIntent`, which filters to pending|running, and deliberately not widened to
293
+ // `core-upgrade-preflight` — a preview already reports itself through `preflight`, and
294
+ // folding the two together would let a preview's refusal read as a failed deploy.
295
+ async function lastCoreMove(pool, instanceId) {
296
+ const { rows } = await pool.query(
297
+ `SELECT * FROM provisioning_intents
298
+ WHERE instance_id = $1 AND action = 'core-upgrade'
299
+ ORDER BY created_at DESC, id DESC LIMIT 1`,
300
+ [instanceId]);
301
+ return rows[0] || null;
302
+ }
303
+
272
304
  async function loadPreview(pool, provisioning, inst) {
273
305
  const { rows } = await pool.query('SELECT * FROM provisioning_core_version_snapshot WHERE instance_id = $1', [inst.id]);
274
306
  const openIntent = provisioning.getOpenIntent ? await provisioning.getOpenIntent(pool, inst.id) : null;
275
- return previewFromSnapshot(inst, rows[0], openIntent);
307
+ return previewFromSnapshot(inst, rows[0], openIntent, await lastCoreMove(pool, inst.id));
276
308
  }
277
309
 
278
310
  function makeCoreUpgradePreviewHandler({ provisioning, pool, parseId, log }) {
@@ -321,5 +353,5 @@ function makeControlPlaneHandler({ provisioning, pool, log }) {
321
353
  };
322
354
  }
323
355
 
324
- module.exports = { CORE_UPGRADE_FROM, UPGRADE_SHAPES, VERSION_RE, isCoreVersion, coreUpgradeFault, enqueueCoreUpgrade, makeCoreUpgradeHandler, previewFromSnapshot, makeCoreUpgradePreviewHandler, makeControlPlaneHandler };
356
+ module.exports = { CORE_UPGRADE_FROM, lastCoreMove, UPGRADE_SHAPES, VERSION_RE, isCoreVersion, coreUpgradeFault, enqueueCoreUpgrade, makeCoreUpgradeHandler, previewFromSnapshot, makeCoreUpgradePreviewHandler, makeControlPlaneHandler };
325
357
 
@@ -0,0 +1,25 @@
1
+ -- provisioning_022 — an index for "the last core move on this box" (task 1004145).
2
+ --
3
+ -- Numbered 022, not 021: this branch and the byo-host shape change (task 1004141) each
4
+ -- allocated max+1 from their own branch point, and 1004141 landed first. migrate.sh
5
+ -- applies in BASENAME order, so two 021s would have their order decided alphabetically
6
+ -- and a fresh replay could differ from the order prod applied. The one that has NOT been
7
+ -- applied anywhere is the one that renumbers — renaming an applied migration re-runs it.
8
+ --
9
+ -- lastCoreMove() reads:
10
+ -- WHERE instance_id = $1 AND action = 'core-upgrade' ORDER BY created_at DESC LIMIT 1
11
+ --
12
+ -- and neither existing index serves it. `ux_provisioning_intents_open` is partial on
13
+ -- state IN ('pending','running') — this query is ABOUT the terminal rows it excludes —
14
+ -- and `idx_provisioning_intents_pending` is created_at under the same partial predicate.
15
+ -- So the read was a filter-and-sort over every intent for the instance.
16
+ --
17
+ -- It runs on EVERY preview of every box, including the fleet fan-out on /deploy, and
18
+ -- provisioning_intents only grows: one row per press, retained as the audit trail
19
+ -- (ADR 0293 D5). Small today at fewer than a hundred rows, so this is a cheap fix now
20
+ -- rather than a puzzling page later.
21
+ --
22
+ -- DESC on created_at so the LIMIT 1 is the leading edge; id is the tiebreak for two
23
+ -- presses inside the same clock tick, which a test-driven double-press can produce.
24
+ CREATE INDEX IF NOT EXISTS idx_provisioning_intents_last_action
25
+ ON provisioning_intents (instance_id, action, created_at DESC, id DESC);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.952",
3
+ "version": "1.19.954",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.952",
9
+ "version": "1.19.954",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.952",
3
+ "version": "1.19.954",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",