@bongos/core 1.21.145 → 1.21.147

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 (68) hide show
  1. package/.bongos-core.json +98 -68
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +6 -2
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/api/openapi.json +90 -7
  8. package/docs/api-reference.md +3 -2
  9. package/docs/copy-inventory.md +17 -15
  10. package/docs/copy-registry.json +37 -19
  11. package/docs/module-api-changelog.md +4 -0
  12. package/docs/page-readings.json +85 -85
  13. package/modules/builder-settings/core-move-seen.js +41 -0
  14. package/modules/builder-settings/routes/settings.js +22 -0
  15. package/modules/builder-settings/settings.js +5 -0
  16. package/modules/hall-ui/public/atlas.html +1 -1
  17. package/modules/hall-ui/public/blockers.html +1 -1
  18. package/modules/hall-ui/public/board-room.html +1 -1
  19. package/modules/hall-ui/public/city.html +1 -1
  20. package/modules/hall-ui/public/collab.html +1 -1
  21. package/modules/hall-ui/public/commands.html +1 -1
  22. package/modules/hall-ui/public/copy-desk.html +1 -1
  23. package/modules/hall-ui/public/core-move-notice.js +70 -0
  24. package/modules/hall-ui/public/deploy.html +1 -1
  25. package/modules/hall-ui/public/diagrams.html +1 -1
  26. package/modules/hall-ui/public/drachmae.html +1 -1
  27. package/modules/hall-ui/public/fleet.html +1 -1
  28. package/modules/hall-ui/public/gate.html +1 -1
  29. package/modules/hall-ui/public/goal-map.html +1 -1
  30. package/modules/hall-ui/public/goals.html +1 -1
  31. package/modules/hall-ui/public/government.html +1 -1
  32. package/modules/hall-ui/public/idea.html +1 -1
  33. package/modules/hall-ui/public/ideas.html +1 -1
  34. package/modules/hall-ui/public/index.html +1 -1
  35. package/modules/hall-ui/public/modules.html +1 -1
  36. package/modules/hall-ui/public/my-agents.html +1 -1
  37. package/modules/hall-ui/public/primer.html +1 -1
  38. package/modules/hall-ui/public/profile.html +1 -1
  39. package/modules/hall-ui/public/project-settings.html +1 -1
  40. package/modules/hall-ui/public/ranks.html +1 -1
  41. package/modules/hall-ui/public/roadmap.html +1 -1
  42. package/modules/hall-ui/public/roster.html +1 -1
  43. package/modules/hall-ui/public/sessions.html +1 -1
  44. package/modules/hall-ui/public/settings-software-update.js +38 -5
  45. package/modules/hall-ui/public/settings.html +2 -2
  46. package/modules/hall-ui/public/shell.js +18 -6
  47. package/modules/hall-ui/public/street.html +1 -1
  48. package/modules/hall-ui/public/studio.html +1 -1
  49. package/modules/hall-ui/public/style.css +11 -0
  50. package/modules/hall-ui/public/task.html +1 -1
  51. package/modules/hall-ui/public/thinking.html +1 -1
  52. package/modules/hall-ui/public/tweak-editor.html +1 -1
  53. package/modules/hall-ui/public/watch.html +1 -1
  54. package/modules/hall-ui/public/work.html +1 -1
  55. package/package-lock.json +2 -2
  56. package/package.json +1 -1
  57. package/release-notes.json +12 -0
  58. package/scripts/gds/provision-auto-upgrade.js +30 -7
  59. package/scripts/gds/provision-core-upgrade.js +9 -3
  60. package/src/bongos/core-moves.js +155 -0
  61. package/src/bongos/routes/core-update.js +55 -3
  62. package/src/bongos/upgrade-hold.js +96 -0
  63. package/src/module-api.js +1 -1
  64. package/tests/core_auto_upgrade.mjs +64 -3
  65. package/tests/core_move_notice.mjs +356 -0
  66. package/tests/core_upgrade_owner_door.mjs +25 -2
  67. package/tests/nav_permission_atoms.mjs +4 -2
  68. package/tests/upgrade_hold.mjs +207 -0
