@bongos/core 1.19.529 → 1.19.531
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 +33 -18
- package/docs/copy-inventory.md +10 -8
- package/docs/copy-registry.json +41 -23
- package/docs/module-api-changelog.md +4 -0
- package/modules/provisioning/routes/callback-page.js +159 -0
- package/modules/provisioning/routes/provisioning.js +4 -136
- package/modules/status-ui/public/explore.html +56 -3
- package/modules/status-ui/public/explore.js +71 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/design_tokens_sync.mjs +1 -1
- package/tests/explore_visible_states.mjs +285 -0
- package/tests/provisioning_callback_page.mjs +74 -0
- package/tests/provisioning_module_selection.mjs +64 -4
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
//
|
|
28
28
|
// Reaches core ONLY through the doorway src/module-api.js (the one-way rule, ADR
|
|
29
29
|
// 0083); it requires this module's own provisioning.js (same-dir), never a sibling.
|
|
30
|
+
// The GitHub-App callback page's presentation (esc / ACCENT / the stylesheet / the
|
|
31
|
+
// client script / callbackPage) lives in ./callback-page.js — moved out under task
|
|
32
|
+
// 1003570 because this file had reached the 1500-line fitness ratchet.
|
|
30
33
|
'use strict';
|
|
31
34
|
|
|
32
35
|
const crypto = require('node:crypto');
|
|
@@ -39,6 +42,7 @@ const { onboardPlan, stepsForShape } = require('../onboard-plan');
|
|
|
39
42
|
const { ALWAYS_ON_CORE, OPTIONAL_MODULES, starterBundleAnswerTable, optionalModules, effectiveModules } = require('../starter-bundles');
|
|
40
43
|
const { recommendationsForDetails } = require('../recommendations');
|
|
41
44
|
const { makeAskRateLimiter, clientIp } = require('../rate-limit');
|
|
45
|
+
const { callbackPage } = require('./callback-page');
|
|
42
46
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
43
47
|
const log = api.logger('provisioning');
|
|
44
48
|
|
|
@@ -139,142 +143,6 @@ function badModuleSelection(modules) {
|
|
|
139
143
|
return null;
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
// Minimal HTML escaper for the callback landing page (no template engine here).
|
|
143
|
-
function esc(s) {
|
|
144
|
-
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
|
|
145
|
-
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
146
|
-
));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Accent pair resolved from the instance's own branding — the pack is the palette
|
|
150
|
-
// authority (task 1002810); the literal fallbacks are the neutral pack's values, so
|
|
151
|
-
// a branding validation error still paints the current family, never a stale one.
|
|
152
|
-
const ACCENT = (() => {
|
|
153
|
-
try {
|
|
154
|
-
const c = api.branding().theme?.ui?.colors || {};
|
|
155
|
-
return { sun: c.accent || '#fb9569', sun600: c.accentDeep || '#a3481a' };
|
|
156
|
-
} catch { return { sun: '#fb9569', sun600: '#a3481a' }; }
|
|
157
|
-
})();
|
|
158
|
-
|
|
159
|
-
// Brand styling for the standalone onboarding pages — sunrise palette + Baloo 2 /
|
|
160
|
-
// Quicksand (accents from the branding pack, config/branding.neutral.json resolved
|
|
161
|
-
// under the instance's own config/branding.json). Light + dark aware; no external
|
|
162
|
-
// dependency beyond an optional Google-Fonts link (system fallback if it doesn't load).
|
|
163
|
-
const PAGE_STYLE = `
|
|
164
|
-
:root{color-scheme:light dark;--sun:${ACCENT.sun};--sun600:${ACCENT.sun600};--sunInk:#2a1305;--coral:#fb7185;--sky:#38bdf8;--sky600:#0284c7;--ok:#16a34a;--warn:#f59e0b;--ink:#2a1f17;--inkSoft:#6b5d52;--card:#fffdfa;--line:#f0e2cf;--bg1:#fde8c8;--bg2:#fff4e6;--bg3:#fffdfa}
|
|
165
|
-
@media (prefers-color-scheme:dark){:root{--ink:#f5efe6;--inkSoft:#cbbdad;--card:#241b13;--line:#3a2c1e;--bg1:#2a1d10;--bg2:#1b130b;--bg3:#130d07}}
|
|
166
|
-
*{box-sizing:border-box}html,body{height:100%}
|
|
167
|
-
body{margin:0;font-family:"Quicksand",system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ink);background:linear-gradient(180deg,var(--bg1) 0%,var(--bg2) 45%,var(--bg3) 100%);background-attachment:fixed;display:flex;align-items:center;justify-content:center;padding:1.5rem;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
|
168
|
-
.card{width:100%;max-width:30rem;background:var(--card);border:1px solid var(--line);border-radius:20px;padding:2.4rem 2rem 2rem;text-align:center;box-shadow:0 18px 50px -22px rgba(120,60,10,.4),0 2px 8px rgba(120,60,10,.07)}
|
|
169
|
-
.badge-warn{font-size:2.4rem;line-height:1;margin:0 auto 1rem}
|
|
170
|
-
h1{font-family:"Baloo 2",system-ui,sans-serif;font-weight:700;font-size:1.5rem;line-height:1.15;margin:.2rem 0 .55rem}
|
|
171
|
-
.sub{color:var(--inkSoft);font-size:1.02rem;line-height:1.5;margin:0 auto;max-width:24rem}
|
|
172
|
-
.sub p{margin:.6rem 0}
|
|
173
|
-
code{font-family:"Fira Code",ui-monospace,SFMono-Regular,monospace;background:rgba(56,189,248,.14);color:var(--sky600);padding:.12em .45em;border-radius:6px;font-size:.9em;overflow-wrap:anywhere}
|
|
174
|
-
@media (prefers-color-scheme:dark){code{color:var(--sky)}}
|
|
175
|
-
.steps{display:flex;align-items:flex-start;justify-content:center;margin:1.9rem 0 1.1rem}
|
|
176
|
-
.step{flex:1 1 0;max-width:9rem;position:relative}
|
|
177
|
-
.step::before{content:"";position:absolute;top:16px;left:-50%;width:100%;height:3px;background:var(--line);z-index:0;transition:background .4s ease}
|
|
178
|
-
.step:first-child::before{display:none}
|
|
179
|
-
.dot{width:34px;height:34px;border-radius:50%;margin:0 auto;display:grid;place-items:center;background:var(--bg3);border:2px solid var(--line);color:var(--inkSoft);font-weight:700;font-size:.95rem;position:relative;z-index:1;transition:all .4s ease}
|
|
180
|
-
.lbl{margin-top:.55rem;font-size:.78rem;color:var(--inkSoft);font-weight:600;line-height:1.25;padding:0 .2rem}
|
|
181
|
-
.step.done .dot{background:var(--ok);border-color:var(--ok);color:#fff}
|
|
182
|
-
.step.done::before{background:var(--ok)}
|
|
183
|
-
.step.active .dot{border-color:var(--sun);color:var(--sun600);animation:pulse 1.5s ease-in-out infinite}
|
|
184
|
-
.step.active .lbl{color:var(--ink)}
|
|
185
|
-
.step.active .dot::after{content:"";position:absolute;inset:-3px;border-radius:50%;border:2px solid transparent;border-top-color:var(--sun);animation:spin .8s linear infinite}
|
|
186
|
-
@keyframes spin{to{transform:rotate(360deg)}}
|
|
187
|
-
@keyframes pulse{0%,100%{box-shadow:0 0 0 4px rgba(249,115,22,.15)}50%{box-shadow:0 0 0 9px rgba(249,115,22,.04)}}
|
|
188
|
-
@media (prefers-reduced-motion:reduce){.step.active .dot,.step.active .dot::after{animation:none}}
|
|
189
|
-
.status-line{min-height:1.4em;color:var(--inkSoft);font-size:.94rem;margin:.4rem auto 0;max-width:24rem;line-height:1.45}
|
|
190
|
-
.cta{margin-top:1.4rem}
|
|
191
|
-
.btn{display:inline-block;background:var(--sun);color:var(--sunInk);text-decoration:none;font-weight:700;font-family:"Baloo 2",system-ui,sans-serif;padding:.72rem 1.5rem;border-radius:999px;font-size:1.02rem;box-shadow:0 10px 22px -8px rgba(249,115,22,.7);transition:transform .15s ease,box-shadow .15s ease}
|
|
192
|
-
.btn:hover{transform:translateY(-1px);box-shadow:0 14px 28px -8px rgba(249,115,22,.8)}
|
|
193
|
-
.muted{color:var(--inkSoft);opacity:.9;font-size:.85rem;margin:1.1rem auto 0;max-width:24rem;line-height:1.45}
|
|
194
|
-
.hidden{display:none!important}
|
|
195
|
-
`;
|
|
196
|
-
|
|
197
|
-
// Client script for the PROGRESS variant only (task 2096): poll the public status
|
|
198
|
-
// endpoint, advance the stepper, and auto-open the instance the moment its credentials
|
|
199
|
-
// land. Deliberately contains NO backticks and NO ${...} so it survives the outer
|
|
200
|
-
// template literal verbatim; the only server data it reads is the JSON config block
|
|
201
|
-
// written just above it. Fails soft — a poll hiccup just retries; a long wait reveals a
|
|
202
|
-
// manual "open" button rather than ever falsely erroring. The auto-open countdown runs
|
|
203
|
-
// ONLY when this page is the wizard's pop-up; a same-tab return (pop-up blocked) keeps
|
|
204
|
-
// the founder here with the door and a way back to the checklist (task 1003236).
|
|
205
|
-
const CALLBACK_SCRIPT = `
|
|
206
|
-
(function(){
|
|
207
|
-
var cfg=window.__CB_SETUP__||{};
|
|
208
|
-
var state=cfg.state||'',domain=cfg.domain||'';
|
|
209
|
-
if(!state||!domain)return;
|
|
210
|
-
var target='https://'+domain;
|
|
211
|
-
var pollUrl=location.pathname.replace('/callback','/status')+'?state='+encodeURIComponent(state);
|
|
212
|
-
var steps=document.getElementById('steps'),statusEl=document.getElementById('status');
|
|
213
|
-
var cta=document.getElementById('cta'),go=document.getElementById('go'),foot=document.getElementById('foot'),back=document.getElementById('back');
|
|
214
|
-
/* Opened from the wizard as a pop-up (window.opener alive, or the tab still carries the name the wizard gave it):
|
|
215
|
-
the checklist is in the other tab, so this one may auto-open the instance. Same-tab fallback (pop-up blocked,
|
|
216
|
-
form target=_self): this IS the founder's checklist tab — never navigate it away; offer both doors (task 1003236). */
|
|
217
|
-
var popup=!!window.opener||window.name==='ghAppSetup';
|
|
218
|
-
var finished=false,tries=0,manual=false;
|
|
219
|
-
function dot(n){return steps.querySelector('[data-step="'+n+'"]');}
|
|
220
|
-
function setActive(n){for(var i=1;i<=3;i++){var el=dot(i);if(!el)continue;el.classList.remove('active','done');if(i<n){el.classList.add('done');el.querySelector('.dot').textContent='\\u2713';}else if(i===n){el.classList.add('active');}}}
|
|
221
|
-
function allDone(){for(var i=1;i<=3;i++){var el=dot(i);if(!el)continue;el.classList.remove('active');el.classList.add('done');el.querySelector('.dot').textContent='\\u2713';}}
|
|
222
|
-
function ready(){if(finished)return;finished=true;allDone();statusEl.textContent='Your instance is ready \\u2014 sign in with GitHub to claim it as owner.';cta.classList.remove('hidden');
|
|
223
|
-
if(!popup){go.textContent='Open '+domain+' \\u2192';if(back)back.classList.remove('hidden');if(foot)foot.textContent='Your setup checklist is waiting where you left it \\u2014 finish it first, or open your instance now.';return;}
|
|
224
|
-
var n=4;(function tick(){if(n<=0){location.href=target;return;}go.textContent='Opening '+domain+' in '+n+'\\u2026';n--;setTimeout(tick,1000);})();}
|
|
225
|
-
function failed(){if(finished)return;finished=true;statusEl.textContent='Setup didn\\u2019t finish. Open your instance page and start the GitHub sign-in setup again.';cta.classList.remove('hidden');go.textContent='Open '+domain+' \\u2192';}
|
|
226
|
-
function revealManual(){if(manual||finished)return;manual=true;cta.classList.remove('hidden');go.textContent='Open '+domain+' \\u2192';if(foot)foot.textContent='This is taking a little longer than usual \\u2014 you can open your instance now, or keep waiting.';}
|
|
227
|
-
function schedule(){if(!finished)setTimeout(poll,3000);}
|
|
228
|
-
function poll(){tries++;fetch(pollUrl,{headers:{accept:'application/json'},cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(j){if(!j){schedule();return;}if(j.ready||j.status==='done'){ready();return;}if(j.status==='error'||j.status==='expired'){failed();return;}setActive(2);if(tries===40)revealManual();schedule();}).catch(function(){schedule();});}
|
|
229
|
-
setActive(2);poll();
|
|
230
|
-
})();
|
|
231
|
-
`;
|
|
232
|
-
|
|
233
|
-
// The standalone landing page for the public GitHub redirect (the mothership serves it;
|
|
234
|
-
// it is NOT the hall shell). Two variants share the Cloud Bongos brand shell (task 2096):
|
|
235
|
-
// • progress — ok + state + domain: an animated stepper that polls the status endpoint
|
|
236
|
-
// and auto-opens the instance when its credentials are placed.
|
|
237
|
-
// • message — everything else (errors + the no-domain success fallback): a calm badge
|
|
238
|
-
// + `body` (server-authored, trusted HTML).
|
|
239
|
-
function callbackPage({ ok, title, body, state, domain }) {
|
|
240
|
-
const progress = !!(ok && state && domain);
|
|
241
|
-
const cfgJson = JSON.stringify({ state: state || '', domain: domain || '' }).replace(/</g, '\\u003c');
|
|
242
|
-
const head = `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
243
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
244
|
-
<meta name="color-scheme" content="light dark">
|
|
245
|
-
<title>${esc(title)}</title>
|
|
246
|
-
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
247
|
-
<link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@600;700;800&family=Quicksand:wght@400;500;600;700&family=Fira+Code&display=swap" rel="stylesheet">
|
|
248
|
-
<style>${PAGE_STYLE}</style></head><body>`;
|
|
249
|
-
if (progress) {
|
|
250
|
-
return `${head}
|
|
251
|
-
<main class="card">
|
|
252
|
-
<h1>${esc(title)}</h1>
|
|
253
|
-
<p class="sub">Finishing sign-in setup for <code>${esc(domain)}</code>.</p>
|
|
254
|
-
<div class="steps" id="steps" aria-label="Setup progress">
|
|
255
|
-
<div class="step done" data-step="1"><div class="dot">✓</div><div class="lbl">GitHub app created</div></div>
|
|
256
|
-
<div class="step active" data-step="2"><div class="dot">2</div><div class="lbl">Placing credentials</div></div>
|
|
257
|
-
<div class="step" data-step="3"><div class="dot">3</div><div class="lbl">Ready to sign in</div></div>
|
|
258
|
-
</div>
|
|
259
|
-
<p class="status-line" id="status">Hang tight — keep this tab open while we finish.</p>
|
|
260
|
-
<div class="cta hidden" id="cta"><a class="btn" id="go" href="https://${esc(domain)}">Open ${esc(domain)} →</a></div>
|
|
261
|
-
<p class="muted hidden" id="back"><a href="/projects?view=new">Finish your remaining setup steps →</a></p>
|
|
262
|
-
<p class="muted" id="foot">This page opens <code>${esc(domain)}</code> for you the moment it’s ready.</p>
|
|
263
|
-
<noscript><p class="muted">When setup finishes, open <a href="https://${esc(domain)}">${esc(domain)}</a> and sign in with GitHub — that first sign-in makes you the owner.</p></noscript>
|
|
264
|
-
</main>
|
|
265
|
-
<script>window.__CB_SETUP__=${cfgJson};</script>
|
|
266
|
-
<script>${CALLBACK_SCRIPT}</script>
|
|
267
|
-
</body></html>`;
|
|
268
|
-
}
|
|
269
|
-
return `${head}
|
|
270
|
-
<main class="card">
|
|
271
|
-
<div class="badge-warn">${ok ? '✓' : '⚠️'}</div>
|
|
272
|
-
<h1>${esc(title)}</h1>
|
|
273
|
-
<div class="sub">${body || ''}</div>
|
|
274
|
-
</main>
|
|
275
|
-
</body></html>`;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
146
|
// Register this module's kernel seam (ADR 0083 / the registerBoxSeams precedent).
|
|
279
147
|
// Idempotent — the loader calls this factory once at boot, but tests may
|
|
280
148
|
// instantiate the router repeatedly; gating on hasProvider keeps the port
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Explore —
|
|
6
|
+
<title>Explore — projects on the map</title>
|
|
7
7
|
<style>
|
|
8
8
|
/* Self-contained so the showcase renders correctly whether or not the host
|
|
9
9
|
injects theme tokens. Warm Cloud Bongos defaults; overridable by the pack. */
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
salmon --accent is a fill and measures 2.1:1 as type on the card. */
|
|
16
16
|
--bg: #edecee; --card: #fbfafc; --ink: #17181a; --ink-soft: #55565a;
|
|
17
17
|
--rule: #c6c6c8; --accent: #fb9569; --accent-deep: #a3481a;
|
|
18
|
+
/* The black hole's own colour, and deliberately NOT a pack token: --ink
|
|
19
|
+
inverts between the schemes and would paint the void white in dark mode.
|
|
20
|
+
A hole is the one thing on this page that is the same colour in both. */
|
|
21
|
+
--orb-void: #0b0b10;
|
|
18
22
|
}
|
|
19
23
|
* { box-sizing: border-box; }
|
|
20
24
|
body {
|
|
@@ -39,6 +43,55 @@
|
|
|
39
43
|
.card .origin { color: var(--accent-deep); font-size: .82rem; word-break: break-all; margin-top: auto; }
|
|
40
44
|
.card .art { width: 100%; height: 120px; object-fit: cover; border-radius: 10px; margin-bottom: .3rem; }
|
|
41
45
|
.empty, .err { text-align: center; color: var(--ink-soft); padding: 3rem 1rem; }
|
|
46
|
+
|
|
47
|
+
/* THE THREE ORBS (BV1.R07, task 1002326; ADR 0182 D1) — how a project stands on
|
|
48
|
+
the map, DRAWN. The label beside each one is the plain word (Public / Private /
|
|
49
|
+
Stealth): the space words belong to the shape, not to the copy, which is the
|
|
50
|
+
goal's own interview decision. The orb is aria-hidden, so that plain word is
|
|
51
|
+
its accessible name. */
|
|
52
|
+
.state { display: flex; align-items: center; gap: .5rem; }
|
|
53
|
+
.state-label {
|
|
54
|
+
font-size: .74rem; letter-spacing: .09em; text-transform: uppercase;
|
|
55
|
+
color: var(--ink-soft);
|
|
56
|
+
}
|
|
57
|
+
.orb { width: 22px; height: 22px; border-radius: 50%; flex: 0 0 auto; position: relative; }
|
|
58
|
+
/* planet — identified and open: a lit sphere, ringed so you can pick it out */
|
|
59
|
+
.orb-planet {
|
|
60
|
+
background: radial-gradient(circle at 33% 30%, #ffffff 0 9%, var(--accent) 42%, var(--accent-deep) 100%);
|
|
61
|
+
}
|
|
62
|
+
.orb-planet::after {
|
|
63
|
+
content: ''; position: absolute; left: -26%; right: -26%; top: 40%; height: 24%;
|
|
64
|
+
border: 1.5px solid var(--accent-deep); border-radius: 50%;
|
|
65
|
+
transform: rotate(-20deg); opacity: .8;
|
|
66
|
+
}
|
|
67
|
+
/* star — identified but closed: visible from anywhere, with no surface to land on */
|
|
68
|
+
.orb-star {
|
|
69
|
+
background: radial-gradient(circle, #ffffff 0 15%, var(--accent) 38%, transparent 70%);
|
|
70
|
+
}
|
|
71
|
+
/* the rays taper rather than end, so the orb reads as a point of light and not
|
|
72
|
+
as a plus sign */
|
|
73
|
+
.orb-star::before, .orb-star::after {
|
|
74
|
+
content: ''; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
|
|
75
|
+
}
|
|
76
|
+
.orb-star::before {
|
|
77
|
+
width: 2px; height: 155%;
|
|
78
|
+
background: linear-gradient(to bottom, transparent, var(--accent) 50%, transparent);
|
|
79
|
+
}
|
|
80
|
+
.orb-star::after {
|
|
81
|
+
width: 155%; height: 2px;
|
|
82
|
+
background: linear-gradient(to right, transparent, var(--accent) 50%, transparent);
|
|
83
|
+
}
|
|
84
|
+
/* black hole — existence without identity: you are shown the ring, never the object */
|
|
85
|
+
.orb-black-hole {
|
|
86
|
+
background: var(--orb-void);
|
|
87
|
+
box-shadow: 0 0 0 1.5px var(--accent), 0 0 10px 1px var(--accent);
|
|
88
|
+
}
|
|
89
|
+
/* an anonymous card has nothing to click and nothing to name, so it does not
|
|
90
|
+
pretend otherwise — no lift on hover, and its heading reads as the placeholder
|
|
91
|
+
it is rather than as a project's name */
|
|
92
|
+
.card.anon:hover { transform: none; box-shadow: none; }
|
|
93
|
+
.card.anon h2 { color: var(--ink-soft); font-weight: 600; }
|
|
94
|
+
|
|
42
95
|
@media (prefers-color-scheme: dark) {
|
|
43
96
|
:root { --bg: #000000; --card: #101012; --ink: #ffffff; --ink-soft: #c0c0c0; --rule: rgba(255,255,255,.16);
|
|
44
97
|
--accent-deep: color-mix(in srgb, var(--accent) 45%, #ffffff); }
|
|
@@ -49,10 +102,10 @@
|
|
|
49
102
|
<div class="wrap">
|
|
50
103
|
<header>
|
|
51
104
|
<h1>Explore</h1>
|
|
52
|
-
<p class="sub">
|
|
105
|
+
<p class="sub">Projects on the map, and how each one stands on it.</p>
|
|
53
106
|
</header>
|
|
54
107
|
<div id="grid" class="grid" hidden></div>
|
|
55
|
-
<div id="empty" class="empty" hidden>No
|
|
108
|
+
<div id="empty" class="empty" hidden>No projects on the map yet — check back soon.</div>
|
|
56
109
|
<div id="err" class="err" hidden>Couldn't load projects right now.</div>
|
|
57
110
|
</div>
|
|
58
111
|
<script src="/explore.js"></script>
|
|
@@ -1,15 +1,39 @@
|
|
|
1
|
-
// explore.js — the public Explore showcase (ADR 0141 §5, task #1002110)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// explore.js — the public Explore showcase (ADR 0141 §5, task #1002110), and since
|
|
2
|
+
// BV1.R07 (task 1002326) the in-repo surface that DOGFOODS the visible-projects feed:
|
|
3
|
+
// every project on the map, drawn as the state it stands in.
|
|
4
|
+
//
|
|
5
|
+
// Fetches the hub's public projects feed and renders one card each. On a NON-hub
|
|
6
|
+
// instance (platform-identity off) the endpoint 404s / returns nothing, so the page
|
|
7
|
+
// shows a graceful empty state.
|
|
8
|
+
//
|
|
9
|
+
// WHAT A CARD SAYS ABOUT VISIBILITY (ADR 0182 D1/D2a). Each feed row carries the
|
|
10
|
+
// render type the server derived from the project's join door — planet, star or
|
|
11
|
+
// black hole. This page draws that shape and labels it with the plain word
|
|
12
|
+
// (Public / Private / Stealth), because the space words are the drawn orb's and
|
|
13
|
+
// not the copy's. A black hole is ANONYMOUS: the feed already nulled everything
|
|
14
|
+
// identifying server-side, and the branch that renders it here reads no
|
|
15
|
+
// identifying field at all, so a server that regressed still could not put a name
|
|
16
|
+
// on this page. Dark matter — a project that has not said what it is — is excluded
|
|
17
|
+
// in SQL and never arrives; a row this page cannot classify is dropped rather than
|
|
18
|
+
// guessed at, which is the same fail-closed rule one layer down.
|
|
4
19
|
//
|
|
5
20
|
// Project fields come from the network, so every value is set via textContent /
|
|
6
21
|
// attribute assignment — never innerHTML — so a project name/tagline can't inject markup.
|
|
22
|
+
//
|
|
23
|
+
// Proof: ../../../tests/explore_visible_states.mjs
|
|
7
24
|
(function () {
|
|
8
25
|
'use strict';
|
|
9
26
|
var grid = document.getElementById('grid');
|
|
10
27
|
var empty = document.getElementById('empty');
|
|
11
28
|
var err = document.getElementById('err');
|
|
12
29
|
|
|
30
|
+
// The map's render types → the plain word shown beside each one. Keyed on the
|
|
31
|
+
// feed's own `orb` field rather than re-derived from `state`: the state→orb
|
|
32
|
+
// mapping has exactly one home (modules/platform-identity/projects-feed.js) and a
|
|
33
|
+
// second copy here is a copy that drifts. An orb missing from this table is a row
|
|
34
|
+
// this page cannot honestly draw, so it draws nothing.
|
|
35
|
+
var ORB_LABEL = { planet: 'Public', star: 'Private', 'black-hole': 'Stealth' };
|
|
36
|
+
|
|
13
37
|
function show(el) { if (el) el.hidden = false; }
|
|
14
38
|
|
|
15
39
|
// Only http(s) origins are linkified — never javascript:/data: etc.
|
|
@@ -20,12 +44,50 @@
|
|
|
20
44
|
} catch (_) { return null; }
|
|
21
45
|
}
|
|
22
46
|
|
|
47
|
+
// The orb + its plain word. The shape is decorative (aria-hidden) — the label is
|
|
48
|
+
// what a screen reader gets, because a shape it cannot see is not a name.
|
|
49
|
+
function stateRow(orb) {
|
|
50
|
+
var row = document.createElement('div');
|
|
51
|
+
row.className = 'state';
|
|
52
|
+
var dot = document.createElement('span');
|
|
53
|
+
dot.className = 'orb orb-' + orb;
|
|
54
|
+
dot.setAttribute('aria-hidden', 'true');
|
|
55
|
+
row.appendChild(dot);
|
|
56
|
+
var label = document.createElement('span');
|
|
57
|
+
label.className = 'state-label';
|
|
58
|
+
label.textContent = ORB_LABEL[orb];
|
|
59
|
+
row.appendChild(label);
|
|
60
|
+
return row;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A black hole publishes EXISTENCE and nothing else (ADR 0182 D2a). Deliberately
|
|
64
|
+
// takes no project argument: there is no identifying value in scope to leak by
|
|
65
|
+
// accident, now or when the feed grows a column.
|
|
66
|
+
function anonymousCard() {
|
|
67
|
+
var el = document.createElement('div');
|
|
68
|
+
el.className = 'card anon';
|
|
69
|
+
el.appendChild(stateRow('black-hole'));
|
|
70
|
+
var h = document.createElement('h2');
|
|
71
|
+
h.textContent = 'A project';
|
|
72
|
+
el.appendChild(h);
|
|
73
|
+
var p = document.createElement('p');
|
|
74
|
+
p.textContent = 'Something is here. Its name, and everything else about it, stay with its owner.';
|
|
75
|
+
el.appendChild(p);
|
|
76
|
+
return el;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// One feed row → one card, or null when the row is not drawable.
|
|
23
80
|
function card(p) {
|
|
81
|
+
var orb = String((p && p.orb) || '');
|
|
82
|
+
if (!Object.prototype.hasOwnProperty.call(ORB_LABEL, orb)) return null;
|
|
83
|
+
if (orb === 'black-hole') return anonymousCard();
|
|
84
|
+
|
|
24
85
|
var href = safeHref(p.origin);
|
|
25
86
|
var el = document.createElement(href ? 'a' : 'div');
|
|
26
87
|
el.className = 'card';
|
|
27
88
|
if (href) { el.href = href; el.target = '_blank'; el.rel = 'noopener noreferrer'; }
|
|
28
89
|
|
|
90
|
+
el.appendChild(stateRow(orb));
|
|
29
91
|
if (p.art_url && safeHref(p.art_url)) {
|
|
30
92
|
var img = document.createElement('img');
|
|
31
93
|
img.className = 'art';
|
|
@@ -55,8 +117,12 @@
|
|
|
55
117
|
.then(function (r) { if (!r.ok) throw new Error('http ' + r.status); return r.json(); })
|
|
56
118
|
.then(function (data) {
|
|
57
119
|
var projects = (data && Array.isArray(data.projects)) ? data.projects : [];
|
|
58
|
-
|
|
59
|
-
|
|
120
|
+
// Filtered BEFORE the count is read: a feed of rows none of which can be drawn
|
|
121
|
+
// is an empty map, and a visitor is owed the empty state rather than a grid
|
|
122
|
+
// with nothing in it.
|
|
123
|
+
var cards = projects.map(card).filter(Boolean);
|
|
124
|
+
if (cards.length === 0) { show(empty); return; }
|
|
125
|
+
cards.forEach(function (el) { grid.appendChild(el); });
|
|
60
126
|
show(grid);
|
|
61
127
|
})
|
|
62
128
|
.catch(function () {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.531",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.531",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.531",
|
|
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/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.531'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -132,7 +132,7 @@ test('the explore page mirrors the pack, var for var', () => {
|
|
|
132
132
|
|
|
133
133
|
test('the provisioning onboarding pages resolve their accent pair from branding', () => {
|
|
134
134
|
const source = fs.readFileSync(
|
|
135
|
-
path.join(REPO_ROOT, 'modules/provisioning/routes/
|
|
135
|
+
path.join(REPO_ROOT, 'modules/provisioning/routes/callback-page.js'), 'utf8');
|
|
136
136
|
assert.match(source, /--sun:\$\{/, '--sun interpolates a resolved value, never a literal');
|
|
137
137
|
assert.match(source, /--sun600:\$\{/, '--sun600 interpolates a resolved value, never a literal');
|
|
138
138
|
const block = source.match(/const ACCENT =[\s\S]*?\}\)\(\);/);
|