@bongos/core 1.25.3 → 1.25.5
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 +72 -47
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +185 -3
- package/docs/api-reference.md +5 -3
- package/docs/copy-inventory.md +17 -18
- package/docs/copy-registry.json +44 -43
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +43 -43
- package/modules/hall-ui/public/builders.js +3 -27
- package/modules/hall-ui/public/hall-render.js +5 -119
- package/modules/hall-ui/public/hall-widgets.js +3 -3
- package/modules/hall-ui/public/index.html +0 -7
- package/modules/hall-ui/public/shell-game.js +22 -4
- package/modules/hall-ui/public/style.css +3 -73
- package/modules/hall-ui/public/visor.css +31 -1
- package/modules/hall-ui/public/visor.js +53 -3
- package/modules/hall-ui/records/home-reshuffle.md +1 -1
- package/modules/hall-ui/records/visor.md +10 -0
- package/modules/provisioning/migrations/provisioning_037_update_subscribed.sql +21 -0
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/routes/provisioning.js +4 -0
- package/modules/provisioning/routes/update-subscription.js +51 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/bongos/update-channel.js +27 -4
- package/scripts/bongos/update-sweep.js +17 -3
- package/src/bongos/api-errors.js +2 -0
- package/src/bongos/db.js +43 -0
- package/src/bongos/route-rank-check.js +1 -0
- package/src/bongos/routes/builders.js +37 -0
- package/src/module-api.js +1 -1
- package/tests/builder_card_requirement.mjs +75 -0
- package/tests/hall_board_world.mjs +7 -35
- package/tests/hall_home_ships_bounded.mjs +11 -27
- package/tests/hall_landing_boot.mjs +7 -8
- package/tests/hall_nav.mjs +2 -2
- package/tests/hall_visor.mjs +9 -2
- package/tests/hall_visor_profile.mjs +150 -0
- package/tests/leaderboard_roles.mjs +3 -17
- package/tests/provisioning_update_subscription_route.mjs +141 -0
- package/tests/update_channel_db.mjs +24 -0
|
@@ -443,8 +443,19 @@
|
|
|
443
443
|
window.location.assign(menuPath() + (rest || '?from=' + encodeURIComponent(here)));
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
|
-
|
|
447
|
-
|
|
446
|
+
// Your profile in the same window (task 1005249): the visor's you-block opens it over
|
|
447
|
+
// the place, as Options does. Its own page is where a window cannot be drawn.
|
|
448
|
+
function profilePath() { return base() + '/profile'; }
|
|
449
|
+
function openProfile() {
|
|
450
|
+
try { buildOverlay('', { path: profilePath(), title: 'Your profile' }); } catch (_) {
|
|
451
|
+
overlay = null; dialog = null;
|
|
452
|
+
window.location.assign(profilePath());
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// `page` is { path, title } for a page other than the menu; with none, the menu.
|
|
456
|
+
function buildOverlay(rest, page) {
|
|
457
|
+
var title = page ? page.title : 'Options';
|
|
458
|
+
var src = (page ? page.path : menuPath()) + withEmbed(rest);
|
|
448
459
|
if (!overlay) {
|
|
449
460
|
if (!window.OTBDialog || typeof window.OTBDialog.bind !== 'function') throw new Error('no dialog');
|
|
450
461
|
overlay = document.createElement('div');
|
|
@@ -464,7 +475,14 @@
|
|
|
464
475
|
});
|
|
465
476
|
}
|
|
466
477
|
var frame = overlay.querySelector('.opt-window__frame');
|
|
467
|
-
//
|
|
478
|
+
// The window says which page it holds: the box, its close button and the frame.
|
|
479
|
+
var named = [['.opt-window__box', 'aria-label', title], ['.opt-window__x', 'aria-label', 'Close ' + title.toLowerCase()]];
|
|
480
|
+
for (var n = 0; n < named.length; n += 1) {
|
|
481
|
+
var el = overlay.querySelector(named[n][0]);
|
|
482
|
+
if (el && el.setAttribute) el.setAttribute(named[n][1], named[n][2]);
|
|
483
|
+
}
|
|
484
|
+
frame.setAttribute('title', title);
|
|
485
|
+
// A fresh address reloads the page; the same one keeps it as it was left.
|
|
468
486
|
if (frame.getAttribute('src') !== src) frame.setAttribute('src', src);
|
|
469
487
|
dialog.open();
|
|
470
488
|
}
|
|
@@ -477,7 +495,7 @@
|
|
|
477
495
|
// The menu's own page never opens the window (nor a page in a frame: see the if below).
|
|
478
496
|
function overlayHere() { return on() && !(document.body && document.body.getAttribute('data-page') === 'settings'); }
|
|
479
497
|
if (!embedded) {
|
|
480
|
-
window.OTBOptionsOverlay = { open: openOverlay, close: closeOverlay, isOpen: overlayOpen };
|
|
498
|
+
window.OTBOptionsOverlay = { open: openOverlay, openProfile: openProfile, close: closeOverlay, isOpen: overlayOpen };
|
|
481
499
|
// A link to the menu (the visor's Options row, the profile chip, the start
|
|
482
500
|
// screen's Options) opens the window in game mode instead of leaving the place.
|
|
483
501
|
if (document.addEventListener) document.addEventListener('click', function (e) {
|
|
@@ -883,6 +883,7 @@ html[data-theme="dark"] .brand-mark__mark { --mark-ink: #e2e1e4; }
|
|
|
883
883
|
html[data-game="on"] .visor__me, html[data-game="on"] .visor__hints { transform: none; }
|
|
884
884
|
html[data-game="on"] .visor__me { gap: var(--ds-space-hair); }
|
|
885
885
|
html[data-game="on"] .visor__sprite { width: 36px; height: 36px; }
|
|
886
|
+
html[data-game="on"] .visor__card { display: none; }
|
|
886
887
|
html[data-game="on"] .visor__num { font-size: var(--ds-text-4xl); }
|
|
887
888
|
html[data-game="on"] .visor__credits { flex-direction: column; align-items: flex-start; gap: 2px; }
|
|
888
889
|
html[data-game="on"] .visor__unit { font-size: var(--ds-text-2xs); line-height: 1; }
|
|
@@ -4337,36 +4338,6 @@ html[data-door="out"] #hall-main { width: 100%; }
|
|
|
4337
4338
|
box-sizing: border-box;
|
|
4338
4339
|
}
|
|
4339
4340
|
|
|
4340
|
-
/* ---------- Home: the Standing card as the mock draws it (task 1003464) ----------
|
|
4341
|
-
docs/design/mocks/hall/board/v3.html: an identity LINE (a 36px avatar, the
|
|
4342
|
-
name over the handle, the rank on the right) and a three-row fact ledger —
|
|
4343
|
-
Credits, On the board, Preferred work. With .view-zone--band stretching its
|
|
4344
|
-
two cards, this content is what makes Standing end level with Goals. */
|
|
4345
|
-
.home-who { display: flex; align-items: center; gap: 12px; padding-bottom: 14px; }
|
|
4346
|
-
.home-who__avatar {
|
|
4347
|
-
width: 36px; height: 36px; flex: none;
|
|
4348
|
-
border-radius: 50%;
|
|
4349
|
-
background: var(--skeleton);
|
|
4350
|
-
border: 1px solid var(--rule);
|
|
4351
|
-
object-fit: cover;
|
|
4352
|
-
}
|
|
4353
|
-
.home-who__meta { display: flex; flex-direction: column; min-width: 0; }
|
|
4354
|
-
.home-who__name { margin: 0; font-size: 15px; font-weight: 500; color: var(--ink); }
|
|
4355
|
-
.home-who__handle { margin: 0; font-family: var(--font-mono); font-size: 12px; color: var(--ink-faint); }
|
|
4356
|
-
.home-who__rank { margin-left: auto; }
|
|
4357
|
-
.home-facts { margin: 0; display: grid; grid-template-columns: 1fr auto; gap: 0; }
|
|
4358
|
-
.home-facts > div { display: contents; }
|
|
4359
|
-
.home-facts dt, .home-facts dd { margin: 0; padding: 9px 0; border-top: 1px solid var(--rule-soft); }
|
|
4360
|
-
.home-facts dt {
|
|
4361
|
-
font-family: var(--label-face);
|
|
4362
|
-
font-size: var(--label-size);
|
|
4363
|
-
letter-spacing: var(--label-space);
|
|
4364
|
-
text-transform: var(--label-case);
|
|
4365
|
-
color: var(--ink-faint);
|
|
4366
|
-
}
|
|
4367
|
-
.home-facts dd { text-align: right; font-size: 14px; color: var(--ink); font-variant-numeric: tabular-nums; }
|
|
4368
|
-
.home-facts > div:first-child dt, .home-facts > div:first-child dd { border-top: 0; padding-top: 0; }
|
|
4369
|
-
|
|
4370
4341
|
/* ---------- hall-kit components (task 2166) ----------
|
|
4371
4342
|
Styles for the shared kit (hall-kit.js / window.OTBKit): pager bar,
|
|
4372
4343
|
filter-bar disclosure + chips, tab strip, empty state, skeleton shimmer,
|
|
@@ -4857,7 +4828,7 @@ html.palette-open { overflow: hidden; }
|
|
|
4857
4828
|
under it, Standing as a chip at the top right with a hover card, the
|
|
4858
4829
|
Needs-attention slices as cards that open a window, and Your work beside
|
|
4859
4830
|
Recently shipped. */
|
|
4860
|
-
body[data-page="home"] .page-head--hall { position: relative; margin: 0 0 12px;
|
|
4831
|
+
body[data-page="home"] .page-head--hall { position: relative; margin: 0 0 12px; }
|
|
4861
4832
|
body[data-page="home"] .stat-tile { padding: 12px 22px; min-height: 0; gap: 4px; }
|
|
4862
4833
|
body[data-page="home"] .stat-tile__num { font-size: clamp(20px, 2vw, 26px); }
|
|
4863
4834
|
body[data-page="home"] #home-stats .stat-row { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
|
@@ -4875,38 +4846,6 @@ body[data-page="home"] #home-stats .stat-row { border-bottom-left-radius: 0; bor
|
|
|
4875
4846
|
.version-tab__fill { display: block; height: 100%; width: 0; background: var(--accent); border-radius: 4px; }
|
|
4876
4847
|
.version-tab__pct { color: var(--ink-soft); font-size: 13px; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
4877
4848
|
|
|
4878
|
-
.hall-standing { position: absolute; top: 0; right: 0; height: 44px; outline: none; }
|
|
4879
|
-
.hall-standing[hidden] { display: none; }
|
|
4880
|
-
/* The chip IS the top of the card: anchored at the right, it grows LEFT as the
|
|
4881
|
-
name slides in, and only .hall-standing__more drops down below it. */
|
|
4882
|
-
.hall-standing__card {
|
|
4883
|
-
position: absolute; top: 0; right: 0; z-index: 20; min-width: 0;
|
|
4884
|
-
background: var(--bg-card); border-radius: 22px; padding: 4px 6px 4px 4px;
|
|
4885
|
-
transition: min-width .2s ease, border-radius .2s ease, padding .2s ease, box-shadow .2s ease;
|
|
4886
|
-
}
|
|
4887
|
-
.hall-standing__card .home-who { gap: 8px; padding: 0; }
|
|
4888
|
-
.hall-standing__card .home-who__avatar { width: 36px; height: 36px; }
|
|
4889
|
-
.hall-standing__card .home-who__meta {
|
|
4890
|
-
max-width: 0; opacity: 0; overflow: hidden; white-space: nowrap;
|
|
4891
|
-
transition: max-width .2s ease, opacity .2s ease;
|
|
4892
|
-
}
|
|
4893
|
-
.hall-standing__more {
|
|
4894
|
-
max-height: 0; overflow: hidden; opacity: 0;
|
|
4895
|
-
transition: max-height .25s ease, opacity .2s ease;
|
|
4896
|
-
}
|
|
4897
|
-
.hall-standing__more .home-facts { margin-top: 12px; }
|
|
4898
|
-
.hall-standing__more .card-foot { margin: 12px 0 0; }
|
|
4899
|
-
.hall-standing:hover .hall-standing__card,
|
|
4900
|
-
.hall-standing:focus-within .hall-standing__card {
|
|
4901
|
-
min-width: 300px; border-radius: var(--radius); padding: 14px 16px 16px;
|
|
4902
|
-
box-shadow: 0 12px 40px rgba(0, 0, 0, .35);
|
|
4903
|
-
}
|
|
4904
|
-
.hall-standing:hover .home-who__meta,
|
|
4905
|
-
.hall-standing:focus-within .home-who__meta { max-width: 200px; opacity: 1; }
|
|
4906
|
-
.hall-standing:hover .hall-standing__more,
|
|
4907
|
-
.hall-standing:focus-within .hall-standing__more { max-height: 420px; opacity: 1; }
|
|
4908
|
-
.hall-standing:focus-visible .hall-standing__card { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
4909
|
-
|
|
4910
4849
|
.attn-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
|
|
4911
4850
|
.attn-card {
|
|
4912
4851
|
display: flex; flex-direction: column; align-items: flex-start; gap: 6px; min-height: 100px;
|
|
@@ -4926,20 +4865,11 @@ body[data-page="home"] #home-stats .stat-row { border-bottom-left-radius: 0; bor
|
|
|
4926
4865
|
.home-split > .home-split__col[hidden] + .home-split__col { grid-column: 1 / -1; }
|
|
4927
4866
|
|
|
4928
4867
|
@media (max-width: 760px) {
|
|
4929
|
-
body[data-page="home"] .page-head--hall { padding-right: 0; }
|
|
4930
|
-
.hall-standing { position: relative; display: block; margin-top: 10px; }
|
|
4931
|
-
.hall-standing__card { left: 0; right: auto; }
|
|
4932
|
-
.hall-standing:hover .hall-standing__card,
|
|
4933
|
-
.hall-standing:focus-within .hall-standing__card { min-width: min(300px, calc(100vw - 32px)); }
|
|
4934
4868
|
.home-split { grid-template-columns: 1fr; }
|
|
4935
4869
|
}
|
|
4936
4870
|
/* With Standing gone from the band, a lone card there (Goals) takes the row. */
|
|
4937
4871
|
.view-zone--band > .scroll:only-child { grid-column: 1 / -1; }
|
|
4938
|
-
|
|
4939
|
-
.home-facts--grid { grid-template-columns: 1fr 1fr; gap: 10px 14px; padding: 12px 0; border-top: 1px solid var(--rule-soft); border-bottom: 1px solid var(--rule-soft); }
|
|
4940
|
-
.home-facts--grid > div { display: flex; flex-direction: column; gap: 2px; }
|
|
4941
|
-
.home-facts--grid dt, .home-facts--grid dd { border-top: 0; padding: 0; text-align: left; }
|
|
4942
|
-
.home-facts--grid dd { font-family: var(--font-display); font-size: 20px; font-weight: 300; }
|
|
4872
|
+
|
|
4943
4873
|
|
|
4944
4874
|
/* NO TOP BLOCK (task 1004836, owner 2026-10-23). The left rail highlights the
|
|
4945
4875
|
page and the top bar names it, so a page draws nothing above its first
|
|
@@ -86,10 +86,40 @@ html[data-game="on"] .visor:not([hidden]) {
|
|
|
86
86
|
position: absolute; left: var(--visor-inset-left); right: var(--visor-inset-right); bottom: var(--visor-inset-bottom);
|
|
87
87
|
display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; column-gap: var(--ds-space-sm);
|
|
88
88
|
}
|
|
89
|
+
/* you are a link (task 1005249): hover or focus shows your card above, a click opens your profile */
|
|
89
90
|
.visor__me {
|
|
90
|
-
justify-self: start; display: flex; align-items: center; gap: var(--ds-space-sm);
|
|
91
|
+
pointer-events: auto; position: relative; justify-self: start; display: flex; align-items: center; gap: var(--ds-space-sm);
|
|
92
|
+
color: inherit; text-decoration: none; cursor: pointer;
|
|
91
93
|
transform: perspective(900px) rotateY(16deg); transform-origin: left center;
|
|
92
94
|
}
|
|
95
|
+
.visor__me:hover { text-decoration: none; }
|
|
96
|
+
.visor__me:hover .visor__name, .visor__me:focus-visible .visor__name { color: var(--ink); }
|
|
97
|
+
.visor__me:focus-visible { outline: 0; box-shadow: var(--press-focus); }
|
|
98
|
+
/* the card: the sheet's night panel between two strong hairlines, standing over you */
|
|
99
|
+
.visor__card {
|
|
100
|
+
position: absolute; left: 0; bottom: calc(100% + var(--ds-space-xs)); z-index: 1;
|
|
101
|
+
box-sizing: border-box; width: 16rem; padding: var(--ds-space-sm);
|
|
102
|
+
display: flex; flex-direction: column; gap: var(--ds-space-xs);
|
|
103
|
+
background: var(--bg-card); color: var(--ink);
|
|
104
|
+
border: 0 solid var(--rule); border-left-width: var(--line-strong); border-right-width: var(--line-strong);
|
|
105
|
+
opacity: 0; visibility: hidden; transform: translateY(4px);
|
|
106
|
+
transition: opacity var(--ds-dur-fast) var(--ds-ease), transform var(--ds-dur-fast) var(--ds-ease), visibility 0s linear var(--ds-dur-fast);
|
|
107
|
+
}
|
|
108
|
+
.visor__me:hover .visor__card, .visor__me:focus-visible .visor__card {
|
|
109
|
+
opacity: 1; visibility: visible; transform: none; transition-delay: 0s;
|
|
110
|
+
}
|
|
111
|
+
.visor__card-who { display: flex; flex-direction: column; gap: 2px; }
|
|
112
|
+
.visor__card-name { font-size: var(--ds-text-lg); color: var(--ink); }
|
|
113
|
+
.visor__card-handle { font-family: var(--font-mono); font-size: var(--ds-text-sm); color: var(--ink-soft); }
|
|
114
|
+
.visor__facts { display: flex; flex-direction: column; }
|
|
115
|
+
.visor__fact {
|
|
116
|
+
display: flex; align-items: baseline; justify-content: space-between; gap: var(--ds-space-xs);
|
|
117
|
+
padding: var(--ds-space-hair) 0; border-top: var(--ds-line) solid var(--rule-soft);
|
|
118
|
+
}
|
|
119
|
+
.visor__fact-k { font-size: var(--ds-text-sm); color: var(--ink-soft); }
|
|
120
|
+
.visor__fact-v { font-size: var(--ds-text-md); color: var(--ink); text-align: right; font-variant-numeric: tabular-nums; }
|
|
121
|
+
.visor__card-open { font-size: var(--ds-text-sm); color: var(--accent-ink); }
|
|
122
|
+
.visor__card-name, .visor__card-open, .visor__fact-k { font-family: var(--name-face); font-weight: var(--name-weight); text-transform: var(--name-case); letter-spacing: var(--name-space); }
|
|
93
123
|
.visor__sprite {
|
|
94
124
|
flex: none; box-sizing: border-box; width: 56px; height: 56px; object-fit: cover; image-rendering: pixelated;
|
|
95
125
|
background: var(--bg-card); border: var(--line-strong) solid var(--char, var(--ink-soft)); clip-path: var(--cut-path-sm);
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
// - the foot row. Left: your sprite (your character's head, wearing the visor, its rim in
|
|
11
11
|
// the character's colour; task 1005218), your character's name and your credits
|
|
12
12
|
// (builders.total_credits — credits only, never a payout; the spec's "does not exist").
|
|
13
|
+
// That block is you: on hover or focus it shows your details (name, handle, rank,
|
|
14
|
+
// credits, karma, preferred work), and a click opens your whole profile in the game's
|
|
15
|
+
// window over the place (task 1005249; the Standing chip that sat top right of Home
|
|
16
|
+
// went with it).
|
|
13
17
|
// Centre: your character's main action, the one accent button, with its key. Right: the
|
|
14
18
|
// quiet key hints, no boxes: your other actions and "Actions" (key A), which opens the
|
|
15
19
|
// sheet (your character's actions, then the keys of the place you are in, then Options on
|
|
@@ -67,7 +71,9 @@
|
|
|
67
71
|
var AVOID = ['l', '0', '/', '?', 'f', 'k', '+', '-', '='];
|
|
68
72
|
var NAMES = { artisan: 'Artisan', artist: 'Artist', patron: 'Patron' };
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
// The hall's own escaper; without it, only safe characters pass. A link's '/' and a
|
|
75
|
+
// handle's '@' are among them (task 1005249: stripping them broke the profile link).
|
|
76
|
+
function esc(v) { return window.OTB && window.OTB.escapeHtml ? window.OTB.escapeHtml(v) : String(v == null ? '' : v).replace(/[^\w .,:@\/-]/g, ''); }
|
|
71
77
|
function craft() { return window.OTBCraftMode || null; }
|
|
72
78
|
// The character on the glass: the held one, else this session's mode, else none.
|
|
73
79
|
function character() {
|
|
@@ -91,6 +97,22 @@
|
|
|
91
97
|
var n = me && me.builder ? Number(me.builder.total_credits) : NaN;
|
|
92
98
|
return isFinite(n) ? n : null;
|
|
93
99
|
}
|
|
100
|
+
// You, from /me: the hover card's facts. Null until /me answers. Pure, for the tests.
|
|
101
|
+
function meFacts(me) {
|
|
102
|
+
var b = me && me.builder;
|
|
103
|
+
if (!b) return null;
|
|
104
|
+
var karma = Number(b.karma);
|
|
105
|
+
var rank = String(b.rank || '');
|
|
106
|
+
var prefs = Array.isArray(b.preferred_disciplines) ? b.preferred_disciplines.filter(Boolean) : [];
|
|
107
|
+
return {
|
|
108
|
+
name: String(b.display_name || b.github_login || 'Builder'),
|
|
109
|
+
handle: b.github_login ? '@' + String(b.github_login) : '',
|
|
110
|
+
rank: rank ? rank.charAt(0).toUpperCase() + rank.slice(1) : '',
|
|
111
|
+
credits: credits(),
|
|
112
|
+
karma: isFinite(karma) ? Math.round(karma) : 0,
|
|
113
|
+
prefs: prefs.join(', '),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
94
116
|
function creditsText(n) {
|
|
95
117
|
if (n == null) return '';
|
|
96
118
|
var v = Math.round(n);
|
|
@@ -299,11 +321,12 @@
|
|
|
299
321
|
progressHtml() +
|
|
300
322
|
focusHtml() +
|
|
301
323
|
'<div class="visor__foot">' +
|
|
302
|
-
'<
|
|
324
|
+
'<a class="visor__me" data-visor-me href="' + esc(hallHref('/profile')) + '">' +
|
|
303
325
|
(sprite ? '<img class="visor__sprite" src="' + esc(sprite) + '" alt="" width="32" height="32" decoding="async">' : '') +
|
|
304
326
|
'<span class="visor__who"><span class="visor__name">' + esc(NAMES[id]) + '</span>' +
|
|
305
327
|
'<span class="visor__credits" data-visor-credits>' + creditsHtml(n) + '</span></span>' +
|
|
306
|
-
|
|
328
|
+
cardHtml() +
|
|
329
|
+
'</a>' +
|
|
307
330
|
mainHtml(main) +
|
|
308
331
|
'<div class="visor__hints">' + scene.actions.map(function (a, i) { return a.lead ? placeHintHtml(a, i, keys.place[i]) : ''; }).join('') + acts.slice(1).map(hintHtml).join('') +
|
|
309
332
|
'<button class="visor__hint visor__hint--sheet" type="button" data-visor-sheet aria-expanded="false" aria-controls="visor-sheet">' +
|
|
@@ -311,6 +334,24 @@
|
|
|
311
334
|
'</div>' +
|
|
312
335
|
'</div>';
|
|
313
336
|
}
|
|
337
|
+
// The hover card over the you-block (task 1005249): who you are and your standing, then
|
|
338
|
+
// what a click does. Text inside the link, so a reader hears it as part of the link.
|
|
339
|
+
function cardHtml() {
|
|
340
|
+
var C = craft();
|
|
341
|
+
var f = meFacts(C && typeof C.me === 'function' ? C.me() : null);
|
|
342
|
+
if (!f) return '<span class="visor__card"><span class="visor__card-open">Open your profile</span></span>';
|
|
343
|
+
var fact = function (k, v) { return '<span class="visor__fact"><span class="visor__fact-k">' + esc(k) + '</span><span class="visor__fact-v">' + esc(v) + '</span></span>'; };
|
|
344
|
+
return '<span class="visor__card">' +
|
|
345
|
+
'<span class="visor__card-who"><span class="visor__card-name">' + esc(f.name) + '</span>' +
|
|
346
|
+
(f.handle ? '<span class="visor__card-handle">' + esc(f.handle) + '</span>' : '') + '</span>' +
|
|
347
|
+
'<span class="visor__facts">' +
|
|
348
|
+
(f.rank ? fact('Rank', f.rank) : '') +
|
|
349
|
+
fact('Credits', f.credits == null ? '—' : creditsText(f.credits).replace(/ credits?$/, '')) +
|
|
350
|
+
fact('Karma', String(f.karma)) +
|
|
351
|
+
fact('Preferred work', f.prefs || 'Not set yet') +
|
|
352
|
+
'</span>' +
|
|
353
|
+
'<span class="visor__card-open">Open your profile</span></span>';
|
|
354
|
+
}
|
|
314
355
|
// The kit's segments (controls.css .seg), always beside the words that say the same thing.
|
|
315
356
|
function progressHtml() {
|
|
316
357
|
if (!progress) return '';
|
|
@@ -434,6 +475,14 @@
|
|
|
434
475
|
if (t.closest('[data-visor-unfocus]')) { dropFocus(); return; }
|
|
435
476
|
if (t.closest('[data-visor-lead]')) { if (focus && focus.lead && !focus.lead.busy) { closeSheet(false); focus.lead.run(); } return; }
|
|
436
477
|
if (t.closest('[data-visor-sheet]')) { openSheet(); return; }
|
|
478
|
+
// You: your profile in the game's window, the place still drawn behind it. With no
|
|
479
|
+
// window (shell-game.js absent), the link opens the profile page.
|
|
480
|
+
if (t.closest('[data-visor-me]')) {
|
|
481
|
+
var W = window.OTBOptionsOverlay;
|
|
482
|
+
closeSheet(false);
|
|
483
|
+
if (W && typeof W.openProfile === 'function' && !e.metaKey && !e.ctrlKey && !e.shiftKey) { e.preventDefault(); W.openProfile(); }
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
437
486
|
var sc = t.closest('[data-visor-scene]');
|
|
438
487
|
if (sc && scene) { var a = scene.actions[Number(sc.getAttribute('data-visor-scene'))]; closeSheet(false); if (a) a.run(); return; }
|
|
439
488
|
var act = t.closest('[data-visor-act]');
|
|
@@ -508,6 +557,7 @@
|
|
|
508
557
|
keyFor: keyFor,
|
|
509
558
|
resolveKeys: resolveKeys,
|
|
510
559
|
creditsText: creditsText,
|
|
560
|
+
meFacts: meFacts,
|
|
511
561
|
character: character,
|
|
512
562
|
pickVersion: pickVersion,
|
|
513
563
|
segStates: segStates,
|
|
@@ -6,7 +6,7 @@ The owner's Builders' Hall feedback (idea 1001262), home-page half, built to the
|
|
|
6
6
|
- **Banner + version tab.** The stat row is slimmer (`body[data-page="home"]` overrides), and under it
|
|
7
7
|
`#home-version` draws the version the greeting's sub-line names, from the same `currentVersion()`
|
|
8
8
|
read — `renderVersionTab` in `hall-render.js`. Hidden while nothing is being built.
|
|
9
|
-
- **Standing is a chip, not a card.** The `home-standing` widget is gone; `#hall-standing` sits in the
|
|
9
|
+
- **Standing is a chip, not a card.** *(Removed in task 1005249: your details are the visor's you-block now, `records/visor.md`.)* The `home-standing` widget is gone; `#hall-standing` sits in the
|
|
10
10
|
page head (it is chrome, not a `<section class="scroll">`, so the registry rule is untouched). The
|
|
11
11
|
card it opens on hover or keyboard focus is still `#home-standing-body`, filled by `renderProfile`.
|
|
12
12
|
The place row reads "Leaderboard place" — the owner asked for no "on the board" wording. The chip
|
|
@@ -19,6 +19,16 @@ The visor is the control and the scene is the view: every main call to action in
|
|
|
19
19
|
- **The glass binds keys in game mode only.** `onKey` now returns unless `html[data-game="on"]`: the glass was never drawn outside game mode, so its keys no longer act there either (before, a held character's letter keys still ran on a classic page).
|
|
20
20
|
- **The Governor City keeps ADR 0403.** In game mode (`html.visor-on`) the door's button (`#cityStart`) and the archived Street links leave the scene (`city.css`). The door is the lead action, on D, in the door's own words and case ("decide 37 in a row", or "start governing"; lowercase is the door's, task 1005210); it re-registers only when those words change. The archived links are rows in the sheet ("Archived: Street", "Archived: Building Level Views"), read off the page's own archived line so the words and the addresses cannot drift; reached by A and the keyboard. The city line stays on the scene as words, raised to `bottom: 224px` because the extra hint wraps the hints onto a second line at a desk width and the centred accent button rises with it. A building's card is a pop-up, not the scene, and keeps its own buttons and keys. Classic mode is as it was.
|
|
21
21
|
|
|
22
|
+
## You on the glass (task 1005249)
|
|
23
|
+
|
|
24
|
+
- **The Standing chip is gone; you are on the visor.** The owner (2026-11-12): the profile card top right of Home (`#hall-standing`, task 1004774) is removed in both modes, and the you-block bottom left of the visor carries it. Home's two reads that fed only that chip (`/me/recent-ships?limit=5` and `/public/leaderboard`) went with it, so the Standing chip's leaderboard place and "#N as an artist" are on no page now; your shipped count is on the Profile page.
|
|
25
|
+
- **Hover or focus shows your card.** `.visor__me` is a link (`pointer-events: auto`), and above it `.visor__card` shows your name and handle, then Rank, Credits, Karma and Preferred work, then "Open your profile". All from `/me` through `OTBCraftMode.me()` (`meFacts`, pure); no extra read. The card is the sheet's night panel between two strong hairlines. A phone has no hover, so the card is not drawn there (the shared phone block) and a tap goes straight to the profile.
|
|
26
|
+
- **A click opens your whole profile in the game's window**, over the place, the way Options opens: `OTBOptionsOverlay.openProfile()` (`shell-game.js`) frames `/profile?embed=1` in the same `.opt-window`, which now names the page it holds (box, close button, frame title). Escape, the scrim and the close button shut it. With no window to draw, the link opens the profile page.
|
|
27
|
+
- **Keyboard order is reading order.** You are the first stop on the glass, then the accent button.
|
|
28
|
+
- **A fix that came with it.** The visor's fallback escaper (used when the hall's own `OTB.escapeHtml` is not loaded) stripped `/` and `@`, which would have broken the link; it lets both through now.
|
|
29
|
+
|
|
30
|
+
Tests: `tests/hall_visor_profile.mjs` (the facts in a vm; the card, the window, Escape and the plain-link fallback in a real browser).
|
|
31
|
+
|
|
22
32
|
## The sky on the glass (task 1005210)
|
|
23
33
|
|
|
24
34
|
- **What left the sky in game mode** (`thinking.css`, under `html.visor-on`): the top row's lens switch and every pill, the jump field, the thought composer, and the foot's clock and legend. The brand line and its figures stay, as words. A building-like pop-up (the panel, the list, the start dialog, the tour) is not the scene and keeps its buttons.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- provisioning_037_update_subscribed.sql — whether an instance takes unattended patch upgrades,
|
|
2
|
+
-- stored on the instance row (task 1004150, ADR 0325 decision 5).
|
|
3
|
+
--
|
|
4
|
+
-- WHY. Every other fleet fact lives in provisioning_instances; "which instances the nightly sweep
|
|
5
|
+
-- moves on its own" lived only in config/update-subscriptions.json. That left GET /provisioning/fleet
|
|
6
|
+
-- unable to answer the one question a fleet view exists for, and let a restored control-plane backup
|
|
7
|
+
-- bring back every instance row with an empty roster — the sweep then reported failed=0 while
|
|
8
|
+
-- upgrading nothing. On the row, the subscription and its update_channel (provisioning_029) travel
|
|
9
|
+
-- and restore together.
|
|
10
|
+
--
|
|
11
|
+
-- DEFAULT false: an instance is armed only by an explicit act, as the shipped-empty roster was
|
|
12
|
+
-- (ADR 0136). The roster file stays honoured as an override, so an operator's current arming is
|
|
13
|
+
-- not silently dropped.
|
|
14
|
+
--
|
|
15
|
+
-- Additive and namespaced (ADR 0083): no down-migration, idempotent — safe to re-run.
|
|
16
|
+
|
|
17
|
+
BEGIN;
|
|
18
|
+
|
|
19
|
+
ALTER TABLE provisioning_instances ADD COLUMN IF NOT EXISTS update_subscribed boolean NOT NULL DEFAULT false;
|
|
20
|
+
|
|
21
|
+
COMMIT;
|
|
@@ -164,6 +164,10 @@ function ownerInstance(row) {
|
|
|
164
164
|
function fleetInstance(row) {
|
|
165
165
|
return {
|
|
166
166
|
owner: { id: row.owner_builder_id, github_login: row.github_login, display_name: row.display_name, rank: row.rank },
|
|
167
|
+
// Fleet-only (task 1004150): which rows take unattended upgrades, and under what rule. Kept off
|
|
168
|
+
// publicInstance, which feeds owner reads — who auto-upgrades is an operator's question.
|
|
169
|
+
update_channel: row.update_channel || 'patch',
|
|
170
|
+
update_subscribed: row.update_subscribed === true,
|
|
167
171
|
...ownerInstance(row),
|
|
168
172
|
};
|
|
169
173
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// modules/provisioning/routes/update-subscription.js — POST /provisioning/instances/:id/update-subscription
|
|
2
|
+
// (task 1004150, ADR 0325 decision 5).
|
|
3
|
+
//
|
|
4
|
+
// WHY. Whether the nightly sweep moves an instance on its own was only a line in
|
|
5
|
+
// config/update-subscriptions.json, so the fleet roster could not report it and a restored
|
|
6
|
+
// backup brought back every row with an empty roster. The flag lives on the row now
|
|
7
|
+
// (provisioning_037); this is how an operator arms or disarms it. It changes a rule, never the
|
|
8
|
+
// core: nothing is queued.
|
|
9
|
+
//
|
|
10
|
+
// Its own routes file (declared in module.json) because routes/provisioning.js sits at the
|
|
11
|
+
// size ratchet — the routes/repo-private.js precedent.
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const express = require('express');
|
|
15
|
+
const api = require('../../../src/module-api');
|
|
16
|
+
const { pool, validateOrRespond, parseId } = api;
|
|
17
|
+
const provisioning = require('../provisioning');
|
|
18
|
+
const { failFrom } = require('../public-refusal');
|
|
19
|
+
|
|
20
|
+
const log = api.logger('provisioning');
|
|
21
|
+
|
|
22
|
+
module.exports = function buildUpdateSubscriptionRouter() {
|
|
23
|
+
const router = express.Router();
|
|
24
|
+
|
|
25
|
+
// Body: { subscribed: boolean }. Gate: the 'provisioning.fleet.manage' permission — the fleet's own.
|
|
26
|
+
router.post('/provisioning/instances/:id/update-subscription', api.requireBuilder, api.requirePermission('provisioning.fleet.manage'), async (req, res) => {
|
|
27
|
+
const id = parseId(req, res);
|
|
28
|
+
if (!id) return;
|
|
29
|
+
if (validateOrRespond(req, res, { subscribed: { type: 'boolean', required: true } }, { strict: true })) return;
|
|
30
|
+
try {
|
|
31
|
+
const inst = await provisioning.getInstanceById(pool, id);
|
|
32
|
+
if (!inst) return res.fail('instance_not_found', 404);
|
|
33
|
+
const subscribed = req.body.subscribed === true;
|
|
34
|
+
const was = inst.update_subscribed === true;
|
|
35
|
+
if (was !== subscribed) {
|
|
36
|
+
await pool.query('UPDATE provisioning_instances SET update_subscribed = $2, updated_at = now() WHERE id = $1', [id, subscribed]);
|
|
37
|
+
await provisioning.recordEvent(pool, {
|
|
38
|
+
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'update-subscription-changed',
|
|
39
|
+
detail: `unattended patch upgrades ${subscribed ? 'armed' : 'disarmed'} by ${req.builder.github_login || req.builder.id}`,
|
|
40
|
+
actor: `api:fleet:${req.builder.id}`,
|
|
41
|
+
}).catch(() => {});
|
|
42
|
+
}
|
|
43
|
+
res.json({ ok: true, update_subscribed: subscribed, previous: was, changed: was !== subscribed });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
log.error('[provisioning] POST /provisioning/instances/:id/update-subscription', err);
|
|
46
|
+
failFrom(res, err, 'update_subscription_failed');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return router;
|
|
51
|
+
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.5",
|
|
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.25.
|
|
3
|
+
"version": "1.25.5",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -11712,5 +11712,21 @@
|
|
|
11712
11712
|
"id": "1005199",
|
|
11713
11713
|
"text": "Founding a project is faster: each finished stage goes to the Board Room by itself, every stage can be voted on at once with one \"vote yes on all\" button, and the Board Room no longer looks locked during genesis."
|
|
11714
11714
|
}
|
|
11715
|
+
],
|
|
11716
|
+
"1.25.4": [
|
|
11717
|
+
{
|
|
11718
|
+
"id": "1005249",
|
|
11719
|
+
"text": "Your details now sit on your character at the bottom left of the game: hover to see them, click to open your full profile without leaving the scene. The old profile card at the top right is gone."
|
|
11720
|
+
}
|
|
11721
|
+
],
|
|
11722
|
+
"1.25.5": [
|
|
11723
|
+
{
|
|
11724
|
+
"id": "1004748",
|
|
11725
|
+
"text": "Archon can now switch on a card requirement for a builder who has registered a card, with every change recorded."
|
|
11726
|
+
},
|
|
11727
|
+
{
|
|
11728
|
+
"id": "1004150",
|
|
11729
|
+
"text": "The fleet view can now show which projects get automatic patch upgrades, and the nightly upgrade run says so out loud when it has nothing to do."
|
|
11730
|
+
}
|
|
11715
11731
|
]
|
|
11716
11732
|
}
|
|
@@ -333,13 +333,14 @@ function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SU
|
|
|
333
333
|
const CHANNEL_DB_ENV = 'UPDATE_CHANNEL_DB';
|
|
334
334
|
const CHANNEL_SOURCE_DEPLOY = 'deploy';
|
|
335
335
|
const CHANNEL_SOURCE_ROSTER = 'roster';
|
|
336
|
-
const DB_CHANNELS_SQL = 'SELECT slug, update_channel FROM provisioning_instances';
|
|
336
|
+
const DB_CHANNELS_SQL = 'SELECT slug, update_channel, update_subscribed FROM provisioning_instances';
|
|
337
337
|
const DB_CHANNELS_TIMEOUT_MS = 15000;
|
|
338
338
|
|
|
339
339
|
// PURE. psql's stderr reduced to a class that is safe to log.
|
|
340
340
|
function psqlFailureClass(stderr) {
|
|
341
341
|
const s = String(stderr || '');
|
|
342
342
|
if (/relation .*provisioning_instances.* does not exist/i.test(s)) return 'no provisioning_instances table (provisioning module off?)';
|
|
343
|
+
if (/column .*update_subscribed.* does not exist/i.test(s)) return 'no update_subscribed column (migration provisioning_037 not applied)';
|
|
343
344
|
if (/column .*update_channel.* does not exist/i.test(s)) return 'no update_channel column (migration provisioning_029 not applied)';
|
|
344
345
|
if (/authentication failed|role .* does not exist|permission denied/i.test(s)) return 'the sweep\'s database login was refused';
|
|
345
346
|
if (/could not connect|connection refused|no such file or directory|could not translate host/i.test(s)) return 'database unreachable';
|
|
@@ -366,11 +367,32 @@ function readDbChannels({ env = process.env, run = spawnSync } = {}) {
|
|
|
366
367
|
}
|
|
367
368
|
if (r.status !== 0) return { ok: false, reason: `psql exit ${r.status}: ${psqlFailureClass(r.stderr)}` };
|
|
368
369
|
const channels = new Map();
|
|
370
|
+
const subscribed = new Map();
|
|
369
371
|
for (const line of String(r.stdout || '').split('\n')) {
|
|
370
|
-
const [slug, ch] = line.split('\t');
|
|
371
|
-
if (slug
|
|
372
|
+
const [slug, ch, sub] = line.split('\t');
|
|
373
|
+
if (!slug || !ch) continue;
|
|
374
|
+
channels.set(slug.trim(), ch.trim());
|
|
375
|
+
subscribed.set(slug.trim(), String(sub || '').trim() === 't');
|
|
372
376
|
}
|
|
373
|
-
return { ok: true, channels };
|
|
377
|
+
return { ok: true, channels, subscribed };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// PURE. Merge the two places a subscription can be written: the project row
|
|
381
|
+
// (provisioning_instances.update_subscribed, task 1004150) and the roster file. The file is an
|
|
382
|
+
// OVERRIDE — an entry in it is subscribed whatever the row says, so an operator's current arming
|
|
383
|
+
// is not dropped — but a row subscribed with NO file entry can't be upgraded (the file carries the
|
|
384
|
+
// `dir`), and it is returned in `unreachable` rather than vanishing, because "subscribed on the
|
|
385
|
+
// fleet view, never touched by the sweep" is the silent failure this column exists to end.
|
|
386
|
+
function applyDbSubscriptions(instances, db) {
|
|
387
|
+
const subscribed = db && db.ok && db.subscribed instanceof Map ? db.subscribed : null;
|
|
388
|
+
const slugs = new Set(instances.map((i) => i.slug));
|
|
389
|
+
const out = instances.map((inst) => {
|
|
390
|
+
if (!subscribed) return { ...inst, subscribedSource: CHANNEL_SOURCE_ROSTER };
|
|
391
|
+
if (subscribed.get(inst.slug) === true) return { ...inst, subscribedSource: CHANNEL_SOURCE_DEPLOY };
|
|
392
|
+
return { ...inst, subscribedSource: CHANNEL_SOURCE_ROSTER, subscribedNote: 'subscribed by the roster file only (the project row says not subscribed)' };
|
|
393
|
+
});
|
|
394
|
+
const unreachable = subscribed ? [...subscribed].filter(([slug, on]) => on && !slugs.has(slug)).map(([slug]) => slug) : [];
|
|
395
|
+
return { instances: out, unreachable };
|
|
374
396
|
}
|
|
375
397
|
|
|
376
398
|
// PURE. Give each roster entry the channel it is actually under, and say where it came from:
|
|
@@ -496,6 +518,7 @@ module.exports = {
|
|
|
496
518
|
readDbChannels,
|
|
497
519
|
CHANNEL_DB_ENV,
|
|
498
520
|
applyDbChannels,
|
|
521
|
+
applyDbSubscriptions,
|
|
499
522
|
channelLine,
|
|
500
523
|
psqlFailureClass,
|
|
501
524
|
CHANNEL_SOURCE_DEPLOY,
|
|
@@ -593,16 +593,29 @@ function main({ instanceRoot: defaultRoot = process.cwd() } = {}) {
|
|
|
593
593
|
// CONTROL PLANE's database — this process's own env, never an entry's `env`, which points at that
|
|
594
594
|
// instance's database. A core too old to carry the reader keeps the roster, as before.
|
|
595
595
|
const db = typeof channel.readDbChannels === 'function' ? channel.readDbChannels({ env: process.env }) : null;
|
|
596
|
-
const
|
|
597
|
-
if (db && !db.ok &&
|
|
596
|
+
const withChannels = db ? channel.applyDbChannels(loaded.instances, db) : loaded.instances;
|
|
597
|
+
if (db && !db.ok && withChannels.length) console.log(` • update rules: read from the roster — ${db.reason}`);
|
|
598
|
+
// Who is subscribed is also on the project row (task 1004150). The roster file stays an override.
|
|
599
|
+
const merged = db && typeof channel.applyDbSubscriptions === 'function'
|
|
600
|
+
? channel.applyDbSubscriptions(withChannels, db) : { instances: withChannels, unreachable: [] };
|
|
601
|
+
const instances = merged.instances;
|
|
598
602
|
|
|
599
603
|
const roster = only ? instances.filter((i) => i.slug === only || i.dir === only) : instances;
|
|
600
604
|
const mode = apply ? 'apply' : 'dry-run';
|
|
601
605
|
console.log(`core-update-subscription (${mode}) — ${roster.length} subscribed instance(s)${present ? '' : ' [no roster file — nothing subscribed]'}`);
|
|
602
|
-
for (const inst of roster)
|
|
606
|
+
for (const inst of roster) {
|
|
607
|
+
if (inst.channelNote) console.log(channel.channelLine(inst));
|
|
608
|
+
if (inst.subscribedNote) console.log(` • ${inst.slug}: ${inst.subscribedNote}`);
|
|
609
|
+
}
|
|
603
610
|
if (only && roster.length === 0) console.error(` ! --only ${only} matched no subscribed instance`);
|
|
604
611
|
|
|
605
612
|
let upgraded = 0, skipped = 0, failed = 0, upToDate = 0, healed = 0;
|
|
613
|
+
// Subscribed on the project row but absent from this roster: there is no `dir` to upgrade, so say
|
|
614
|
+
// so and count it a skip. Silence here is how a restored backup reported failed=0 on a dead sweep.
|
|
615
|
+
for (const slug of only ? [] : merged.unreachable) {
|
|
616
|
+
console.error(` ! ${slug}: subscribed on its project row, but this roster has no entry with a dir for it — not upgraded`);
|
|
617
|
+
skipped++;
|
|
618
|
+
}
|
|
606
619
|
const refusalsFile = refusalsPath();
|
|
607
620
|
const refusals = readRefusals(refusalsFile);
|
|
608
621
|
|
|
@@ -817,6 +830,7 @@ function main({ instanceRoot: defaultRoot = process.cwd() } = {}) {
|
|
|
817
830
|
upgraded++;
|
|
818
831
|
}
|
|
819
832
|
|
|
833
|
+
if (!only && roster.length === 0 && skipped === 0) console.log('\ncore-update-subscription: NOTHING SUBSCRIBED — no instance was considered, so no upgrade was attempted (this is not a clean run).');
|
|
820
834
|
console.log(`\ncore-update-subscription: upgraded=${upgraded} up-to-date=${upToDate} skipped=${skipped} failed=${failed}${healed ? ` healed=${healed}` : ''} (${mode})`);
|
|
821
835
|
return failed > 0 ? 1 : 0;
|
|
822
836
|
}
|
package/src/bongos/api-errors.js
CHANGED
|
@@ -89,6 +89,8 @@ const OVERRIDES = Object.freeze({
|
|
|
89
89
|
bfg_write_disabled: 'forbidden',
|
|
90
90
|
target_is_system_principal: 'forbidden',
|
|
91
91
|
cannot_change_own_tester: 'forbidden',
|
|
92
|
+
cannot_disable_own_card_requirement: 'forbidden',
|
|
93
|
+
no_card_registered: 'conflict',
|
|
92
94
|
self_vote_forbidden: 'forbidden',
|
|
93
95
|
self_dep: 'conflict',
|
|
94
96
|
newcomer_must_ship_first: 'forbidden',
|