@@ -0,0 +1,41 @@
1
+ // modules/builder-settings/core-move-seen.js — which core move a builder has already been
2
+ // told about (task 1004731).
3
+ //
4
+ // After the project's core moves, its hall shows a one-time notice ("Upgraded to core X on
5
+ // <date>", and what changed). One-time has to mean once per PERSON, across devices: an owner
6
+ // who read it on their laptop should not be told again on their phone. So, like the profile
7
+ // nudge's dismissal (profile-nudge-prefs.js), it is stored SERVER-side, in one reserved key
8
+ // of the `behavior_prefs` jsonb on `builders` (migration 119), and adds no migration.
9
+ //
10
+ // The value is the id of the core_upgrades ledger row the builder acknowledged. Only the
11
+ // newest row can be a notice, so remembering one id is enough: the next move writes a new
12
+ // row with a new id, and is news again. Core (src/bongos/core-moves.js) never reads this key
13
+ // itself; it asks the `builder-settings` port's `coreMoveSeen`.
14
+
15
+ const RESERVED_KEY = 'core_move_seen';
16
+ const ID_RE = /^\d{1,19}$/;
17
+
18
+ /** The ledger row id this builder has acknowledged, or null. PURE. */
19
+ function seenId(behaviorPrefs) {
20
+ const raw = behaviorPrefs && behaviorPrefs[RESERVED_KEY];
21
+ return typeof raw === 'string' && ID_RE.test(raw) ? raw : null;
22
+ }
23
+
24
+ /** Validate a PATCH body `{ id: "<ledger row id>" }`. PURE. */
25
+ function validatePatch(body) {
26
+ if (body == null || typeof body !== 'object' || Array.isArray(body)) {
27
+ return { ok: false, errors: [{ error: 'body_must_be_object' }] };
28
+ }
29
+ const id = body.id == null ? '' : String(body.id);
30
+ if (!ID_RE.test(id)) return { ok: false, errors: [{ key: 'id', error: 'invalid_value', allowed: 'a core_upgrades row id' }] };
31
+ const extra = Object.keys(body).filter((k) => k !== 'id');
32
+ if (extra.length) return { ok: false, errors: extra.map((key) => ({ key, error: 'unknown_key', allowed: ['id'] })) };
33
+ return { ok: true, id };
34
+ }
35
+
36
+ /** The builder's behavior_prefs with the acknowledged id set; sibling keys untouched. PURE. */
37
+ function applySeen(existing, id) {
38
+ return { ...(existing || {}), [RESERVED_KEY]: id };
39
+ }
40
+
41
+ module.exports = { seenId, validatePatch, applySeen };
@@ -30,6 +30,7 @@ const modelAllocPrefs = require('../model-alloc-prefs');
30
30
  const lawLocks = require('../law-locks');
31
31
  const renderPrefs = require('../render-prefs');
32
32
  const profileNudgePrefs = require('../profile-nudge-prefs');
33
+ const coreMoveSeen = require('../core-move-seen');
33
34
  const displayName = require('../display-name');
34
35
  const filterViews = require('../filter-views');
35
36
  // task 1003208: structured logging (pino via the doorway) — was console.*.
@@ -495,6 +496,27 @@ module.exports = function buildBuilderSettingsRouter() {
495
496
  }
496
497
  });
497
498
 
499
+ // The core move this builder has been told about (task 1004731, core-move-seen.js):
500
+ // closing the hall's "Upgraded to core X" notice stores that move's ledger id here, so
501
+ // it is not shown to them again on any device. Body: { id: "<core_upgrades row id>" }.
502
+ //
503
+ // rank: any-builder — own acknowledgement; same posture as /me/profile-nudge.
504
+ router.patch('/me/core-move-seen', api.requireBuilder, async (req, res) => {
505
+ if (api.validateOrRespond(req, res, {
506
+ id: { required: true, type: 'string', maxLength: 19 },
507
+ }, { strict: true })) return undefined;
508
+ try {
509
+ const v = coreMoveSeen.validatePatch(req.body || {});
510
+ if (!v.ok) return res.fail('invalid_patch', 422, { errors: v.errors });
511
+ const existing = await db.getBuilderBehaviorPrefs(req.builder.id);
512
+ const saved = await db.setBuilderBehaviorPrefs(req.builder.id, coreMoveSeen.applySeen(existing, v.id));
513
+ res.json({ ok: true, seen: coreMoveSeen.seenId(saved) });
514
+ } catch (err) {
515
+ log.error('PATCH /me/core-move-seen', err);
516
+ res.fail('write_failed', { status: 500, message: 'internal error' });
517
+ }
518
+ });
519
+
498
520
  // Per-builder SAVED FILTER VIEWS (task 1004553; shares the behavior_prefs jsonb /
