@bongos/core 1.20.76 → 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 +93 -63
- 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 +531 -458
- package/docs/copy-registry.json +1269 -589
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +11 -1
- package/docs/page-readings.json +1208 -1082
- 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/modules/provisioning/planet-physics.js +8 -0
- package/modules/provisioning/soft-limits.js +9 -0
- package/modules/public-landing/public/projects.html +579 -37
- package/modules/public-landing/public/projects.probes.json +6 -3
- package/modules/public-landing/public/projects.states.json +20 -14
- package/modules/ui-design/kit/serve.js +26 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +24 -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/projects_hub_app_step.mjs +4 -1
- package/tests/provisioning_planet_physics.mjs +24 -2
- package/tests/update_channel.mjs +80 -1
- package/tests/update_subscription_engine.mjs +99 -0
- package/tests/wizard_physics.mjs +11 -4
- package/tests/wizard_physics_more.mjs +470 -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
|
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
// 6 rewards reward_intents (all being considered — intent, never binding)
|
|
15
15
|
// 7 framing framing (A–D: managed, hybrid, local, open-source) and services
|
|
16
16
|
//
|
|
17
|
+
// plus `voice` (planet · professional), which nobody is asked: it follows the entity
|
|
18
|
+
// answer (spec D5) and can be switched later.
|
|
19
|
+
//
|
|
17
20
|
// THREE KINDS OF QUESTION. `one` takes a single member of its list, `many` takes an
|
|
18
21
|
// array of members (stored de-duplicated, in the list's own order, so two spellings of
|
|
19
22
|
// one answer store the same bytes), and `count` takes a whole number in a range.
|
|
@@ -100,6 +103,11 @@ const PLANET_PHYSICS_QUESTIONS = Object.freeze({
|
|
|
100
103
|
reward_intents: Object.freeze({ kind: 'many', values: Object.freeze(['non-monetary', 'cash', 'equity', 'future-payouts']) }),
|
|
101
104
|
framing: Object.freeze({ kind: 'one', values: FRAMINGS }),
|
|
102
105
|
services: Object.freeze({ kind: 'many', values: SERVICES }),
|
|
106
|
+
// Not a question: the VOICE the project's copy speaks in (spec D5, task 1004419). The
|
|
107
|
+
// startup flow sets it from the entity answer — the planet voice for individuals and
|
|
108
|
+
// groups, a plain professional one for a formed organization — and stores it here
|
|
109
|
+
// so the project's settings can switch it later with an ordinary PATCH …/detail.
|
|
110
|
+
voice: Object.freeze({ kind: 'one', values: Object.freeze(['planet', 'professional']) }),
|
|
103
111
|
});
|
|
104
112
|
|
|
105
113
|
// The answer as it may be STORED, or undefined when it is not one. `many` answers come
|
|
@@ -42,6 +42,15 @@ const SOFT_LIMIT_RULES = Object.freeze([
|
|
|
42
42
|
effect: 'hide',
|
|
43
43
|
reason: 'Your planet is new, so there is nothing built yet to describe.',
|
|
44
44
|
}),
|
|
45
|
+
// Task 1004419: question 6 is hidden for a founder building alone with nobody
|
|
46
|
+
// expected — one builder now AND one by month three. Expecting company keeps it.
|
|
47
|
+
Object.freeze({
|
|
48
|
+
id: 'rewards-need-other-builders',
|
|
49
|
+
when: { builders_at_start: { in: [1] }, builders_expected: { in: [1] } },
|
|
50
|
+
options: ['reward_intents'],
|
|
51
|
+
effect: 'hide',
|
|
52
|
+
reason: "You're building solo, so there is nobody to reward yet. Expect builders to join and the question comes back.",
|
|
53
|
+
}),
|
|
45
54
|
Object.freeze({
|
|
46
55
|
id: 'equity-needs-a-company',
|
|
47
56
|
when: { entity: { in: ['intent-company', 'intent-nonprofit', 'intent-solo', 'intent-unsure', 'university-project', 'social-group', 'private-individual'] } },
|