@bongos/core 1.20.77 → 1.20.78
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 +79 -54
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0136-update-channel-subscription-policy.md +1 -1
- package/docs/adr/0360-bongos-follows-every-candidate-every-other-project-follows-releases.md +66 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +70 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +22 -14
- package/docs/copy-registry.json +125 -41
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +5 -1
- package/docs/page-readings.json +150 -137
- package/docs/recipes/upgrading-the-core.md +11 -2
- package/modules/copy-desk/artist-stats.js +15 -1
- package/modules/copy-desk/module.json +1 -1
- package/modules/copy-desk/page-status.js +41 -0
- package/modules/copy-desk/routes/copy-desk-artist.js +44 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +11 -2
- package/modules/hall-ui/public/deploy.states.json +1 -1
- package/modules/hall-ui/public/profile-crafts.js +231 -0
- package/modules/hall-ui/public/profile.css +60 -0
- package/modules/hall-ui/public/profile.html +31 -12
- package/modules/hall-ui/public/profile.js +180 -75
- package/modules/hall-ui/public/profile.states.json +34 -0
- package/modules/hall-ui/public/sky-draw.js +66 -39
- package/modules/hall-ui/public/sky-interaction.js +1 -1
- package/modules/hall-ui/public/sky-panel.js +1 -1
- package/modules/hall-ui/public/thinking.css +52 -24
- package/modules/hall-ui/records/profile-roles.md +18 -0
- package/modules/npm-release/public/work.js +72 -0
- package/modules/npm-release/work.js +67 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +18 -0
- package/scripts/gds/update-channel.js +68 -1
- package/scripts/gds/update-sweep.js +100 -4
- package/scripts/migrate.sh +6 -2
- package/src/module-api.js +1 -1
- package/tests/hall_sky.mjs +77 -0
- package/tests/helpers.mjs +32 -7
- package/tests/migrate_applied_check.mjs +94 -0
- package/tests/npm_release_page.mjs +67 -0
- package/tests/npm_release_work.mjs +82 -0
- package/tests/profile_activity.mjs +23 -15
- package/tests/profile_role_headline.mjs +4 -4
- package/tests/profile_role_tabs.mjs +163 -0
- package/tests/update_channel.mjs +80 -1
- package/tests/update_subscription_engine.mjs +99 -0
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
if (!h || h.kind === 'shuttle') { card.classList.remove('on'); card.setAttribute('aria-hidden', 'true'); return; }
|
|
32
32
|
const { goalById } = S; let html = '';
|
|
33
33
|
if (h.kind === 'idea') { const i = h.ref; const o = OBJ[i.st] || { noun: i.st, word: i.st }; const wellG = i.well && goalById.get(i.well); const r = reach(i).length; html = `<div class="k">${glyphHtml(i)}<span class="lbl${i.st === 'star' ? ' hot' : ''}">${o.noun} · ${o.word}${i.origin === 'claude' ? ' · filed by a session' : ''}</span></div><div class="t">${esc(i.title)}</div><div class="m">${i.shaped ? `<b>${i.shaped.kind === 'goal' ? 'became this goal' : 'became a ring'}</b> · ` : ''}${wellG ? `caught by <b>${esc(short(wellG.title))}</b>` : `open space · ${Math.round(i.age)} days adrift`} · ${esc(became(i))}${i.size && i.size.credits ? ` · size <b>${i.size.credits}</b>` : ''}${i.lit < 1 ? ` · <b>${Math.round(i.lit * 100)}%</b> alive` : ''}${r ? ' · its reach is drawn' : ''}</div>`; }
|
|
34
|
-
else if (h.kind === 'ring') { const c = h.ref; const g = h.goal; html = `<div class="k"><span class="mk well" style="${c.satisfied ? 'border-style:solid;border-color:rgba(
|
|
34
|
+
else if (h.kind === 'ring') { const c = h.ref; const g = h.goal; html = `<div class="k"><span class="mk well" style="${c.satisfied ? 'border-style:solid;border-color:rgba(var(--sky-ink-rgb),.7)' : ''}"></span><span class="lbl">ring ${h.index + 1} of ${g.crit.length} · ${c.satisfied ? 'closed' : 'open'}</span></div><div class="t">${esc(SKY.plain(c.text).slice(0, 180))}${SKY.plain(c.text).length > 180 ? '…' : ''}</div><div class="m">${c.tasks.length ? `<b>${c.tasks.length}</b> task${c.tasks.length === 1 ? '' : 's'} on this ring · <b>${c.credits}</b> credits` : 'no task gates it yet'}${c.uatLabel ? ` · ${esc(c.uatLabel)}` : (c.pendingReview && !c.uat ? ' · awaiting review' : '')}${c.backendOnly ? ' · backend-only' : ''}${c.bornOf ? ` · <b style="color:var(--accent)">born of a thought</b>: ${esc(ideaTitle(c.bornOf))}` : ''}</div>`; }
|
|
35
35
|
else if (h.kind === 'goal') { const g = h.ref; if (!seen(g)) html = `<div class="k"><span class="mk unseen"></span><span class="lbl">unexplored</span></div><div class="t">Something is here.</div><div class="m">Follow a route from a goal you know to reach it — or turn explore off to see everything.</div>`; else { const met = g.crit.filter((c) => c.satisfied).length; const state = g.status === 'open' ? (g.live > 0 ? 'gravity well · alive' : 'gravity well') : g.status === 'achieved' ? 'settled system' : 'collapsed'; html = `<div class="k"><span class="mk ${g.status === 'open' ? (g.live > 0 ? 'alive' : 'well') : g.status === 'achieved' ? 'settled' : 'collapsed'}"></span><span class="lbl${g.live > 0 ? ' hot' : ''}">${state} · ${esc(g.band.label)}</span></div><div class="t">${esc(g.title)}</div><div class="m"><b>${g.taskDone}</b> of <b>${g.taskTotal}</b> tasks${g.credits ? ` · <b>${g.credits}</b> credits` : ''} · ${g.crit.length ? `<b>${met}</b> of <b>${g.crit.length}</b> rings closed` : 'no rings written'}${g.live > 0 ? ` · <b>${g.active}</b> held now, <b>${g.recent}</b> landed this week` : ''}${g.thoughts.filter(inLayer).length ? ` · <b>${g.thoughts.filter(inLayer).length}</b> of your thoughts` : ''}${g.bornOf ? ` · <b style="color:var(--accent)">born of a thought</b>` : ''}${edgesOf(g).length ? ` · <b>${edgesOf(g).length}</b> route${edgesOf(g).length === 1 ? '' : 's'}` : ''}</div>`; } }
|
|
36
36
|
else if (h.kind === 'task' || h.kind === 'ltask') { const t = h.ref; const kin = t.goal && t.kin ? t.goal.tasks.filter((x) => x.kin === t.kin && x !== t).length : 0; html = `<div class="k"><span class="mk task${t.status === 'active' ? ' live' : ''}"></span><span class="lbl${t.status === 'active' ? ' hot' : ''}">task · ${esc(t.status)}</span></div><div class="t">${esc(t.title)}</div><div class="m">${t.credits != null ? `<b>${t.credits}</b> credits · ` : ''}${t.ci >= 0 ? `ring ${t.ci + 1}` : 'outer orbit'}${t.sourceRef ? ' · born of a thought' : ''}${kin ? ` · lines up with <b>${kin}</b> kin` : ''}${t.drift ? ' · in motion' : ''}</div>`; }
|
|
37
37
|
else if (h.kind === 'stub') { const e = h.edge; const waits = e.a === h.goal; html = `<div class="k"><span class="mk route"></span><span class="lbl chart">route · ${waits ? 'waits on' : 'waited on by'}</span></div><div class="t">${seen(h.other) ? esc(h.other.title) : 'an unexplored goal'}</div><div class="m"><b>${e.count}</b> task dependenc${e.count === 1 ? 'y' : 'ies'} · click to chart it · double-click to chart every route from here</div>`; }
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
${rch.length ? fact('lives on in', rch.map((g) => `<a href="#" data-goal="${esc(g.id)}">${esc(short(g.title))}</a>`).join(', ')) : ''}
|
|
40
40
|
${fact('size — credits', i.size ? `${i.size.credits}${i.size.historical ? ' <span class="lbl">hist.</span>' : ''}` : 'unknown', true)}
|
|
41
41
|
${i.size ? fact('of which', esc(sizeWords(i))) : ''}
|
|
42
|
-
${bright ? fact('brightness — still alive', `<span class="bar" style="width:${Math.max(4, Math.round(i.lit * 70))}px;${i.lit > 0 ? '' : 'background:rgba(
|
|
42
|
+
${bright ? fact('brightness — still alive', `<span class="bar" style="width:${Math.max(4, Math.round(i.lit * 70))}px;${i.lit > 0 ? '' : 'background:rgba(var(--sky-ink-rgb),.2)'}"></span>${esc(bright)}`) : ''}
|
|
43
43
|
${pos ? fact('position', esc(pos)) : ''}
|
|
44
44
|
${fact('filed', esc(fmtDate(i.capturedAt)) + (i.kind ? ` · ${esc(i.kind)}` : '') + (i.grade === 'full' ? ' · full idea' : '') + ' · ' + esc(SKY.viaWord(i.filedVia)) + (i.origin === 'claude' ? ' · by a session, under your name' : ''))}
|
|
45
45
|
</div>
|
|
@@ -24,11 +24,39 @@
|
|
|
24
24
|
body[data-page] .sky-stage {
|
|
25
25
|
color-scheme: dark;
|
|
26
26
|
--void: #000000; --ink: #ffffff; --ink-soft: #c0c0c0; --ink-faint: #a8a8a8; --ink-dim: #65656a; --ink-ghost: #3a3a3f;
|
|
27
|
-
--rule: rgba(
|
|
27
|
+
--rule: rgba(var(--sky-ink-rgb),.16); --rule-soft: rgba(var(--sky-ink-rgb),.08); --glass: rgba(var(--sky-ink-rgb),.075); --glass-strong: rgba(var(--sky-ink-rgb),.14);
|
|
28
28
|
--accent: #fb9569; --accent-on: #2a1305; --chart: #f2d27a;
|
|
29
|
+
--sky-ink-rgb: 255,255,255; --sky-void-rgb: 0,0,0; --sky-accent-rgb: 251,149,105; --sky-chart-rgb: 242,210,122;
|
|
30
|
+
--sky-cold: #141418; --sky-cold-rgb: 20,20,24; --sky-drop: rgba(0,0,0,.6); --sky-temp: 1;
|
|
29
31
|
--label: 400 11px/1.2 "JetBrains Mono", ui-monospace, Consolas, monospace; --mono: 400 13px/1.4 "JetBrains Mono", ui-monospace, Consolas, monospace;
|
|
30
32
|
--face: Manrope, "Segoe UI", system-ui, -apple-system, sans-serif;
|
|
31
33
|
}
|
|
34
|
+
/* LIGHT (task 1004500, owner ask 2026-10-01). The sky follows the hall's theme:
|
|
35
|
+
the void becomes paper and the white ladder becomes an ink ladder at the SAME
|
|
36
|
+
alphas, so every mark keeps its place in the hierarchy; the accent and the
|
|
37
|
+
chart yellow darken until they read on paper. Dark above is the template's,
|
|
38
|
+
untouched. Written twice for the same reason style.css writes dark twice:
|
|
39
|
+
html[data-theme] once shell.js runs, the media query for first paint.
|
|
40
|
+
sky-draw.js reads these channels (--sky-*-rgb) off the stage, so the canvas
|
|
41
|
+
repaints with the toggle. */
|
|
42
|
+
html[data-theme="light"] body[data-page] .sky-stage {
|
|
43
|
+
color-scheme: light;
|
|
44
|
+
--void: #f4f3f5; --ink: #17181a; --ink-soft: #3f4044; --ink-faint: #55565a; --ink-dim: #6c6d72; --ink-ghost: #a3a4a8;
|
|
45
|
+
--rule: rgba(23,24,26,.16); --rule-soft: rgba(23,24,26,.08); --glass: rgba(23,24,26,.06); --glass-strong: rgba(23,24,26,.11);
|
|
46
|
+
--accent: #b8501f; --accent-on: #ffffff; --chart: #8f6a0c;
|
|
47
|
+
--sky-ink-rgb: 23,24,26; --sky-void-rgb: 244,243,245; --sky-accent-rgb: 184,80,31; --sky-chart-rgb: 143,106,12;
|
|
48
|
+
--sky-cold: #dcdbde; --sky-cold-rgb: 220,219,222; --sky-drop: rgba(23,24,26,.14); --sky-temp: .42;
|
|
49
|
+
}
|
|
50
|
+
@media (prefers-color-scheme: light) {
|
|
51
|
+
html:not([data-theme]) body[data-page] .sky-stage {
|
|
52
|
+
color-scheme: light;
|
|
53
|
+
--void: #f4f3f5; --ink: #17181a; --ink-soft: #3f4044; --ink-faint: #55565a; --ink-dim: #6c6d72; --ink-ghost: #a3a4a8;
|
|
54
|
+
--rule: rgba(23,24,26,.16); --rule-soft: rgba(23,24,26,.08); --glass: rgba(23,24,26,.06); --glass-strong: rgba(23,24,26,.11);
|
|
55
|
+
--accent: #b8501f; --accent-on: #ffffff; --chart: #8f6a0c;
|
|
56
|
+
--sky-ink-rgb: 23,24,26; --sky-void-rgb: 244,243,245; --sky-accent-rgb: 184,80,31; --sky-chart-rgb: 143,106,12;
|
|
57
|
+
--sky-cold: #dcdbde; --sky-cold-rgb: 220,219,222; --sky-drop: rgba(23,24,26,.14); --sky-temp: .42;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
32
60
|
html, body { height: 100%; }
|
|
33
61
|
body[data-page] .sky-stage { margin: 0; background: var(--void); color: var(--ink); font-family: var(--face); font-size: 15px; line-height: 1.5; overflow: hidden; overflow: clip; -webkit-font-smoothing: antialiased; }
|
|
34
62
|
/* clip, not just hidden (task 1004342): a hidden-overflow box is still scrollable, so
|
|
@@ -100,10 +128,10 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
|
|
|
100
128
|
.pill[aria-pressed="true"]::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); margin-right: 8px; vertical-align: 1px; }
|
|
101
129
|
.pill.icon { width: 36px; padding: 0; display: inline-grid; place-items: center; }
|
|
102
130
|
.pill.icon::before { display: none; }
|
|
103
|
-
.spot { outline: 2px solid var(--accent) !important; outline-offset: 4px; box-shadow: 0 0 0 8px rgba(
|
|
131
|
+
.spot { outline: 2px solid var(--accent) !important; outline-offset: 4px; box-shadow: 0 0 0 8px rgba(var(--sky-accent-rgb),.12), 0 0 40px rgba(var(--sky-accent-rgb),.35); }
|
|
104
132
|
|
|
105
133
|
.composer { position: fixed; left: 50%; top: calc(76px + env(safe-area-inset-top, 0px)); transform: translateX(-50%); width: min(620px, calc(100% - 32px)); z-index: 6; display: flex; flex-direction: column; gap: 8px; align-items: center; }
|
|
106
|
-
.composer form { display: flex; width: 100%; height: 48px; border-radius: 999px; background: rgba(
|
|
134
|
+
.composer form { display: flex; width: 100%; height: 48px; border-radius: 999px; background: rgba(var(--sky-void-rgb),.5); box-shadow: inset 0 0 0 1px var(--rule); backdrop-filter: blur(6px); align-items: center; padding: 0 6px 0 22px; gap: 8px; }
|
|
107
135
|
.composer form:focus-within { box-shadow: inset 0 0 0 1px var(--ink-faint); }
|
|
108
136
|
.composer input { flex: 1; min-width: 0; height: 100%; background: transparent; border: 0; color: var(--ink); font: 300 17px/1 var(--face); letter-spacing: -.01em; outline: none; }
|
|
109
137
|
.composer input::placeholder { color: var(--ink-dim); }
|
|
@@ -112,12 +140,12 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
|
|
|
112
140
|
.gravity { min-height: 18px; font: var(--mono); font-variant-numeric: tabular-nums; color: var(--ink-dim); text-align: center; }
|
|
113
141
|
.gravity b { font-weight: 400; color: var(--ink-soft); }
|
|
114
142
|
.gravity .hot { color: var(--accent); }
|
|
115
|
-
.receipt { display: flex; align-items: center; gap: 12px; height: 34px; padding: 0 8px 0 16px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--rule-soft); background: rgba(
|
|
116
|
-
.receipt .dot { width: 7px; height: 7px; border-radius: 50%; background: rgba(
|
|
143
|
+
.receipt { display: flex; align-items: center; gap: 12px; height: 34px; padding: 0 8px 0 16px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--rule-soft); background: rgba(var(--sky-void-rgb),.45); color: var(--ink-soft); font-size: 13px; }
|
|
144
|
+
.receipt .dot { width: 7px; height: 7px; border-radius: 50%; background: rgba(var(--sky-ink-rgb),.5); box-shadow: 0 0 0 4px rgba(var(--sky-ink-rgb),.05); }
|
|
117
145
|
.receipt button { border: 0; background: transparent; color: var(--ink-dim); cursor: pointer; height: 26px; padding: 0 8px; border-radius: 999px; font-size: 12px; white-space: nowrap; flex-shrink: 0; }
|
|
118
146
|
.receipt button:hover { color: var(--ink); background: var(--glass); }
|
|
119
147
|
|
|
120
|
-
.glass { background: rgba(
|
|
148
|
+
.glass { background: rgba(var(--sky-ink-rgb),.07); box-shadow: inset 0 0 0 1px rgba(var(--sky-ink-rgb),.14), inset 0 1px 0 rgba(var(--sky-ink-rgb),.12), 0 18px 40px var(--sky-drop); backdrop-filter: blur(14px) saturate(140%); -webkit-backdrop-filter: blur(14px) saturate(140%); }
|
|
121
149
|
.card { position: fixed; z-index: 7; max-width: 300px; padding: 12px 14px; border-radius: 14px; pointer-events: none; color: var(--ink); font-size: 13.5px; line-height: 1.4; display: flex; flex-direction: column; gap: 5px; opacity: 0; transform: translateY(4px); transition: opacity .12s ease, transform .12s ease; }
|
|
122
150
|
.card.on { opacity: 1; transform: none; }
|
|
123
151
|
.card .k { display: flex; align-items: center; gap: 8px; }
|
|
@@ -131,7 +159,7 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
|
|
|
131
159
|
.menu button { border: 0; background: transparent; color: var(--ink-soft); text-align: left; padding: 8px 10px; border-radius: 8px; font-size: 13.5px; cursor: pointer; }
|
|
132
160
|
.menu button:hover { background: var(--glass-strong); color: var(--ink); }
|
|
133
161
|
|
|
134
|
-
.panel { position: fixed; right: 20px; top: calc(140px + env(safe-area-inset-top, 0px)); bottom: calc(96px + env(safe-area-inset-bottom, 0px)); width: 340px; z-index: 5; display: flex; flex-direction: column; gap: 14px; padding: 22px 24px; border-radius: 16px; background: rgba(
|
|
162
|
+
.panel { position: fixed; right: 20px; top: calc(140px + env(safe-area-inset-top, 0px)); bottom: calc(96px + env(safe-area-inset-bottom, 0px)); width: 340px; z-index: 5; display: flex; flex-direction: column; gap: 14px; padding: 22px 24px; border-radius: 16px; background: rgba(var(--sky-void-rgb),.62); box-shadow: inset 0 0 0 1px var(--rule-soft); backdrop-filter: blur(8px); overflow: auto; }
|
|
135
163
|
.panel .kind { display: flex; align-items: center; gap: 12px; min-height: 28px; }
|
|
136
164
|
.panel .kind .glyph { width: 32px; display: grid; place-items: center; }
|
|
137
165
|
.panel h2 { margin: 0; font-size: 21px; font-weight: 300; line-height: 1.25; letter-spacing: -.02em; text-wrap: balance; }
|
|
@@ -157,7 +185,7 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
|
|
|
157
185
|
.orbit-list .st { font: var(--label); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-dim); flex-shrink: 0; width: 72px; }
|
|
158
186
|
.orbit-list .st.live { color: var(--accent); }
|
|
159
187
|
|
|
160
|
-
.room { position: fixed; left: 20px; top: calc(140px + env(safe-area-inset-top, 0px)); bottom: calc(96px + env(safe-area-inset-bottom, 0px)); width: 440px; z-index: 5; display: flex; flex-direction: column; gap: 14px; padding: 22px 0 0; border-radius: 16px; background: rgba(
|
|
188
|
+
.room { position: fixed; left: 20px; top: calc(140px + env(safe-area-inset-top, 0px)); bottom: calc(96px + env(safe-area-inset-bottom, 0px)); width: 440px; z-index: 5; display: flex; flex-direction: column; gap: 14px; padding: 22px 0 0; border-radius: 16px; background: rgba(var(--sky-void-rgb),.62); box-shadow: inset 0 0 0 1px var(--rule-soft); backdrop-filter: blur(8px); }
|
|
161
189
|
.room .head { display: flex; align-items: baseline; gap: 14px; padding: 0 24px; }
|
|
162
190
|
.room .head h2 { margin: 0; font-size: 21px; font-weight: 300; letter-spacing: -.02em; }
|
|
163
191
|
.room .head .lbl { margin-left: auto; }
|
|
@@ -175,9 +203,9 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
|
|
|
175
203
|
.empty { padding: 20px 24px; color: var(--ink-dim); font-size: 14px; }
|
|
176
204
|
|
|
177
205
|
.jump { position: fixed; left: 20px; top: calc(76px + env(safe-area-inset-top, 0px)); z-index: 6; }
|
|
178
|
-
.jump input { width: 260px; height: 40px; border-radius: 999px; border: 0; box-shadow: inset 0 0 0 1px var(--rule-soft); background: rgba(
|
|
206
|
+
.jump input { width: 260px; height: 40px; border-radius: 999px; border: 0; box-shadow: inset 0 0 0 1px var(--rule-soft); background: rgba(var(--sky-void-rgb),.5); color: var(--ink); padding: 0 16px; font: 400 14px/1 var(--face); }
|
|
179
207
|
.jump input::placeholder { color: var(--ink-dim); }
|
|
180
|
-
.jump ul { list-style: none; margin: 6px 0 0; padding: 6px; border-radius: 12px; background: rgba(
|
|
208
|
+
.jump ul { list-style: none; margin: 6px 0 0; padding: 6px; border-radius: 12px; background: rgba(var(--sky-void-rgb),.8); box-shadow: inset 0 0 0 1px var(--rule-soft); max-height: 300px; overflow: auto; width: 360px; }
|
|
181
209
|
.jump li button { width: 100%; text-align: left; border: 0; background: transparent; color: var(--ink-soft); padding: 8px 10px; border-radius: 8px; font-size: 13.5px; cursor: pointer; display: flex; gap: 10px; align-items: baseline; }
|
|
182
210
|
.jump li button:hover, .jump li button:focus-visible { background: var(--glass); color: var(--ink); }
|
|
183
211
|
.jump li .lbl { flex-shrink: 0; width: 60px; }
|
|
@@ -185,7 +213,7 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
|
|
|
185
213
|
.tour, .start { position: fixed; left: 50%; bottom: calc(110px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%); width: min(560px, calc(100% - 32px)); z-index: 8; padding: 18px 22px 16px; border-radius: 16px; display: flex; flex-direction: column; gap: 10px; }
|
|
186
214
|
.tour .step { display: flex; align-items: center; gap: 10px; }
|
|
187
215
|
.tour .dots { display: flex; gap: 5px; margin-left: auto; }
|
|
188
|
-
.tour .dots i { width: 5px; height: 5px; border-radius: 50%; background: rgba(
|
|
216
|
+
.tour .dots i { width: 5px; height: 5px; border-radius: 50%; background: rgba(var(--sky-ink-rgb),.2); }
|
|
189
217
|
.tour .dots i.on { background: var(--accent); }
|
|
190
218
|
.tour h3, .start h3 { margin: 0; font-size: 19px; font-weight: 300; letter-spacing: -.02em; line-height: 1.25; }
|
|
191
219
|
.tour p, .start p { margin: 0; font-size: 14px; line-height: 1.5; color: var(--ink-soft); }
|
|
@@ -206,7 +234,7 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
|
|
|
206
234
|
.alt { margin-left: auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; text-align: right; color: var(--ink-dim); font: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
207
235
|
.alt b { color: var(--ink-soft); font-weight: 400; }
|
|
208
236
|
.hint { color: var(--ink-ghost); }
|
|
209
|
-
.time { display: flex; align-items: center; gap: 8px; width: min(360px, 100%); align-self: center; padding: 3px 4px 3px 4px; border-radius: 999px; background: rgba(
|
|
237
|
+
.time { display: flex; align-items: center; gap: 8px; width: min(360px, 100%); align-self: center; padding: 3px 4px 3px 4px; border-radius: 999px; background: rgba(var(--sky-void-rgb),.45); box-shadow: inset 0 0 0 1px var(--rule-soft); opacity: .8; }
|
|
210
238
|
.time:hover { opacity: 1; }
|
|
211
239
|
.time .play { width: 24px; height: 24px; border-radius: 50%; border: 0; background: var(--glass-strong); color: var(--ink); cursor: pointer; display: grid; place-items: center; font-size: 9px; flex-shrink: 0; }
|
|
212
240
|
.time .play[aria-pressed="true"] { background: var(--accent); color: var(--accent-on); }
|
|
@@ -218,7 +246,7 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
|
|
|
218
246
|
.composer .mode:hover { color: var(--ink); }
|
|
219
247
|
.composer .mode[aria-pressed="true"] { background: var(--glass-strong); color: var(--ink); }
|
|
220
248
|
.composer .mode[aria-pressed="true"]::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); margin-right: 7px; vertical-align: 1px; }
|
|
221
|
-
.steps { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; max-width: 100%; font-size: 13px; color: var(--ink-soft); padding: 5px 8px 5px 14px; border-radius: 999px; background: rgba(
|
|
249
|
+
.steps { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; max-width: 100%; font-size: 13px; color: var(--ink-soft); padding: 5px 8px 5px 14px; border-radius: 999px; background: rgba(var(--sky-void-rgb),.5); box-shadow: inset 0 0 0 1px var(--rule-soft); }
|
|
222
250
|
.steps b { font: var(--label); letter-spacing: .14em; text-transform: uppercase; color: var(--accent); font-weight: 400; }
|
|
223
251
|
.steps .skip { border: 0; background: transparent; color: var(--ink-dim); cursor: pointer; font-size: 12px; height: 24px; padding: 0 8px; border-radius: 999px; }
|
|
224
252
|
.steps .skip:hover { color: var(--ink); background: var(--glass); }
|
|
@@ -227,18 +255,18 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
|
|
|
227
255
|
.mk { display: block; width: 9px; height: 9px; border-radius: 50%; box-sizing: border-box; }
|
|
228
256
|
.mk.star { background: var(--accent); box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 75%, transparent); }
|
|
229
257
|
.mk.star.dim { opacity: .55; box-shadow: 0 0 4px color-mix(in srgb, var(--accent) 40%, transparent); }
|
|
230
|
-
.mk.protostar { width: 7px; height: 7px; background: rgba(
|
|
231
|
-
.mk.satellite { width: 6px; height: 6px; background: rgba(
|
|
232
|
-
.mk.remnant { width: 12px; height: 12px; background: transparent; border: 1px solid rgba(
|
|
233
|
-
.mk.cold { width: 6px; height: 6px; background:
|
|
234
|
-
.mk.well { width: 14px; height: 14px; background: transparent; border: 1px dashed rgba(
|
|
235
|
-
.mk.alive { width: 14px; height: 14px; background: transparent; border: 1px solid rgba(
|
|
236
|
-
.mk.settled { width: 12px; height: 12px; background: transparent; border: 1px solid rgba(
|
|
237
|
-
.mk.collapsed { width: 9px; height: 9px; background:
|
|
238
|
-
.mk.unseen { width: 12px; height: 12px; background: transparent; border: 1px dashed rgba(
|
|
239
|
-
.mk.task { width: 4px; height: 4px; background: rgba(
|
|
258
|
+
.mk.protostar { width: 7px; height: 7px; background: rgba(var(--sky-ink-rgb),.5); box-shadow: 0 0 0 4px rgba(var(--sky-ink-rgb),.05); }
|
|
259
|
+
.mk.satellite { width: 6px; height: 6px; background: rgba(var(--sky-ink-rgb),.4); outline: 1px solid rgba(var(--sky-ink-rgb),.16); outline-offset: 3px; }
|
|
260
|
+
.mk.remnant { width: 12px; height: 12px; background: transparent; border: 1px solid rgba(var(--sky-ink-rgb),.4); }
|
|
261
|
+
.mk.cold { width: 6px; height: 6px; background: var(--sky-cold); border: 1px solid rgba(var(--sky-ink-rgb),.18); }
|
|
262
|
+
.mk.well { width: 14px; height: 14px; background: transparent; border: 1px dashed rgba(var(--sky-ink-rgb),.35); }
|
|
263
|
+
.mk.alive { width: 14px; height: 14px; background: transparent; border: 1px solid rgba(var(--sky-ink-rgb),.35); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent), 0 0 10px var(--accent); }
|
|
264
|
+
.mk.settled { width: 12px; height: 12px; background: transparent; border: 1px solid rgba(var(--sky-ink-rgb),.5); box-shadow: inset 0 0 0 3px var(--void), inset 0 0 0 4px rgba(var(--sky-ink-rgb),.35), 0 0 8px rgba(var(--sky-ink-rgb),.25); }
|
|
265
|
+
.mk.collapsed { width: 9px; height: 9px; background: var(--void); border: 1px solid rgba(var(--sky-ink-rgb),.3); box-shadow: 0 0 0 2px rgba(var(--sky-void-rgb),1), 0 0 0 3px rgba(var(--sky-ink-rgb),.08); }
|
|
266
|
+
.mk.unseen { width: 12px; height: 12px; background: transparent; border: 1px dashed rgba(var(--sky-ink-rgb),.2); }
|
|
267
|
+
.mk.task { width: 4px; height: 4px; background: rgba(var(--sky-ink-rgb),.6); }
|
|
240
268
|
.mk.task.live { background: var(--accent); box-shadow: 0 0 6px var(--accent); }
|
|
241
|
-
.mk.born { width: 10px; height: 10px; background: transparent; border: 1px solid var(--accent); box-shadow: inset 0 0 0 2px
|
|
269
|
+
.mk.born { width: 10px; height: 10px; background: transparent; border: 1px solid var(--accent); box-shadow: inset 0 0 0 2px var(--void), inset 0 0 0 5px var(--accent); }
|
|
242
270
|
.mk.route { width: 14px; height: 2px; border-radius: 1px; background: var(--chart); box-shadow: 0 0 6px var(--chart); }
|
|
243
271
|
|
|
244
272
|
@media (max-width: 900px) {
|
|
@@ -30,3 +30,21 @@ Three fixtures, one per craft: `builders__profile` (the engineer, id 95, the vie
|
|
|
30
30
|
- The engineer tile "works shipped" has no "across N goals" foot, the ideator's "thoughts filed" no "since <month>" foot, the artist's "credits from pages" no "N a page" foot, and the surface rows no "where" sub-label: none of those is in the read.
|
|
31
31
|
- **Karma ends every artist's and ideator's set of tiles**, which the mock does not draw. The owner's call, reviewing this build on 2026-10-01: "karma for artists and ideators is definitely key". The engineer's set carries it already.
|
|
32
32
|
- The tiles' grid fits at 148px rather than 170px, so five tiles sit on one row inside the hall's narrower panel.
|
|
33
|
+
|
|
34
|
+
## The Activity tabs, one per role (task 1004435, WA6.RP05)
|
|
35
|
+
|
|
36
|
+
The section used to offer All / Engineer / Artist / Ideator / UI tabs over one task ledger, so an ideator's tab listed tasks whose discipline happened to be "ideator" and an artist's listed only artist-labelled tasks they had shipped. Now there is **one tab per role the person has** (`roles.shown`, the header's chips, main first), each counted the way the header leads, each its craft's own record, each with its own panel:
|
|
37
|
+
|
|
38
|
+
- **Engineer**: the works completed and goals created ledgers, WHOLE (no craft filter: most of the corpus is unclassified and a filter reads as "you did less").
|
|
39
|
+
- **Ideator**: the room's public trail, `public/profile-crafts.js`. One read of up to the route's cap of thoughts (`GET /inbox/by-builder/:id`, which already carried each thought's `state` and `size`), drawn as the sky's five marks by the day each was filed (size = diameter, brightness = a star's opacity, a folded-in thought tethered to the one it joined), the legend in the sky's words from `OTBIdeaObjects`, and every drawn thought a row in the ledger under it: state, size (~ for a historical estimate), still alive, and the old trail's "what became of it" cell folded in whole. A mark and its row light together on hover or focus; clicking a mark focuses its row.
|
|
40
|
+
- **Artist**: the pages they rewrote in Tweak Mode that were approved, four at a time with "Show N more", from `GET /copy-desk/artists/:id/pages` (signed in; copy-desk's `composeArtistPages`). Each row: page, surface, approval day, changed-line count, and a before/after: the round's first changed line beside the approval renders (`/tasks/:id/visuals`, `before-` and `after-desktop-light`) as small thumbnails.
|
|
41
|
+
|
|
42
|
+
Signed out, the section is one sign-in line (owner decision (a)).
|
|
43
|
+
|
|
44
|
+
### Deliberate differences from the mock (RP05)
|
|
45
|
+
|
|
46
|
+
- **The tab labels are lowercase**, the hall's own `.setup__tab` style; the mock capitalises them. The live hall's chrome wins over a mock's (the blend rule).
|
|
47
|
+
- **"What became of it" is the existing cell**, with its links, descent and triage note, not the mock's short phrase: the task asked to fold the old trail in, not to lose what it said.
|
|
48
|
+
- **The whole after line is highlighted**, where the mock highlights only the changed words: the batch stores whole lines, not the changed span.
|
|
49
|
+
- **The artist tab's lede does not say what each page paid**: the pay per page is not in the read.
|
|
50
|
+
- **The trail's window** is the span of the person's own thoughts (at least a month), with a line at each month, rather than the mock's fixed three months.
|
|
@@ -88,6 +88,77 @@
|
|
|
88
88
|
</li>`;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// ---- this hall's own upgrades (task 1004297) ----------------------------------------
|
|
92
|
+
//
|
|
93
|
+
// The Bongos hall takes every new version by itself, so the page has to say two things the
|
|
94
|
+
// stages cannot: whether that is still happening, and whether a version was tried here and
|
|
95
|
+
// rolled back. Both come off the server's reading; nothing is inferred here.
|
|
96
|
+
const HOW = {
|
|
97
|
+
door: 'Upgrade pressed on /deploy',
|
|
98
|
+
sweep: 'the automatic update',
|
|
99
|
+
server: 'on the server',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
function ago(iso) {
|
|
103
|
+
const t = Date.parse(iso);
|
|
104
|
+
if (!Number.isFinite(t)) return 'a while ago';
|
|
105
|
+
const mins = Math.max(0, Math.round((Date.now() - t) / 60000));
|
|
106
|
+
if (mins < 1) return 'just now';
|
|
107
|
+
if (mins < 60) return `${mins} minute${mins === 1 ? '' : 's'} ago`;
|
|
108
|
+
const hours = Math.round(mins / 60);
|
|
109
|
+
if (hours < 48) return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
|
110
|
+
return `${Math.round(hours / 24)} days ago`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// A stalled wait is the automation having stopped, and gets the warning box; an ordinary one
|
|
114
|
+
// (published minutes ago) is one quiet sentence, because it is the system working.
|
|
115
|
+
function waitingHtml(w) {
|
|
116
|
+
if (!w || !w.first) return '';
|
|
117
|
+
const more = w.versions > 1 ? ` (and ${w.versions - 1} newer)` : '';
|
|
118
|
+
const since = w.since ? ` ${ago(w.since)}` : '';
|
|
119
|
+
if (w.stalled) {
|
|
120
|
+
return `<div class="ov-note ov-note--warn"><p><strong>The automatic update may have stopped.</strong> ${escapeHtml(w.first)}${escapeHtml(more)} was published${escapeHtml(since)} and this hall has not installed it. Each new version should reach this hall by itself within about fifteen minutes of being published.</p></div>`;
|
|
121
|
+
}
|
|
122
|
+
return `<p class="ov-row__sub">${escapeHtml(w.first)}${escapeHtml(more)} was published${escapeHtml(since)}; this hall takes it on its next automatic update.</p>`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function upgradeRow(u) {
|
|
126
|
+
const facts = [`<span class="ov-fact">${escapeHtml(HOW[u.how] || HOW.server)}</span>`];
|
|
127
|
+
if (u.rolled_back) facts.unshift('<span class="ov-fact"><b>rolled back</b></span>');
|
|
128
|
+
const move = `${u.from || '(none)'} → ${u.to || '?'}`;
|
|
129
|
+
return `<li class="ov-row ov-row--dense${u.rolled_back ? ' nr-upgrade--back' : ''}">
|
|
130
|
+
<div class="ov-row__main">
|
|
131
|
+
<span class="ov-row__title ov-key">${escapeHtml(u.rolled_back ? `Tried ${move}` : move)}</span>
|
|
132
|
+
${u.rolled_back ? `<p class="ov-row__sub">${escapeHtml(u.reason ? `It did not come back healthy (${unDash(u.reason)}), so the hall went back to ${u.from || 'the version before'}.` : `It did not come back healthy, so the hall went back to ${u.from || 'the version before'}.`)}</p>` : ''}
|
|
133
|
+
<div class="ov-row__facts">${facts.join('')}</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="ov-row__right">${when(u.at)}</div>
|
|
136
|
+
</li>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function upgradesHtml(d) {
|
|
140
|
+
const u = d.upgrades;
|
|
141
|
+
if (!u) return '';
|
|
142
|
+
let body;
|
|
143
|
+
if (!u.ok) body = `<p class="ov-row__sub">${escapeHtml(u.error || 'This hall\'s upgrade ledger could not be read')}.</p>`;
|
|
144
|
+
else if (!u.rows.length) body = '<p class="ov-row__sub">No upgrade is recorded on this hall yet.</p>';
|
|
145
|
+
else body = `<ul class="ov-rows">${u.rows.map(upgradeRow).join('')}</ul>`;
|
|
146
|
+
const latest = u.ok && u.rows[0];
|
|
147
|
+
const warn = latest && latest.rolled_back
|
|
148
|
+
? `<div class="ov-note ov-note--warn"><p><strong>The last upgrade here was rolled back.</strong> ${escapeHtml(latest.to || 'That version')} was installed, did not come back healthy, and was undone. This hall will not try it again for a day; the next newer version is installed as soon as it is published.</p></div>`
|
|
149
|
+
: '';
|
|
150
|
+
return `<div class="ov-group" data-upgrades>
|
|
151
|
+
<div class="ov-group__head">
|
|
152
|
+
<span class="ov-group__name">Upgrades on this hall</span>
|
|
153
|
+
${u.ok ? `<span class="ov-group__n">${u.rows.length}</span>` : ''}
|
|
154
|
+
</div>
|
|
155
|
+
<p class="ov-row__sub">Each new version should reach this hall by itself within about fifteen minutes of being published, checked that the site came back healthy, and rolled back if it did not.</p>
|
|
156
|
+
${warn}
|
|
157
|
+
${warn ? '' : waitingHtml(d.waiting)}
|
|
158
|
+
${body}
|
|
159
|
+
</div>`;
|
|
160
|
+
}
|
|
161
|
+
|
|
91
162
|
function moreButton(key, total, shown) {
|
|
92
163
|
return total > shown ? `<button type="button" class="ov-toggle" data-expand="${escapeHtml(key)}">Show all ${total}</button>` : '';
|
|
93
164
|
}
|
|
@@ -251,6 +322,7 @@
|
|
|
251
322
|
${problemsHtml(data)}
|
|
252
323
|
${figsHtml(stages, data)}
|
|
253
324
|
${groups || '<p class="ov-row__sub">Nothing was finished in this window.</p>'}
|
|
325
|
+
${upgradesHtml(data)}
|
|
254
326
|
<p class="ov-foot">read ${escapeHtml(new Date(data.checked_at).toLocaleTimeString())} · ${escapeHtml(data.package)} · release notes from ${escapeHtml(data.notes.source ? `${data.notes.source.from === 'package' ? 'the published' : 'this hall\'s'} ${data.notes.source.version} package` : 'nowhere')}</p>`;
|
|
255
327
|
work.hidden = false;
|
|
256
328
|
if (vers) {
|
|
@@ -189,6 +189,66 @@ async function taskTitles(pool, ids) {
|
|
|
189
189
|
return new Map(rows.map((r) => [String(r.id), r.title]));
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// THIS HALL'S OWN UPGRADES (task 1004297) — the core_upgrades ledger `bongos upgrade` writes on
|
|
193
|
+
// every move and every rollback, newest first. The Bongos hall takes each new version by itself
|
|
194
|
+
// now, so this list is the record of that happening — and a rolled-back row is the one thing on
|
|
195
|
+
// this page that says a version was tried here and refused. Fail-soft: an unreadable ledger is
|
|
196
|
+
// reported as unreadable, never as "no upgrades".
|
|
197
|
+
//
|
|
198
|
+
// HOW IT WAS MOVED (`how`: a method, never a person's name) is only as precise as the ledger is: 'owner-control' is the Upgrade button on
|
|
199
|
+
// /deploy; a row the lane noted as `auto-subscription` is the unattended sweep; anything else was
|
|
200
|
+
// started on the server, by the sweep through go-live or by a person at a shell, and the ledger
|
|
201
|
+
// does not tell those two apart — so the page says "on the server" rather than guess.
|
|
202
|
+
const UPGRADES_SHOWN = 8;
|
|
203
|
+
async function recentUpgrades(pool) {
|
|
204
|
+
try {
|
|
205
|
+
const { rows } = await pool.query(
|
|
206
|
+
`SELECT from_version, to_version, applied_at, source, note
|
|
207
|
+
FROM core_upgrades
|
|
208
|
+
ORDER BY applied_at DESC, id DESC
|
|
209
|
+
LIMIT $1`,
|
|
210
|
+
[UPGRADES_SHOWN]
|
|
211
|
+
);
|
|
212
|
+
return {
|
|
213
|
+
ok: true,
|
|
214
|
+
rows: rows.map((r) => {
|
|
215
|
+
const note = r.note == null ? '' : String(r.note);
|
|
216
|
+
const rolledBack = /^rolled_back:/.test(note);
|
|
217
|
+
const how = r.source === 'owner-control' ? 'door'
|
|
218
|
+
: /auto-subscription/.test(note) ? 'sweep'
|
|
219
|
+
: 'server';
|
|
220
|
+
return {
|
|
221
|
+
from: r.from_version || null,
|
|
222
|
+
to: r.to_version || null,
|
|
223
|
+
at: r.applied_at ? new Date(r.applied_at).toISOString() : null,
|
|
224
|
+
how,
|
|
225
|
+
rolled_back: rolledBack,
|
|
226
|
+
// "rolled_back: <reason> (attempted A → B, restored C)" — the reason, bounded.
|
|
227
|
+
reason: rolledBack ? boundText(note.replace(/^rolled_back:\s*/, '').replace(/\s*\(attempted[^)]*\)\s*$/, '')) : null,
|
|
228
|
+
};
|
|
229
|
+
}),
|
|
230
|
+
};
|
|
231
|
+
} catch (e) {
|
|
232
|
+
return { ok: false, error: `this hall's upgrade ledger could not be read (${e.message})` };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// HOW LONG THE NEWEST VERSION HAS BEEN WAITING HERE (task 1004297). The Bongos hall takes every
|
|
237
|
+
// new version by itself, within one sweep of it being published; a version that has waited longer
|
|
238
|
+
// than twice that is the automation having stopped, and the page says so rather than letting it
|
|
239
|
+
// look like an ordinary queue. The sweep's period is host configuration (15 minutes, declared in
|
|
240
|
+
// config/scheduled-routines.json); this is the threshold, not a reading of it.
|
|
241
|
+
const WAIT_ALARM_MIN = 30;
|
|
242
|
+
function waitingHere({ live, versions, published, now }) {
|
|
243
|
+
if (!live || !Array.isArray(versions)) return null;
|
|
244
|
+
const newer = versions.filter((v) => compareVersions(v, live) > 0).sort(compareVersions);
|
|
245
|
+
if (!newer.length) return null;
|
|
246
|
+
const first = newer[0];
|
|
247
|
+
const since = published[first] ? Date.parse(published[first]) : NaN;
|
|
248
|
+
const minutes = Number.isFinite(since) ? Math.max(0, Math.round((now - since) / 60000)) : null;
|
|
249
|
+
return { versions: newer.length, first, since: Number.isFinite(since) ? new Date(since).toISOString() : null, minutes, stalled: minutes != null && minutes > WAIT_ALARM_MIN };
|
|
250
|
+
}
|
|
251
|
+
|
|
192
252
|
function clampInt(v, dflt, max) {
|
|
193
253
|
const n = Number.parseInt(v, 10);
|
|
194
254
|
return Number.isInteger(n) && n > 0 ? Math.min(n, max) : dflt;
|
|
@@ -256,9 +316,11 @@ async function readWork({
|
|
|
256
316
|
for (const v of hist.versions) for (const e of v.entries || []) if (e.id && !e.title) e.title = more.get(e.id) || null;
|
|
257
317
|
}
|
|
258
318
|
|
|
319
|
+
const at = now ? now() : Date.now();
|
|
320
|
+
const isCore = pkg === CORE_PACKAGE;
|
|
259
321
|
return {
|
|
260
322
|
package: pkg,
|
|
261
|
-
checked_at: new Date(
|
|
323
|
+
checked_at: new Date(at).toISOString(),
|
|
262
324
|
live: liveVersion,
|
|
263
325
|
released,
|
|
264
326
|
newest: registry.ok ? registry.newest : null,
|
|
@@ -267,6 +329,10 @@ async function readWork({
|
|
|
267
329
|
window_days: windowDays,
|
|
268
330
|
stages,
|
|
269
331
|
history: hist,
|
|
332
|
+
// Only the core's page has a "this hall" to have upgraded: another package is not what this
|
|
333
|
+
// server runs.
|
|
334
|
+
upgrades: isCore ? await recentUpgrades(pool) : null,
|
|
335
|
+
waiting: isCore && registry.ok ? waitingHere({ live: liveVersion, versions: registry.versions, published, now: at }) : null,
|
|
270
336
|
};
|
|
271
337
|
}
|
|
272
338
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.78",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.78",
|
|
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.20.
|
|
3
|
+
"version": "1.20.78",
|
|
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
|
@@ -8591,5 +8591,23 @@
|
|
|
8591
8591
|
"id": "1004419",
|
|
8592
8592
|
"text": "New founders now answer the last three planet questions: how many people will build (with a growth guess they can adjust), how builders might be rewarded (skipped for someone building alone), and how much the platform should r"
|
|
8593
8593
|
}
|
|
8594
|
+
],
|
|
8595
|
+
"1.20.78": [
|
|
8596
|
+
{
|
|
8597
|
+
"id": "1004500",
|
|
8598
|
+
"text": "Your thinking and the project sky now have a light mode. When the hall is set to light, the sky turns to paper with ink-drawn rings and thoughts and light panels; switch to dark and the black night sky comes back exactly as be"
|
|
8599
|
+
},
|
|
8600
|
+
{
|
|
8601
|
+
"id": "1004435",
|
|
8602
|
+
"text": "A profile's Activity section now has one tab for each role the person has, each with a count. The engineer tab lists their finished work and the goals they created. The ideator tab draws their ideas as the sky trail, with a li"
|
|
8603
|
+
},
|
|
8604
|
+
{
|
|
8605
|
+
"id": "1004488",
|
|
8606
|
+
"text": "The script that updates the database on every deploy could randomly re-run a handful of updates it had already made. Most were harmless repeats, but one could change live data or fail a deploy. It now checks reliably, proven o"
|
|
8607
|
+
},
|
|
8608
|
+
{
|
|
8609
|
+
"id": "1004297",
|
|
8610
|
+
"text": "Each project can now be set to take every new version (Bongos) or only released ones (everyone else), a version that fails is no longer retried in a loop, and the deploy page shows each upgrade, any rollback, and when the auto"
|
|
8611
|
+
}
|
|
8594
8612
|
]
|
|
8595
8613
|
}
|
|
@@ -17,6 +17,16 @@
|
|
|
17
17
|
// A MAJOR bump is NEVER automatic under any channel — a major is a breaking change that wants a human.
|
|
18
18
|
// Prereleases (1.17.2-rc.1) are never auto-target either — only stable releases.
|
|
19
19
|
//
|
|
20
|
+
// A second, independent axis says WHICH published versions an instance may take at all (task 1004297,
|
|
21
|
+
// ADR 0360) — its FOLLOW:
|
|
22
|
+
// released — only versions at or below the one the registry labels `latest`, i.e. what the owner
|
|
23
|
+
// has released to other projects. DEFAULT, for every project.
|
|
24
|
+
// candidates — every published version, the moment it is published. The platform's own hall, and
|
|
25
|
+
// only it: Bongos runs each version before anyone else is offered it.
|
|
26
|
+
// The channel bounds how FAR a jump may go; the follow bounds which versions exist to jump to. Until
|
|
27
|
+
// candidates publish under their own label (task 1004298), `latest` is the newest version and the two
|
|
28
|
+
// follows pick the same target — the axis exists first so that the split changes nothing for Bongos.
|
|
29
|
+
//
|
|
20
30
|
// WHY the registry as the source (not vendored tarballs): a subscription needs a LIVE source of
|
|
21
31
|
// truth for "what's the newest published core?" — that's `npm view @bongos/core versions` against the
|
|
22
32
|
// private registry (ADR 0134, task 2090). The vendored path is for a one-off manual bump.
|
|
@@ -28,6 +38,10 @@ const { spawnSync } = require('node:child_process');
|
|
|
28
38
|
const CORE_PKG = '@bongos/core';
|
|
29
39
|
const VALID_CHANNELS = ['pinned', 'patch', 'minor'];
|
|
30
40
|
const DEFAULT_CHANNEL = 'patch';
|
|
41
|
+
const VALID_FOLLOWS = ['released', 'candidates'];
|
|
42
|
+
// Unknown or absent reads as `released`: the follow that takes FEWER versions is the safe default,
|
|
43
|
+
// the way an unknown channel reads as `patch` rather than `minor`.
|
|
44
|
+
const DEFAULT_FOLLOW = 'released';
|
|
31
45
|
const NPM_VIEW_TIMEOUT_MS = 30000;
|
|
32
46
|
// The subscription manifest an operator edits to opt instances in. Ships EMPTY (instances: []) in the
|
|
33
47
|
// core template — a fresh instance subscribes nothing until the operator lists a target here.
|
|
@@ -109,6 +123,22 @@ function normalizeChannel(channel) {
|
|
|
109
123
|
return VALID_CHANNELS.includes(c) ? c : DEFAULT_CHANNEL;
|
|
110
124
|
}
|
|
111
125
|
|
|
126
|
+
function normalizeFollow(follow) {
|
|
127
|
+
const f = String(follow == null ? '' : follow).trim().toLowerCase();
|
|
128
|
+
return VALID_FOLLOWS.includes(f) ? f : DEFAULT_FOLLOW;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The published versions an instance's FOLLOW admits (task 1004297). PURE. `candidates` admits every
|
|
132
|
+
// one; `released` admits those at or below `released` — the version the registry labels `latest` —
|
|
133
|
+
// and NOTHING when that label is unknown: an instance that follows releases must never take a version
|
|
134
|
+
// because nobody could say whether it was released. Feed the result to resolveChannelTarget.
|
|
135
|
+
function versionsForFollow({ follow, available, released } = {}) {
|
|
136
|
+
const list = Array.isArray(available) ? available.filter((v) => typeof v === 'string') : [];
|
|
137
|
+
if (normalizeFollow(follow) === 'candidates') return list;
|
|
138
|
+
if (!parseSemver(released)) return [];
|
|
139
|
+
return list.filter((v) => compareSemver(v, released) <= 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
112
142
|
// Would `channel` permit an unattended jump from `installed` to `candidate`? True only when candidate
|
|
113
143
|
// is a STABLE release strictly newer than installed AND within the channel's blast radius. Any invalid
|
|
114
144
|
// input, an unknown installed version, a prerelease candidate, or a major bump ⇒ false (fail-closed).
|
|
@@ -174,6 +204,34 @@ function parseNpmViewVersions(stdout) {
|
|
|
174
204
|
return { ok: false, error: 'unexpected npm view shape', versions: [] };
|
|
175
205
|
}
|
|
176
206
|
|
|
207
|
+
// Which version is RELEASED — the one the registry labels `latest` (task 1004297) — via
|
|
208
|
+
// `npm view … dist-tags --json`. A separate read from listAvailableVersions on purpose: that one's
|
|
209
|
+
// output shape is relied on by the deploy door and `bongos upgrade`, and it stays exactly as it is.
|
|
210
|
+
// Fail-SOFT like its sibling: { ok:false, error } on any failure, never a throw. `released` is null
|
|
211
|
+
// unless the label names a stable version.
|
|
212
|
+
function readReleaseTags({ cwd = process.cwd(), pkg = CORE_PKG, run = spawnSync, env = process.env, timeoutMs = NPM_VIEW_TIMEOUT_MS } = {}) {
|
|
213
|
+
let res;
|
|
214
|
+
try {
|
|
215
|
+
res = run('npm', ['view', pkg, 'dist-tags', '--json'], { cwd, env, encoding: 'utf8', timeout: timeoutMs });
|
|
216
|
+
} catch (e) {
|
|
217
|
+
return { ok: false, error: e && e.message ? e.message : String(e), released: null, tags: {} };
|
|
218
|
+
}
|
|
219
|
+
if (!res || res.status !== 0) {
|
|
220
|
+
const reason = (res && (res.stderr || res.error && res.error.message)) || `exit ${res && res.status}`;
|
|
221
|
+
return { ok: false, error: String(reason).trim().split('\n')[0] || 'npm view failed', released: null, tags: {} };
|
|
222
|
+
}
|
|
223
|
+
let tags;
|
|
224
|
+
try { tags = JSON.parse(String(res.stdout || '').trim() || '{}'); } catch (e) {
|
|
225
|
+
return { ok: false, error: `unparseable npm output: ${e.message}`, released: null, tags: {} };
|
|
226
|
+
}
|
|
227
|
+
if (!tags || typeof tags !== 'object' || Array.isArray(tags)) return { ok: false, error: 'unexpected npm view shape', released: null, tags: {} };
|
|
228
|
+
const latest = typeof tags.latest === 'string' ? tags.latest : null;
|
|
229
|
+
const released = latest && isStable(latest) ? latest : null;
|
|
230
|
+
return released
|
|
231
|
+
? { ok: true, released, tags }
|
|
232
|
+
: { ok: false, error: 'the registry names no released (latest) version', released: null, tags };
|
|
233
|
+
}
|
|
234
|
+
|
|
177
235
|
// ---- subscription manifest --------------------------------------------------
|
|
178
236
|
|
|
179
237
|
// Read + normalize config/update-subscriptions.json — the operator-edited opt-in list of instances
|
|
@@ -191,6 +249,7 @@ function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SU
|
|
|
191
249
|
let json;
|
|
192
250
|
try { json = JSON.parse(raw); } catch (e) { return { defaultChannel: DEFAULT_CHANNEL, instances: [], source: abs, present: true, dropped: 0, error: `malformed JSON: ${e.message}`, reason: ROSTER_MALFORMED }; }
|
|
193
251
|
const defaultChannel = normalizeChannel(json.defaultChannel);
|
|
252
|
+
const defaultFollow = normalizeFollow(json.defaultFollow);
|
|
194
253
|
const rows = Array.isArray(json.instances) ? json.instances : [];
|
|
195
254
|
// An entry without a dir can't be upgraded — drop it, but COUNT the drops. Silently discarding an
|
|
196
255
|
// operator's malformed row is how an instance believes it is enrolled while nothing ever touches it.
|
|
@@ -203,6 +262,9 @@ function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SU
|
|
|
203
262
|
service: r.service ? String(r.service) : null,
|
|
204
263
|
healthUrl: r.healthUrl ? String(r.healthUrl) : null,
|
|
205
264
|
channel: normalizeChannel(r.channel == null ? defaultChannel : r.channel),
|
|
265
|
+
// Which versions this instance takes at all (task 1004297): `released` unless the operator
|
|
266
|
+
// says `candidates` — which only the platform's own hall should.
|
|
267
|
+
follow: normalizeFollow(r.follow == null ? defaultFollow : r.follow),
|
|
206
268
|
// Deploy topology (task 1003219). Optional, and only meaningful together: with
|
|
207
269
|
// versionUrl + deployTimer present the sweep can hand the bump to go-live.js and
|
|
208
270
|
// inherit its backup, pull-deploy-timer guard and INDEPENDENT box-side version
|
|
@@ -223,7 +285,7 @@ function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SU
|
|
|
223
285
|
: {},
|
|
224
286
|
}));
|
|
225
287
|
return {
|
|
226
|
-
defaultChannel, instances, source: abs, present: true, dropped,
|
|
288
|
+
defaultChannel, defaultFollow, instances, source: abs, present: true, dropped,
|
|
227
289
|
reason: instances.length ? ROSTER_OK : ROSTER_EMPTY,
|
|
228
290
|
};
|
|
229
291
|
}
|
|
@@ -396,6 +458,11 @@ module.exports = {
|
|
|
396
458
|
CORE_PKG,
|
|
397
459
|
VALID_CHANNELS,
|
|
398
460
|
DEFAULT_CHANNEL,
|
|
461
|
+
VALID_FOLLOWS,
|
|
462
|
+
DEFAULT_FOLLOW,
|
|
463
|
+
normalizeFollow,
|
|
464
|
+
versionsForFollow,
|
|
465
|
+
readReleaseTags,
|
|
399
466
|
SUBSCRIPTIONS_REL,
|
|
400
467
|
parseSemver,
|
|
401
468
|
isStable,
|