499
521
  // migration 119). A view is a named set of filter values for one hall page. They
500
522
  // are PERSONAL: every route reads and writes only req.builder.id's own prefs, so
@@ -33,6 +33,7 @@ const modelAllocPrefs = require('./model-alloc-prefs');
33
33
  const renderPrefs = require('./render-prefs');
34
34
  const builderNeeds = require('./builder-needs');
35
35
  const lawLocks = require('./law-locks');
36
+ const coreMoveSeen = require('./core-move-seen');
36
37
 
37
38
  module.exports = {
38
39
  // --- the raw behavior_prefs blob read (GET /me resolves this once, then shapes
@@ -53,6 +54,10 @@ module.exports = {
53
54
  // values, not the builder's own.
54
55
  describeWithLaws: (...a) => lawLocks.describe(...a),
55
56
 
57
+ // task 1004731: the core_upgrades row id this builder has acknowledged (the hall's
58
+ // one-time "Upgraded to core X" notice), from the behavior_prefs blob; null if none.
59
+ coreMoveSeen: (behaviorPrefs) => coreMoveSeen.seenId(behaviorPrefs),
60
+
56
61
  // --- the per-builder model ceiling for a skill (cascade-dispatch clamps to it;
57
62
  // never exceeds it). Mirrors the former `skillPrefs.getForBuilder` call.
58
63
  skillCeiling: (builderPrefs, skillName) => skillPrefs.getForBuilder(builderPrefs, skillName),
@@ -93,7 +93,7 @@
93
93
 
94
94
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
95
95
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
96
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
96
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
97
97
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
98
98
  <script src="/builders/palette.js" defer></script>
99
99
  <script src="/builders/atlas.js?v=2026-09-02-index" defer></script>
@@ -52,7 +52,7 @@
52
52
  chrome exists before the page script runs. -->
53
53
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
54
54
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
55
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
55
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
56
56
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
57
57
  <script src="/builders/palette.js" defer></script>
58
58
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -62,7 +62,7 @@
62
62
 
63
63
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
64
64
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
65
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
65
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
66
66
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
67
67
  <script src="/builders/palette.js" defer></script>
68
68
  <script src="/builders/dom-utils.js?v=2026-06-16-board"></script>
@@ -105,7 +105,7 @@
105
105
 
106
106
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
107
107
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
108
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
108
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
109
109
  <script src="/builders/palette.js" defer></script>
110
110
  <script src="/builders/gov-living.js?v=2026-10-09-positions1"></script>
111
111
  <script src="/builders/gov-feeds.js?v=2026-10-09-living3"></script>
@@ -150,7 +150,7 @@
150
150
 
151
151
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
152
152
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
153
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
153
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
154
154
  <script src="/builders/palette.js" defer></script>
155
155
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
156
156
  <script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
@@ -56,7 +56,7 @@
56
56
 
57
57
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
58
58
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
59
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
59
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
60
60
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
61
61
  <script src="/builders/palette.js" defer></script>
62
62
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -179,7 +179,7 @@
179
179
 
180
180
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
181
181
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
182
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
182
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
183
183
  <script src="/builders/palette.js" defer></script>
184
184
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
185
185
  <script src="/builders/hall-kit.js"></script>
@@ -0,0 +1,70 @@
1
+ // modules/hall-ui/public/core-move-notice.js — "Upgraded to core X on <date>", once, after
2
+ // this project's core moved, with the release-note entries between the old and the new
3
+ // version (task 1004731).
4
+ //
5
+ // THE SERVER DECIDES, THIS ONLY DRAWS. GET /core-update carries `moved` only when there is
6
+ // one for THIS builder (src/bongos/core-moves.js: the newest ledger row is a move to the core
7
+ // this server runs, recent, and not yet seen by them), with the sentence already worded.
8
+ //
9
+ // SEEN IS PER BUILDER. Closing it, or following it to Settings → Software update, marks that
10
+ // move seen: on the server (`remember: 'server'` → PATCH /me/core-move-seen, the
11
+ // builder-settings module's behavior_prefs, so no other device shows it again), or in this
12
+ // browser when the server has nowhere to keep it (`remember: 'browser'`).
13
+ //
14
+ // Its own file, loaded by shell.js only when there is a notice to draw (the gov-positions.js
15
+ // way): shell.js sits at the 1500-line ratchet, and most page loads have nothing to say here.
16
+
17
+ (function () {
18
+ 'use strict';
19
+
20
+ var SEEN_KEY = 'bongos.coreMove.seen';
21
+ function esc(s) {
22
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
23
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
24
+ });
25
+ }
26
+
27
+ /** Draw the notice under the top bar. `opts.api` is shell.js's client; `opts.href` Settings. */
28
+ function render(moved, opts) {
29
+ if (!moved || !moved.id || !moved.message) return;
30
+ var api = opts && opts.api;
31
+ var onServer = moved.remember === 'server' && !!api;
32
+ if (!onServer) {
33
+ try {
34
+ if (window.localStorage && window.localStorage.getItem(SEEN_KEY) === String(moved.id)) return;
35
+ } catch (e) { /* no storage: show it */ }
36
+ }
37
+ var topbar = document.getElementById('app-topbar');
38
+ if (!topbar) return;
39
+ var el = document.createElement('div');
40
+ el.className = 'core-update-notice core-move-notice';
41
+ el.id = 'core-move-notice';
42
+ el.setAttribute('role', 'status');
43
+ var entries = Array.isArray(moved.entries) ? moved.entries : [];
44
+ var list = entries.length
45
+ ? '<ul class="core-move-notice__list">' + entries.map(function (e) { return '<li>' + esc(e && e.text) + '</li>'; }).join('') +
46
+ (moved.more > 0 ? '<li>…and ' + esc(String(moved.more)) + ' more.</li>' : '') + '</ul>'
47
+ : '';
48
+ el.innerHTML = '<div class="core-move-notice__body"><span>' + esc(moved.message) +
49
+ (entries.length ? ' What changed:' : '') + '</span>' + list + '</div>' +
50
+ '<a class="core-update-notice__action core-move-notice__more" href="' + esc((opts && opts.href) || '/settings#software-update') + '">Update history</a>' +
51
+ '<button type="button" class="core-update-notice__close core-move-notice__close" aria-label="Dismiss">×</button>';
52
+ function markSeen() {
53
+ try {
54
+ if (window.localStorage) window.localStorage.setItem(SEEN_KEY, String(moved.id));
55
+ } catch (e) { /* best-effort */ }
56
+ if (onServer) {
57
+ api.request('PATCH', '/api/bongos/me/core-move-seen', { body: { id: String(moved.id) } })
58
+ .then(null, function () { /* seen is best-effort; it shows once more at worst */ });
59
+ }
60
+ }
61
+ el.querySelector('.core-move-notice__more').addEventListener('click', markSeen);
62
+ el.querySelector('.core-move-notice__close').addEventListener('click', function () {
63
+ markSeen();
64
+ el.remove();
65
+ });
66
+ topbar.insertAdjacentElement('afterend', el);
67
+ }
68
+
69
+ window.CoreMoveNotice = { render: render };
70
+ })();
@@ -76,7 +76,7 @@
76
76
 
77
77
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
78
78
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
79
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
79
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
80
80
  <script src="/builders/palette.js" defer></script>
81
81
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
82
82
  <script src="/builders/hall-kit.js"></script>
@@ -99,7 +99,7 @@
99
99
  time so the chrome exists before the deferred page scripts execute. -->
100
100
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
101
101
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
102
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
102
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></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
  <script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
@@ -100,7 +100,7 @@
100
100
 
101
101
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
102
102
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
103
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
103
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
104
104
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
105
105
  <script src="/builders/palette.js" defer></script>
106
106
  <script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
@@ -56,7 +56,7 @@
56
56
 
57
57
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
58
58
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
59
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
59
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
60
60
  <script src="/builders/palette.js" defer></script>
61
61
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
62
62
  <script src="/builders/hall-kit.js"></script>
@@ -79,7 +79,7 @@
79
79
 
80
80
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
81
81
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
82
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
82
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
83
83
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
84
84
  <script src="/builders/palette.js" defer></script>
85
85
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -139,7 +139,7 @@
139
139
 
140
140
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
141
141
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
142
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
142
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
143
143
  <script src="/builders/palette.js" defer></script>
144
144
 
145
145
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -232,7 +232,7 @@
232
232
 
233
233
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
234
234
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
235
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
235
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
236
236
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
237
237
  <script src="/builders/palette.js" defer></script>
238
238
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -136,7 +136,7 @@
136
136
 
137
137
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
138
138
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
139
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
139
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
140
140
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
141
141
  <script src="/builders/palette.js" defer></script>
142
142
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -52,7 +52,7 @@
52
52
 
53
53
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
54
54
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
55
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
55
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></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
@@ -227,7 +227,7 @@
227
227
 
228
228
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
229
229
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
230
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
230
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
231
231
  <script src="/builders/palette.js" defer></script>
232
232
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
233
233
  <!-- task 1003069 (R08) — the quick-filing form's routed result links a task
@@ -108,7 +108,7 @@
108
108
  BEFORE builders.js so the chrome ids exist when events are wired. -->
109
109
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab" defer></script>
110
110
  <script src="/builders/craft-mode.js?v=2026-10-09-living3" defer></script>
111
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1" defer></script>
111
+ <script src="/builders/shell.js?v=2026-10-18-core-move1" defer></script>
112
112
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
113
113
  <script src="/builders/palette.js" defer></script>
114
114
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
@@ -103,7 +103,7 @@
103
103
 
104
104
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
105
105
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
106
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
106
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
107
107
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
108
108
  <script src="/builders/palette.js" defer></script>
109
109
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
@@ -61,7 +61,7 @@
61
61
 
62
62
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
63
63
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
64
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
64
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
65
65
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
66
66
  <script src="/builders/palette.js" defer></script>
67
67
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -117,7 +117,7 @@
117
117
 
118
118
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
119
119
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
120
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
120
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
121
121
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
122
122
  <script src="/builders/palette.js" defer></script>
123
123
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -209,7 +209,7 @@
209
209
  chrome exists before the page script runs. -->
210
210
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
211
211
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
212
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
212
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
213
213
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
214
214
  <script src="/builders/palette.js" defer></script>
215
215
  <!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
@@ -72,7 +72,7 @@
72
72
 
73
73
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
74
74
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
75
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
75
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
76
76
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
77
77
  <script src="/builders/palette.js" defer></script>
78
78
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -79,7 +79,7 @@
79
79
  chrome exists before the page script runs. -->
80
80
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
81
81
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
82
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
82
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
83
83
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
84
84
  <script src="/builders/palette.js" defer></script>
85
85
  <!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
@@ -43,7 +43,7 @@
43
43
 
44
44
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
45
45
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
46
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
46
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
47
47
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
48
48
  <script src="/builders/palette.js" defer></script>
49
49
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -55,7 +55,7 @@
55
55
  chrome exists before the page script runs. -->
56
56
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
57
57
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
58
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
58
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></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
@@ -113,7 +113,7 @@
113
113
  chrome exists before the page script runs. -->
114
114
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
115
115
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
116
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
116
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
117
117
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
118
118
  <script src="/builders/palette.js" defer></script>
119
119
  <!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
@@ -166,6 +166,35 @@
166
166
  return `${head}${warn}<ol class="su-versions" aria-labelledby="su-new-h">${items.join('')}</ol>${more ? `<div class="row-end">${more}</div>` : ''}${beyond}`;
167
167
  }
168
168
 
169
+ // A move this project's own ledger says was rolled back (task 1004730): the server's plain
170
+ // sentence, said as it is. It names the version tried, that it was rolled back, and that
171
+ // the project is still on its old version and working; nothing here re-derives it.
172
+ function heldHtml(held) {
173
+ if (!held || !held.message) return '';
174
+ return `<p class="panel-status" role="status">${escapeHtml(held.message)}</p>`;
175
+ }
176
+
177
+ // The short history of moves (task 1004731, src/bongos/core-moves.js): each core this
178
+ // project moved to, when, how it was started, and whether it went in. Only for the people
179
+ // allowed to upgrade (the server sends `history: null` to anyone else). The words for
180
+ // "how" are only as precise as the ledger: a move run on the server by neither the Update
181
+ // button nor an automatic rule is said as exactly that.
182
+ const HOW_WORDS = { automatic: 'Automatic', by_hand: 'By hand, from the Update button', on_server: 'Run on the server' };
183
+ const OUTCOME_WORDS = { upgraded: 'Installed', moved_back: 'Moved back to it', rolled_back: 'Rolled back' };
184
+ function historyHtml(history) {
185
+ if (!Array.isArray(history) || !history.length) return '';
186
+ const items = history.map((m) => {
187
+ const when = m.at ? longDate(m.at) : null;
188
+ const outcome = OUTCOME_WORDS[m.outcome] || OUTCOME_WORDS.upgraded;
189
+ const pill = m.outcome === 'rolled_back' ? 'su-pill' : 'su-pill su-pill--ok';
190
+ return `<li class="su-version">
191
+ <span class="prow__label">${mono(m.version)}${when ? `<span class="prow__tag">${escapeHtml(when)}</span>` : ''}</span>
192
+ <p class="prow__desc">${escapeHtml(HOW_WORDS[m.how] || HOW_WORDS.on_server)}${m.from ? `, from ${mono(m.from)}` : ''}. <span class="${pill}">${escapeHtml(outcome)}</span></p>
193
+ </li>`;
194
+ }).join('');
195
+ return `<h3 class="su-h" id="su-history-h">Recent updates</h3><ol class="su-versions" aria-labelledby="su-history-h">${items}</ol>`;
196
+ }
197
+
169
198
  function render(data) {
170
199
  const body = byId('su-body');
171
200
  const note = byId('su-note');
@@ -177,19 +206,23 @@
177
206
 
178
207
  if (u.reading !== 'ok') {
179
208
  body.innerHTML = row('This project runs', u.running ? mono(u.running) : 'a version it could not name')
180
- + `<p class="panel-status panel-status--danger" role="status">Could not check for a newer version: ${escapeHtml(u.error || 'no answer')}.</p>`;
209
+ + heldHtml(data.held)
210
+ + `<p class="panel-status panel-status--danger" role="status">Could not check for a newer version: ${escapeHtml(u.error || 'no answer')}.</p>`
211
+ + historyHtml(data.history);
181
212
  if (note) note.textContent = notes.join(' ');
182
213
  return;
183
214
  }
184
215
 
185
- let html = row('This project runs', mono(u.running));
216
+ let html = row('This project runs', mono(u.running)) + heldHtml(data.held);
217
+ const onHold = !!(data.held && u.offered && data.held.version === u.offered);
186
218
  if (!u.offered) {
187
219
  html += row('Newest for this project', `${mono(u.running)} — this project is up to date.`, '<span class="su-pill su-pill--ok">Up to date</span>');
188
220
  } else {
189
221
  const when = ago(u.offered_published_at);
190
222
  const pending = data.can_update && data.ask && data.ask.available && askPending(data.ask, u.offered);
191
- html += row('Newest for this project', `${mono(u.offered)}${when ? ` — published ${escapeHtml(when)}` : ''}`, `<span class="su-pill">${pending ? 'Requested' : 'Update ready'}</span>`);
192
- html += doorHtml(data, u.offered);
223
+ html += row('Newest for this project', `${mono(u.offered)}${when ? ` — published ${escapeHtml(when)}` : ''}`, `<span class="su-pill">${onHold ? 'On hold' : pending ? 'Requested' : 'Update ready'}</span>`);
224
+ // No button to take again the very version that was just rolled back here (task 1004730).
225
+ if (!onHold) html += doorHtml(data, u.offered);
193
226
  html += whatsNewHtml(u.whats_new, (data.door || {}).kind === 'local');
194
227
  }
195
228
  // A newer line this project's channel does not take is a fact worth one sentence — it
@@ -197,7 +230,7 @@
197
230
  if (u.newest && u.newest !== (u.offered || u.running) && String(u.newest).split('.')[1] !== String(u.running).split('.')[1]) {
198
231
  notes.push(`${u.newest} is also published; it starts a new line, and this project takes updates within ${String(u.running).split('.').slice(0, 2).join('.')} on its own.`);
199
232
  }
200
- body.innerHTML = html;
233
+ body.innerHTML = html + historyHtml(data.history);
201
234
  if (note) note.textContent = notes.join(' ');
202
235
 
203
236
  const askBtn = byId('su-ask');
@@ -472,7 +472,7 @@
472
472
  chrome exists before the page scripts run. -->
473
473
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
474
474
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
475
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
475
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
476
476
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
477
477
  <script src="/builders/palette.js" defer></script>
478
478
  <!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
@@ -498,7 +498,7 @@
498
498
  settings.js is against the 1500-line ratchet. -->
499
499
  <script src="/builders/settings-render.js?v=2026-09-16-render-levels"></script>
500
500
  <!-- task 1004296: Settings → Software update. Own file on the same precedent. -->
501
- <script src="/builders/settings-software-update.js?v=2026-10-06-ask"></script>
501
+ <script src="/builders/settings-software-update.js?v=2026-10-18-history"></script>
502
502
  <!-- task 1004501: Settings → Your Autobongos runner. Own file on the same precedent. -->
503
503
  <script src="/builders/settings-autobongos.js?v=2026-10-01-autobongos"></script>
504
504
  <!-- task 1003793: Settings → Module install reports. Own file on the same precedent. -->
@@ -774,9 +774,17 @@
774
774
  coreUpdateAsked = true;
775
775
  api.request('GET', '/api/bongos/core-update').then(function (res) {
776
776
  if (!res || res.status !== 200 || !res.data) return;
777
- if (res.data.update) renderCoreUpdateBanner(res.data.update);
777
+ if (res.data.update) renderCoreUpdateBanner(res.data.update, res.data.held);
778
+ if (res.data.moved) renderCoreMoveNotice(res.data.moved); // task 1004731: loads core-move-notice.js, which draws it
778
779
  }, function () { /* fail-soft: no banner is the correct answer to no answer */ });
779
780
  }
781
+ function renderCoreMoveNotice(moved) {
782
+ if (!document.head || !document.head.appendChild) return;
783
+ var tag = document.createElement('script');
784
+ tag.src = '/builders/core-move-notice.js' + (SHELL_V ? '?v=' + SHELL_V : '');
785
+ tag.onload = function () { if (window.CoreMoveNotice) window.CoreMoveNotice.render(moved, { api: api, href: SOFTWARE_UPDATE_HREF }); };
786
+ document.head.appendChild(tag);
787
+ }
780
788
 
781
789
  // Where the banner's Upgrade goes (task 1004296). The platform's hall runs the deploy
782
790
  // door itself, so there it is /deploy. Every other hall — a hosted project, a
@@ -789,7 +797,7 @@
789
797
  // is current AND when the registry could not be read; `reading` separates them,
790
798
  // and neither renders. A banner is a claim about being out of date, and a wrong
791
799
  // one on a project that is actually current is worse than silence.
792
- function renderCoreUpdateBanner(update) {
800
+ function renderCoreUpdateBanner(update, held) {
793
801
  if (!update || update.reading !== 'ok' || !update.behind || !update.latest) return;
794
802
  // A dismissal is remembered PER VERSION, not forever: the next release is new
795
803
  // news and must re-announce itself, or the surface is dead after one click.
@@ -826,10 +834,14 @@
826
834
  : (onUpdatePanel
827
835
  ? '<span class="core-update-notice__hint">Its details are below.</span>'
828
836
  : '<a class="core-update-notice__action" href="' + SOFTWARE_UPDATE_HREF + '">Upgrade</a>');
829
- el.innerHTML =
830
- '<span>Core <strong>' + esc(update.latest) + '</strong> is available — this project runs ' +
831
- esc(update.running || 'an unknown version') + '.</span>' +
832
- action +
837
+ // task 1004730: the newest version already failed here and was rolled back. Say so, never
838
+ // "available" with a button to take it again; Software update carries the full sentence.
839
+ var hold = held && held.version === update.latest ? held : null;
840
+ el.innerHTML = (hold
841
+ ? '<span>Core <strong>' + esc(hold.version) + '</strong> was tried on this project and rolled back. It is still on ' +
842
+ esc(hold.still_on) + ' and working.</span><a class="core-update-notice__action" href="' + SOFTWARE_UPDATE_HREF + '">Details</a>'
843
+ : '<span>Core <strong>' + esc(update.latest) + '</strong> is available — this project runs ' +
844
+ esc(update.running || 'an unknown version') + '.</span>' + action) +
833
845
  '<button type="button" class="core-update-notice__close" aria-label="Dismiss">×</button>';
834
846
  el.querySelector('.core-update-notice__close').addEventListener('click', function () {
835
847
  try {
@@ -230,7 +230,7 @@
230
230
 
231
231
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
232
232
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
233
- <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
233
+ <script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
234
234
  <script src="/builders/palette.js" defer></script>
235
235
  <script src="/builders/gov-living.js?v=2026-10-09-positions1"></script>
236
236
  <script src="/builders/gov-feeds.js?v=2026-10-09-living3"></script>