@bongos/core 1.19.429 → 1.19.430
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 +38 -33
- package/clients/bongos-client/index.d.ts +3 -3
- package/docs/api/openapi.json +10 -0
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +114 -106
- package/docs/copy-registry.json +214 -141
- package/docs/design/mocks/hub/picker.html +421 -0
- package/docs/design/projects-hub-direction-v2.md +22 -0
- package/docs/module-api-changelog.md +2 -0
- package/modules/platform-identity/routes/projects.js +24 -0
- package/modules/provisioning/module.json +1 -1
- package/modules/provisioning/provisioning.js +63 -2
- package/modules/provisioning/routes/provisioning.js +28 -6
- package/modules/provisioning/tests/provisioning.mjs +34 -0
- package/modules/ui-design/kit/fixtures/featured-hub.json +1 -1
- package/modules/ui-design/kit/fixtures/featured.json +1 -1
- package/modules/ui-design/kit/fixtures/provisioning-instances.json +1 -1
- package/modules/ui-design/kit/serve.js +28 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +37 -0
- package/public-landing/projects.html +164 -5
- package/public-landing/projects.probes.json +12 -0
- package/public-landing/projects.states.json +3 -2
- package/src/module-api.js +1 -1
- package/tests/projects_hub.mjs +122 -7
- package/tests/provisioning_settings.mjs +81 -2
- package/tests/provisioning_settings_env.mjs +6 -0
|
@@ -744,6 +744,22 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
744
744
|
</details>
|
|
745
745
|
</div>
|
|
746
746
|
|
|
747
|
+
<!-- the planet picker (task 1003347; direction record §16, v1 the bands): the
|
|
748
|
+
owner's own choice of how the platform draws this project — the sky,
|
|
749
|
+
the list, this page's head and the card read the same row. The pick is
|
|
750
|
+
a settings save with a receipt, like the two above; unlike them it
|
|
751
|
+
reaches no instance and restarts nothing (the planet is the platform's
|
|
752
|
+
to draw). The fold is a SIBLING of the volatile slot so no re-render
|
|
753
|
+
can drop it. -->
|
|
754
|
+
<div class="card mSec" id="mPlanetPick">
|
|
755
|
+
<h2>Its planet</h2>
|
|
756
|
+
<div id="mPick"><p class="finePrint">Reading the project…</p></div>
|
|
757
|
+
<details class="finePrint" id="mPickWhat">
|
|
758
|
+
<summary>What the choice changes</summary>
|
|
759
|
+
<p>How this project is drawn: on the sky, in your list, at the top of this page and on its card. A planet is a <b>shape</b> (the blob, the bongo pair, the bead), a <b>material</b> (chrome, frost, gold, obsidian), a <b>style</b> (tinted: the colour replaces the material’s own; washed: the material lit by the colour; plain: as it is) and a <b>colour</b>, one of the world’s five. Until you choose, the platform picks one for it. Saving here changes nothing on the project itself: no restart, nothing to apply.</p>
|
|
760
|
+
</details>
|
|
761
|
+
</div>
|
|
762
|
+
|
|
747
763
|
<!-- live since task 1003042: the teardown control, for a project in ANY
|
|
748
764
|
state that still holds something. The server used to refuse a
|
|
749
765
|
'requested' or an 'error' project outright — and force-teardown reused
|
|
@@ -1300,6 +1316,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1300
1316
|
were orphans). Keeping the per-project variety is what makes a sky legible;
|
|
1301
1317
|
deriving it from contract tokens is what makes it survive a re-brand. */
|
|
1302
1318
|
var ORB_ACCENTS = ['var(--sea)', 'var(--terracotta)', 'var(--gold-soft)', 'var(--sun-lit)', 'var(--accent)'];
|
|
1319
|
+
/* the five BY NAME — what an owner's choice stores (task 1003347: a token name,
|
|
1320
|
+
never a colour), in the same order, so a name maps to its var() */
|
|
1321
|
+
var ORB_ACCENT_NAMES = ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'];
|
|
1322
|
+
function accentVar(name) { var i = ORB_ACCENT_NAMES.indexOf(name); return i === -1 ? '' : ORB_ACCENTS[i]; }
|
|
1323
|
+
/* a template name the sky knows, or '' — an unknown name is not a choice */
|
|
1324
|
+
function knownTemplate(name) { return ORB_TEMPLATES.indexOf(name) !== -1 ? name : ''; }
|
|
1303
1325
|
/* THE PLANET TEMPLATES (task 1003319; the materials, task 1003337). A project's
|
|
1304
1326
|
orb is one of the world's three whole objects (blob / pair / bead) in one of
|
|
1305
1327
|
four materials (chrome — the first round, named by omission — frost, gold,
|
|
@@ -1357,9 +1379,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1357
1379
|
p._tpl = ORB_WALK[i % ORB_WALK.length];
|
|
1358
1380
|
});
|
|
1359
1381
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1382
|
+
/* the owner's own choice first (task 1003347: planet_accent / planet_template ride
|
|
1383
|
+
the featured feed and the owned row alike), then the walk */
|
|
1384
|
+
function accentOf(p) { return accentVar(p.planet_accent) || p._acc || ORB_ACCENTS[0]; }
|
|
1385
|
+
/* the owner's choice wins, then a curated template, then the walk; an unknown name is not a choice */
|
|
1362
1386
|
function templateOf(p) {
|
|
1387
|
+
if (knownTemplate(p.planet_template)) return p.planet_template;
|
|
1363
1388
|
return ORB_TEMPLATES.indexOf(p.template) !== -1 ? p.template : (p._tpl || ORB_WALK[0]);
|
|
1364
1389
|
}
|
|
1365
1390
|
/* the manage cards' accent rule — one project, one colour, in the sky AND
|
|
@@ -4270,13 +4295,19 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4270
4295
|
by its slug, the rule the map's appended entries use — so one project is
|
|
4271
4296
|
one object in both views and never re-colours between them. ── */
|
|
4272
4297
|
function planetOf(inst) {
|
|
4298
|
+
/* the owner's own choice (task 1003347) rides the row itself, so it wins in
|
|
4299
|
+
every view before the catalog or the walk is asked; the sky reads the same
|
|
4300
|
+
choice off the featured feed (templateOf / accentOf), so the two agree */
|
|
4301
|
+
var own = { tpl: knownTemplate(inst.planet_template), acc: accentVar(inst.planet_accent) };
|
|
4273
4302
|
var h = inst.domain ? hostOf(inst.domain) : '';
|
|
4274
4303
|
for (var i = 0; h && i < catalog.length; i++) {
|
|
4275
4304
|
if (hostOf(catalog[i].origin) === h) {
|
|
4276
|
-
|
|
4305
|
+
var c = catalog[i];
|
|
4306
|
+
return { tpl: own.tpl || templateOf(c), acc: own.acc || accentOf(c), curated: ORB_TEMPLATES.indexOf(c.template) !== -1,
|
|
4307
|
+
chosen: !!(own.tpl || own.acc || knownTemplate(c.planet_template) || accentVar(c.planet_accent)) };
|
|
4277
4308
|
}
|
|
4278
4309
|
}
|
|
4279
|
-
return { tpl: slugTemplate(inst.slug), acc: ORB_ACCENTS[slugAccent(inst.slug)], curated: false };
|
|
4310
|
+
return { tpl: own.tpl || slugTemplate(inst.slug), acc: own.acc || ORB_ACCENTS[slugAccent(inst.slug)], curated: false, chosen: !!(own.tpl || own.acc) };
|
|
4280
4311
|
}
|
|
4281
4312
|
/* shape[-material]-style → the data attributes cosmos.css draws from (the map's rule) */
|
|
4282
4313
|
function planetAttrs(pl) {
|
|
@@ -4299,6 +4330,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4299
4330
|
function repaintPlanets() {
|
|
4300
4331
|
Object.keys(byId).forEach(function (id) { renderCard(byId[id]); });
|
|
4301
4332
|
if (manageInst) paintManagePlanet(manageInst);
|
|
4333
|
+
/* the picker may have seeded from the walk before the catalog landed: reseed it
|
|
4334
|
+
only if the resolution actually changed — the owner's pending pick, and a
|
|
4335
|
+
receipt just shown, stand otherwise */
|
|
4336
|
+
if (manageInst && !mPickTouched && JSON.stringify(pickFrom(planetOf(manageInst))) !== mPickSeed) { mPick = null; renderPick(manageInst); }
|
|
4302
4337
|
}
|
|
4303
4338
|
|
|
4304
4339
|
/* ── the row's object: the planet, its state on the object (ghost = torn
|
|
@@ -4808,7 +4843,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4808
4843
|
function paintManagePlanet(inst) {
|
|
4809
4844
|
var pl = planetOf(inst);
|
|
4810
4845
|
$('mOrb').innerHTML = orbFor(inst);
|
|
4811
|
-
$('mPlanet').innerHTML = ledgerRow(['planet', planetWords(pl) + (pl.curated ? '' : '<small>chosen for it by the platform.</small>')]);
|
|
4846
|
+
$('mPlanet').innerHTML = ledgerRow(['planet', planetWords(pl) + (pl.chosen ? '<small>your choice.</small>' : pl.curated ? '' : '<small>chosen for it by the platform.</small>')]);
|
|
4812
4847
|
$('mPlanet').hidden = false;
|
|
4813
4848
|
}
|
|
4814
4849
|
function renderManage(inst) {
|
|
@@ -4846,6 +4881,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4846
4881
|
only, so a queued settings push shows up on the tick without a reload */
|
|
4847
4882
|
renderVisState(inst);
|
|
4848
4883
|
renderJoinState(inst);
|
|
4884
|
+
/* its planet (task 1003347) reads off the row — fetch-free too; a pick in
|
|
4885
|
+
progress survives the tick because only a different project re-renders it */
|
|
4886
|
+
renderPick(inst);
|
|
4849
4887
|
/* the page names its project — the heading holds #mName; and in the tab */
|
|
4850
4888
|
document.title = (inst.slug ? inst.slug + ' — ' : '') + BASE_TITLE;
|
|
4851
4889
|
/* the card's own story, on the project's own page (task 1003232): the
|
|
@@ -5299,6 +5337,127 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5299
5337
|
if (t && t.hasAttribute('data-vis-save')) doVisSave();
|
|
5300
5338
|
});
|
|
5301
5339
|
|
|
5340
|
+
/* ── its planet (task 1003347; direction record §16, v1 the bands): the owner's
|
|
5341
|
+
own choice of how the platform draws the project. Reads OFF THE ROW
|
|
5342
|
+
(publicInstance carries planet_template / planet_accent), so the section is
|
|
5343
|
+
fetch-free like the ledger and safe on the minute tick; the save is the
|
|
5344
|
+
settings PATCH the two sections above use, and its reply is a receipt — the
|
|
5345
|
+
head, the ledger row and the list repaint from the row, never from words in
|
|
5346
|
+
the reply. A planet is shape × material × style (ORB_TEMPLATES, the sky's own
|
|
5347
|
+
names) in one of the five accents BY NAME. ── */
|
|
5348
|
+
var PICK_SHAPES = [['blob', 'the blob'], ['pair', 'the bongo pair'], ['bead', 'the bead']];
|
|
5349
|
+
var PICK_MATERIALS = [['chrome', 'chrome'], ['frost', 'frost'], ['gold', 'gold'], ['obsidian', 'obsidian']];
|
|
5350
|
+
var PICK_STYLES = [['tint', 'tinted'], ['wash', 'washed'], ['plain', 'plain']];
|
|
5351
|
+
var PICK_ACCENTS = [['sea', 'sea'], ['terracotta', 'terracotta'], ['gold-soft', 'soft gold'], ['sun-lit', 'sunlit'], ['accent', 'the accent']];
|
|
5352
|
+
var PICK_FIELDS = { mPickShape: 'shape', mPickMaterial: 'material', mPickStyle: 'style', mPickAccent: 'accent' };
|
|
5353
|
+
var mPick = null; /* the pending choice: { shape, material, style, accent } */
|
|
5354
|
+
var mPickFor = null; /* which project it belongs to — a different one starts afresh */
|
|
5355
|
+
var mPickTouched = false; /* the owner changed something since the seed — a late catalog must not undo it */
|
|
5356
|
+
var mPickSeed = ''; /* what the seed resolved to, so a late catalog rebuilds only on a real change */
|
|
5357
|
+
/* shape × material × style → the template name (the chrome material's plain is spelled `chrome`) */
|
|
5358
|
+
function pickTemplate(s) {
|
|
5359
|
+
if (s.material === 'chrome') return s.shape + '-' + (s.style === 'plain' ? 'chrome' : s.style);
|
|
5360
|
+
return s.shape + '-' + s.material + '-' + s.style;
|
|
5361
|
+
}
|
|
5362
|
+
/* a drawn planet → the four fields (the walk's var() back to its name) */
|
|
5363
|
+
function pickFrom(pl) {
|
|
5364
|
+
var parts = pl.tpl.split('-');
|
|
5365
|
+
var style = parts[parts.length - 1];
|
|
5366
|
+
return { shape: parts[0], material: parts.length === 3 ? parts[1] : 'chrome', style: style === 'chrome' ? 'plain' : style,
|
|
5367
|
+
accent: ORB_ACCENT_NAMES[ORB_ACCENTS.indexOf(pl.acc)] || ORB_ACCENT_NAMES[0] };
|
|
5368
|
+
}
|
|
5369
|
+
/* one band: the label, then the choices as hairline cells of one band, a word each */
|
|
5370
|
+
function pickBand(label, name, opts, on) {
|
|
5371
|
+
return '<div class="pickBand"><span class="pickLabel">' + label + '</span><div class="pickCells" role="radiogroup" aria-label="' + label + '">' +
|
|
5372
|
+
opts.map(function (o) {
|
|
5373
|
+
return '<label class="pickCell' + (o[0] === on ? ' on' : '') + '"><input type="radio" name="' + name + '" value="' + esc(o[0]) + '"' +
|
|
5374
|
+
(o[0] === on ? ' checked' : '') + '>' + esc(o[1]) + '</label>';
|
|
5375
|
+
}).join('') + '</div></div>';
|
|
5376
|
+
}
|
|
5377
|
+
function renderPick(inst) {
|
|
5378
|
+
var pl = planetOf(inst);
|
|
5379
|
+
if (!mPick || mPickFor !== inst.id) { mPick = pickFrom(pl); mPickSeed = JSON.stringify(mPick); mPickFor = inst.id; mPickTouched = false; $('mPick').innerHTML = ''; }
|
|
5380
|
+
else if ($('mPick').querySelector('.pick')) return; /* the tick: a pick in progress stands */
|
|
5381
|
+
var s = mPick;
|
|
5382
|
+
var preview = { tpl: pickTemplate(s), acc: accentVar(s.accent) };
|
|
5383
|
+
$('mPick').innerHTML =
|
|
5384
|
+
'<div class="pick">' +
|
|
5385
|
+
'<div class="pickPreview"><span class="worldOrb"' + planetAttrs(preview) + ' style="--acc:' + preview.acc + ';--d:132px" aria-hidden="true"><span class="disc"></span></span>' +
|
|
5386
|
+
'<p class="pickWords">' + planetWords(preview) + '<small>' + (pl.chosen ? 'your choice' : 'the platform’s pick for it') + '</small></p></div>' +
|
|
5387
|
+
'<div class="pickBands">' +
|
|
5388
|
+
pickBand('Shape', 'mPickShape', PICK_SHAPES, s.shape) +
|
|
5389
|
+
pickBand('Material', 'mPickMaterial', PICK_MATERIALS, s.material) +
|
|
5390
|
+
pickBand('Style', 'mPickStyle', PICK_STYLES, s.style) +
|
|
5391
|
+
'<div class="pickBand"><span class="pickLabel">Colour</span><div class="pickSwatches" role="radiogroup" aria-label="Colour">' +
|
|
5392
|
+
PICK_ACCENTS.map(function (a) {
|
|
5393
|
+
return '<label class="pickSw' + (a[0] === s.accent ? ' on' : '') + '" style="--sw:' + accentVar(a[0]) + '"><input type="radio" name="mPickAccent" value="' + a[0] + '"' +
|
|
5394
|
+
(a[0] === s.accent ? ' checked' : '') + '><span>' + esc(a[1]) + '</span></label>';
|
|
5395
|
+
}).join('') + '</div></div>' +
|
|
5396
|
+
'</div>' +
|
|
5397
|
+
'</div>' +
|
|
5398
|
+
'<div class="actions"><button type="button" class="btn small" data-pick-save>Save its planet</button>' +
|
|
5399
|
+
(pl.chosen ? '<button type="button" class="btn small ghost" data-pick-reset>Let the platform choose again</button>' : '') + '</div>' +
|
|
5400
|
+
'<div class="projMsg" id="mPickMsg" role="status" aria-live="polite"></div>';
|
|
5401
|
+
}
|
|
5402
|
+
$('mPick').addEventListener('change', function (ev) {
|
|
5403
|
+
var t = ev.target;
|
|
5404
|
+
var key = t && t.type === 'radio' ? PICK_FIELDS[t.name] : null;
|
|
5405
|
+
if (!key || !manageInst || !mPick) return;
|
|
5406
|
+
mPick[key] = t.value;
|
|
5407
|
+
mPickTouched = true;
|
|
5408
|
+
$('mPick').innerHTML = ''; /* the preview follows the pick: redraw, keeping the cell's focus */
|
|
5409
|
+
renderPick(manageInst);
|
|
5410
|
+
var again = document.querySelector('#mPick input[name="' + t.name + '"][value="' + t.value + '"]');
|
|
5411
|
+
if (again) again.focus({ preventScroll: true });
|
|
5412
|
+
});
|
|
5413
|
+
$('mPick').addEventListener('click', function (ev) {
|
|
5414
|
+
var t = ev.target.closest && ev.target.closest('button');
|
|
5415
|
+
if (!t) return;
|
|
5416
|
+
if (t.hasAttribute('data-pick-save')) doPickSave(false);
|
|
5417
|
+
else if (t.hasAttribute('data-pick-reset')) doPickSave(true);
|
|
5418
|
+
});
|
|
5419
|
+
/* the save: the settings PATCH; null hands the pick back to the platform */
|
|
5420
|
+
function doPickSave(reset) {
|
|
5421
|
+
if (!manageInst || !mPick) return;
|
|
5422
|
+
var body = reset ? { planet_template: null, planet_accent: null } : { planet_template: pickTemplate(mPick), planet_accent: mPick.accent };
|
|
5423
|
+
var box = $('mPickMsg');
|
|
5424
|
+
document.querySelectorAll('#mPick .actions button').forEach(function (b) { b.disabled = true; });
|
|
5425
|
+
box.className = 'projMsg'; box.textContent = 'Saving…';
|
|
5426
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(manageId) + '/settings', {
|
|
5427
|
+
method: 'PATCH', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
5428
|
+
body: JSON.stringify(body)
|
|
5429
|
+
}).then(function (res) {
|
|
5430
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
5431
|
+
if (res.ok) {
|
|
5432
|
+
/* the row now holds the choice: the head, the ledger row and the list
|
|
5433
|
+
row repaint from it, and the sky re-reads the feed that carries it */
|
|
5434
|
+
var planet = data.planet || {};
|
|
5435
|
+
manageInst.planet_template = planet.planet_template || null;
|
|
5436
|
+
manageInst.planet_accent = planet.planet_accent || null;
|
|
5437
|
+
var row = byId[manageInst.id];
|
|
5438
|
+
if (row) { row.planet_template = manageInst.planet_template; row.planet_accent = manageInst.planet_accent; }
|
|
5439
|
+
mPick = null; mPickTouched = false;
|
|
5440
|
+
$('mPick').innerHTML = '';
|
|
5441
|
+
repaintPlanets();
|
|
5442
|
+
var done = $('mPickMsg');
|
|
5443
|
+
done.className = 'projMsg ok';
|
|
5444
|
+
done.textContent = (data.push && data.push.message) || 'Saved.';
|
|
5445
|
+
loadCatalog();
|
|
5446
|
+
return;
|
|
5447
|
+
}
|
|
5448
|
+
var e = apiError(data, res.status);
|
|
5449
|
+
if (res.status === 401) { box.className = 'projMsg err'; box.innerHTML = signInAgain(); return; }
|
|
5450
|
+
box.className = 'projMsg err';
|
|
5451
|
+
box.textContent = e.message || 'The choice could not be saved — try again in a moment.';
|
|
5452
|
+
});
|
|
5453
|
+
}).catch(function () {
|
|
5454
|
+
box.className = 'projMsg err';
|
|
5455
|
+
box.textContent = 'Couldn’t reach the platform — check your connection and try again.';
|
|
5456
|
+
}).then(function () {
|
|
5457
|
+
document.querySelectorAll('#mPick .actions button').forEach(function (b) { b.disabled = false; });
|
|
5458
|
+
});
|
|
5459
|
+
}
|
|
5460
|
+
|
|
5302
5461
|
/* ── how people join (task 1003044): the joinability toggle — the visibility
|
|
5303
5462
|
section's shape, key for key: the CHOICE from the platform's settings row,
|
|
5304
5463
|
what the project RUNS from its own manifest (brand.project.joinability),
|
|
@@ -129,6 +129,18 @@
|
|
|
129
129
|
{ "name": "manage flows: how people join — pick a cell, save, the receipt lands under the choice", "auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
|
|
130
130
|
"steps": [["wait",900],["check","input[name=\"mJoinPick\"][value=\"apply\"]"],["click","[data-join-save]"],["wait",600]],
|
|
131
131
|
"expect": {"text":[["#mJoinMsg","Saved. The project restarts to apply it."],["#mName","demo"]],"checked":"input[name=\"mJoinPick\"][value=\"apply\"]","visible":["#mOrb .worldOrb","#mFacts .ledger .k"]} },
|
|
132
|
+
{ "name": "manage flows: its planet (task 1003347) — pick the bongo pair in gold, washed, on terracotta; save; the receipt lands and the head and the ledger row repaint from the row",
|
|
133
|
+
"auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
|
|
134
|
+
"steps": [["wait",900],["check","#mPick input[name=\"mPickShape\"][value=\"pair\"]"],["wait",100],["check","#mPick input[name=\"mPickMaterial\"][value=\"gold\"]"],["wait",100],["check","#mPick input[name=\"mPickStyle\"][value=\"wash\"]"],["wait",100],["check","#mPick input[name=\"mPickAccent\"][value=\"terracotta\"]"],["wait",100],["click","[data-pick-save]"],["wait",700]],
|
|
135
|
+
"expect": {"text":[["#mPickMsg","Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it."],["#mPlanet .v","the bongo pair in gold, washedyour choice."],["#mName","demo"]],"hasClass":[["#mPickMsg","ok",true]],"attr":[["#mOrb .worldOrb","data-template","pair-gold-wash"],["#mPick .pickPreview .worldOrb","data-template","pair-gold-wash"]],"checked":"#mPick input[name=\"mPickAccent\"][value=\"terracotta\"]","visible":["#mPick [data-pick-reset]"]} },
|
|
136
|
+
{ "name": "manage flows: its planet — the pick handed back to the platform: the row's choice clears and the head falls back to the catalog's",
|
|
137
|
+
"auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
|
|
138
|
+
"steps": [["wait",900],["click","[data-pick-reset]"],["wait",700]],
|
|
139
|
+
"expect": {"text":[["#mPickMsg","Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it."],["#mName","demo"]],"hasClass":[["#mPickMsg","ok",true]],"attr":[["#mOrb .worldOrb","data-template","pair-obsidian-tint"]]} },
|
|
140
|
+
{ "name": "manage flows: its planet at 390 — the bands fold to pills and a pick still saves",
|
|
141
|
+
"auth": true, "width": 390, "url": "/projects?view=manage&id=101",
|
|
142
|
+
"steps": [["wait",900],["check","#mPick input[name=\"mPickShape\"][value=\"blob\"]"],["wait",100],["click","[data-pick-save]"],["wait",700]],
|
|
143
|
+
"expect": {"hasClass":[["#mPickMsg","ok",true]],"attr":[["#mOrb .worldOrb","data-shape","blob"]],"noOverflow":true} },
|
|
132
144
|
{ "name": "manage flows: the ender asks for the name typed back, queues the teardown, and the page re-reads the project as queued", "auth": true, "width": 1440, "url": "/projects?view=manage&id=105",
|
|
133
145
|
"steps": [["wait",900],["click","[data-del-ask]"],["wait",200],["fill","#mDelName","mooring"],["wait",100],["click","[data-del-yes]"],["wait",900]],
|
|
134
146
|
"expect": {"text":[["#mDel .addr","A teardown is queued for this project."],["#mName","mooring"]],"hidden":["#mDelName","[data-del-ask]"],"visible":["#mStrip .strip.flowDown"]} },
|
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
"ignoreControls": [".skip", ".vh"],
|
|
10
10
|
"states": {
|
|
11
11
|
"out-map": { "auth": false, "expect": { "expanded": false } },
|
|
12
|
-
"in-map": { "auth": true, "expect": { "expanded": false, "count": [["#orbField .worldOrb[data-shape][data-style]", 7]], "attr": [["#selfOrb", "data-template", "pair-chrome"], ["#orbField a.worldOrb[data-origin=\"https://amazonprimea.com\"]", "data-template", "bead-wash"], ["#orbField a.worldOrb[data-origin=\"https://demo.cloudbongos.com\"]", "data-shape", "pair"], ["#orbField a.worldOrb[data-origin=\"https://demo.cloudbongos.com\"]", "data-material", "
|
|
12
|
+
"in-map": { "auth": true, "expect": { "expanded": false, "count": [["#orbField .worldOrb[data-shape][data-style]", 7]], "attr": [["#selfOrb", "data-template", "pair-chrome"], ["#orbField a.worldOrb[data-origin=\"https://amazonprimea.com\"]", "data-template", "bead-wash"], ["#orbField a.worldOrb[data-origin=\"https://demo.cloudbongos.com\"]", "data-shape", "pair"], ["#orbField a.worldOrb[data-origin=\"https://demo.cloudbongos.com\"]", "data-material", "obsidian"], ["#orbField a.worldOrb[data-origin=\"https://demo.cloudbongos.com\"]", "data-template", "pair-obsidian-tint"], ["#orbField a.worldOrb[data-origin=\"https://emersonian-circles.cloudbongos.com\"]", "data-template", "bead-obsidian-plain"]] } },
|
|
13
13
|
"in-map-projects": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "open": "#menuProjects" } },
|
|
14
14
|
"in-map-profile": { "auth": true, "actions": [["click", "#who"], ["wait", 350]], "expect": { "expanded": true, "open": "#menuProfile" } },
|
|
15
15
|
"in-map-kbd": { "auth": true, "actions": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 350], ["press", "ArrowDown"], ["wait", 120]], "expect": { "expanded": true, "activeText": "Table" } },
|
|
16
16
|
"in-map-search": { "auth": true, "actions": [["clickIfVisible", "#qToggle"], ["wait", 200], ["fill", "#mapQ", "emer"], ["wait", 300]], "expect": { "value": [["#mapQ", "emer"]] } },
|
|
17
17
|
"in-map-mine-on": { "auth": true, "actions": [["check", "#onlyMine"], ["wait", 300]], "expect": { "checked": "#onlyMine" } },
|
|
18
18
|
"in-table": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 250], ["click", "a[data-sub=\"table\"]"], ["wait", 400]], "expect": { "visible": ["#sub-table-body", ".tblMast", ".mastObj", "#selfHall"], "closed": "#menuProjects", "count": [["#mapRows tr[data-generated] .rowHall", 1]] } },
|
|
19
|
-
"in-mine": { "auth": true, "url": "/projects?view=mine", "expect": { "hidden": ["#qWrap", "#empty"], "visible": ["#mineBar", "#projectsList", "#projectsList .proj .worldOrb .disc"], "count": [["#projectsList .proj", 5], ["#projectsList .proj.gone", 1], ["#projectsList .proj .worldOrb[data-shape][data-material][data-style]", 5], ["#projectsList .proj .strip", 1], ["#projectsList .proj .worldOrb.ashen", 2], ["#projectsList [data-restart]", 1]] } },
|
|
19
|
+
"in-mine": { "auth": true, "url": "/projects?view=mine", "expect": { "hidden": ["#qWrap", "#empty"], "visible": ["#mineBar", "#projectsList", "#projectsList .proj .worldOrb .disc"], "count": [["#projectsList .proj", 5], ["#projectsList .proj.gone", 1], ["#projectsList .proj .worldOrb[data-shape][data-material][data-style]", 5], ["#projectsList .proj .worldOrb[data-template=\"blob-frost-tint\"]", 1], ["#projectsList .proj .strip", 1], ["#projectsList .proj .worldOrb.ashen", 2], ["#projectsList [data-restart]", 1]] } },
|
|
20
20
|
"in-manage": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mMain", "#mChip .chip", "#mOrb .worldOrb", "#mFacts .ledger .k", "#mActs"], "hidden": ["#qWrap", "#mGate", "#mMissing", "#mErr"], "text": [["#mName", "demo"], ["#mTitle", "demo"]] } },
|
|
21
|
+
"in-manage-planet": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mPlanetPick", "#mPick .pick .pickPreview .worldOrb .disc", "#mPick [data-pick-save]", "#mPick [data-pick-reset]", "#mPickWhat summary"], "count": [["#mPick .pickCells[role=\"radiogroup\"]", 3], ["#mPick .pickCell", 10], ["#mPick .pickCell.on", 3], ["#mPick .pickSw", 5], ["#mPick .pickSw.on", 1]], "attr": [["#mPick .pickPreview .worldOrb", "data-template", "pair-obsidian-tint"], ["#mOrb .worldOrb", "data-template", "pair-obsidian-tint"]], "checked": "#mPick input[name=\"mPickAccent\"][value=\"sea\"]", "text": [["#mPlanet .v small", "your choice."]] } },
|
|
21
22
|
"in-manage-snag": { "auth": true, "url": "/projects?view=manage&id=103", "expect": { "visible": ["#mMain", "#mErr", "#mActs"], "text": [["#mName", "tidepool"]] } },
|
|
22
23
|
"in-manage-gone": { "auth": true, "url": "/projects?view=manage&id=104", "expect": { "visible": ["#mMain", "#mActs"], "hidden": ["#mErr"], "text": [["#mName", "old-lantern"]] } },
|
|
23
24
|
"in-manage-missing": { "auth": true, "url": "/projects?view=manage&id=999", "expect": { "visible": ["#mMissing"], "hidden": ["#mMain", "#mGate"] } },
|
package/src/module-api.js
CHANGED
|
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
33
33
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
34
34
|
// the entry to that file. Look for a version's history there, not here.
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
|
-
const CORE_VERSION = '1.19.
|
|
36
|
+
const CORE_VERSION = '1.19.430'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
37
37
|
|
|
38
38
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
39
39
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -187,8 +187,10 @@ test('the mine row draws the project\'s planet — the catalog\'s template when
|
|
|
187
187
|
assert.doesNotMatch(orb, /class="orb/, 'the sphere is gone');
|
|
188
188
|
const planet = (SCRIPT.match(/function planetOf\(inst\) \{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
189
189
|
assert.match(planet, /hostOf\(catalog\[i\]\.origin\) === h/, 'matched by host, the same join the map makes');
|
|
190
|
-
assert.match(planet, /tpl:
|
|
191
|
-
assert.match(planet, /tpl:
|
|
190
|
+
assert.match(planet, /var own = \{ tpl: knownTemplate\(inst\.planet_template\), acc: accentVar\(inst\.planet_accent\) \};/, 'the owner\'s own choice is read off the row first (task 1003347)');
|
|
191
|
+
assert.match(planet, /tpl: own\.tpl \|\| templateOf\(c\), acc: own\.acc \|\| accentOf\(c\)/, 'a catalog match takes the catalog\'s template + accent under the owner\'s choice');
|
|
192
|
+
assert.match(planet, /tpl: own\.tpl \|\| slugTemplate\(inst\.slug\), acc: own\.acc \|\| ORB_ACCENTS\[slugAccent\(inst\.slug\)\]/, 'owned-only walks by slug under the owner\'s choice');
|
|
193
|
+
assert.match(planet, /chosen: !!\(own\.tpl \|\| own\.acc\)/, 'the row says whose pick it is');
|
|
192
194
|
assert.match(SCRIPT, /assignAccents\(\); applyMapFilter\(\); repaintPlanets\(\);/, 'a catalog that lands late repaints the rows');
|
|
193
195
|
assert.doesNotMatch(COSMOS, /\n\.orb\{/, 'cosmos no longer carries the sphere');
|
|
194
196
|
});
|
|
@@ -1531,8 +1533,9 @@ test('the planet templates: shape × material × style; the walk changes all thr
|
|
|
1531
1533
|
test('the walk assigns the template by canonical position beside the accent; a curated template wins; the platform is the pair in chrome', () => {
|
|
1532
1534
|
assert.match(SCRIPT, /function assignAccents[\s\S]*?p\._acc = ORB_ACCENTS\[i % ORB_ACCENTS\.length\];\s*p\._tpl = ORB_WALK\[i % ORB_WALK\.length\];/,
|
|
1533
1535
|
'one walk, two assignments');
|
|
1534
|
-
assert.match(SCRIPT, /function templateOf\(p\) \{\s*return ORB_TEMPLATES\.indexOf\(p\.template\) !== -1 \? p\.template : \(p\._tpl \|\| ORB_WALK\[0\]\);/,
|
|
1535
|
-
'a curated name
|
|
1536
|
+
assert.match(SCRIPT, /function templateOf\(p\) \{\s*if \(knownTemplate\(p\.planet_template\)\) return p\.planet_template;\s*return ORB_TEMPLATES\.indexOf\(p\.template\) !== -1 \? p\.template : \(p\._tpl \|\| ORB_WALK\[0\]\);/,
|
|
1537
|
+
'the owner\'s choice wins, then a curated name; an unknown name is not a choice (task 1003347)');
|
|
1538
|
+
assert.match(SCRIPT, /function accentOf\(p\) \{ return accentVar\(p\.planet_accent\) \|\| p\._acc \|\| ORB_ACCENTS\[0\]; \}/, 'the owner\'s colour by NAME, then the walk');
|
|
1536
1539
|
assert.match(SCRIPT, /_acc: ORB_ACCENTS\[slugAccent\(inst\.slug\)\], _tpl: slugTemplate\(inst\.slug\)/, 'an owned-only entry walks by its slug');
|
|
1537
1540
|
assert.match(SCRIPT, /function slugTemplate\(slug\) \{ return ORB_WALK\[slugHash\(slug\) % ORB_WALK\.length\]; \}/, 'by the walk, not the whole set');
|
|
1538
1541
|
assert.match(SCRIPT, /var tpl = templateOf\(p\), tplParts = tpl\.split\('-'\);\s*a\.setAttribute\('data-template', tpl\);\s*a\.setAttribute\('data-shape', tplParts\[0\]\);\s*a\.setAttribute\('data-material', tplParts\.length === 3 \? tplParts\[1\] : 'chrome'\);\s*a\.setAttribute\('data-style', tplParts\[tplParts\.length - 1\]\);/,
|
|
@@ -1647,13 +1650,125 @@ test('the kit proves the field path: the fixture carries a curated template and
|
|
|
1647
1650
|
const fixture = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', 'featured.json'), 'utf8'));
|
|
1648
1651
|
const otb = fixture.projects.find((p) => p.origin === 'https://amazonprimea.com');
|
|
1649
1652
|
assert.equal(otb.template, 'bead-wash');
|
|
1650
|
-
|
|
1651
|
-
assert.
|
|
1653
|
+
const demo = fixture.projects.find((p) => p.origin === 'https://demo.cloudbongos.com');
|
|
1654
|
+
assert.equal(demo.template, 'pair-gold-wash', 'a material template rides the wire (task 1003337)');
|
|
1655
|
+
assert.equal(demo.planet_template, 'pair-obsidian-tint', 'the owner\'s choice rides the same wire (task 1003347)');
|
|
1656
|
+
assert.equal(demo.planet_accent, 'sea', 'a token NAME, never a colour');
|
|
1657
|
+
assert.ok(fixture.projects.every((p) => 'template' in p && 'planet_template' in p && 'planet_accent' in p), 'the wire never drops a key');
|
|
1652
1658
|
const states = JSON.parse(fs.readFileSync(path.join(ROOT, 'public-landing', 'projects.states.json'), 'utf8'));
|
|
1653
1659
|
const attrs = states.states['in-map'].expect.attr.map((a) => a.join(' '));
|
|
1654
1660
|
assert.ok(attrs.includes('#selfOrb data-template pair-chrome'));
|
|
1655
1661
|
assert.ok(attrs.some((a) => a.includes('amazonprimea.com') && a.endsWith('data-template bead-wash')), 'the curated template reaches the orb');
|
|
1656
|
-
assert.ok(attrs.some((a) => a.includes('demo.cloudbongos.com') && a.endsWith('data-material
|
|
1662
|
+
assert.ok(attrs.some((a) => a.includes('demo.cloudbongos.com') && a.endsWith('data-material obsidian')), 'the material reaches the orb as its own attribute');
|
|
1663
|
+
assert.ok(attrs.some((a) => a.includes('demo.cloudbongos.com') && a.endsWith('data-template pair-obsidian-tint')), 'the owner\'s choice outranks the curated template on the sky (task 1003347)');
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
/* ── the planet picker (task 1003347; direction record §16, v1 the bands): the owner's own
|
|
1667
|
+
choice, read first everywhere a planet is drawn; the "its planet" section on the manage
|
|
1668
|
+
page; a settings save that restarts nothing ── */
|
|
1669
|
+
test('the picker: the owner\'s choice is one of the sky\'s thirty-six names and one of the five accents BY NAME — the same lists the server validates (task 1003347)', () => {
|
|
1670
|
+
const req = createRequire(import.meta.url);
|
|
1671
|
+
const prov = req(path.join(ROOT, 'modules', 'provisioning', 'provisioning.js'));
|
|
1672
|
+
assert.match(SCRIPT, /var ORB_ACCENT_NAMES = \['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'\];/, 'the five by name, in the palette\'s order');
|
|
1673
|
+
assert.deepEqual([...prov.PLANET_ACCENTS], ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'], 'the server\'s allow-list is the same five');
|
|
1674
|
+
assert.match(SCRIPT, /function accentVar\(name\) \{ var i = ORB_ACCENT_NAMES\.indexOf\(name\); return i === -1 \? '' : ORB_ACCENTS\[i\]; \}/, 'a name maps to its var(); an unknown name is nothing');
|
|
1675
|
+
// the client mirror of PROJECT_TEMPLATES (pinned above) and the server's PLANET mirror agree
|
|
1676
|
+
const pi = req(path.join(ROOT, 'modules', 'platform-identity', 'platform-identity.js'));
|
|
1677
|
+
assert.deepEqual([...prov.PLANET_TEMPLATES], [...pi.PROJECT_TEMPLATES], 'provisioning validates the very names the catalog and the sky know');
|
|
1678
|
+
assert.match(SCRIPT, /function pickTemplate\(s\) \{\s*if \(s\.material === 'chrome'\) return s\.shape \+ '-' \+ \(s\.style === 'plain' \? 'chrome' : s\.style\);\s*return s\.shape \+ '-' \+ s\.material \+ '-' \+ s\.style;/, 'shape × material × style composes a stored name; the chrome material\'s plain is spelled chrome');
|
|
1679
|
+
assert.match(SCRIPT, /function pickFrom\(pl\) \{[\s\S]*?style: style === 'chrome' \? 'plain' : style,\s*accent: ORB_ACCENT_NAMES\[ORB_ACCENTS\.indexOf\(pl\.acc\)\] \|\| ORB_ACCENT_NAMES\[0\] \};/, 'and a drawn planet decomposes back into the four fields');
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
test('the picker is live markup on the manage page: the section, its body slot and a sibling fold (the .mSec shape), and the ledger row says whose pick it is', () => {
|
|
1683
|
+
assert.match(HUB, /<div class="card mSec" id="mPlanetPick">\s*<h2>Its planet<\/h2>\s*<div id="mPick">/, 'the section with its volatile slot');
|
|
1684
|
+
assert.match(HUB, /<details class="finePrint" id="mPickWhat">/, 'the fold is a SIBLING of the slot');
|
|
1685
|
+
const sec = (HUB.match(/<div class="card mSec" id="mPlanetPick">([\s\S]*?)<\/details>/) || [, ''])[1];
|
|
1686
|
+
assert.match(sec, /<b>shape<\/b>[\s\S]*<b>material<\/b>[\s\S]*<b>style<\/b>[\s\S]*<b>colour<\/b>/, 'the fold names the four choices');
|
|
1687
|
+
assert.match(sec, /no restart, nothing to apply/, 'and says the planet is the platform\'s to draw');
|
|
1688
|
+
assert.match(HUB, /id="mJoinability"[\s\S]*id="mPlanetPick"[\s\S]*id="mDelete"/, 'between how people join and the ender');
|
|
1689
|
+
const pm = (SCRIPT.match(/function paintManagePlanet\(inst\) \{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
1690
|
+
assert.match(pm, /pl\.chosen \? '<small>your choice\.<\/small>' : pl\.curated \? '' : '<small>chosen for it by the platform\.<\/small>'/, 'your choice · (curated, no clause) · chosen for it by the platform');
|
|
1691
|
+
const rm = (SCRIPT.match(/function renderManage\(inst\) \{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
1692
|
+
assert.match(rm, /renderPick\(inst\);/, 'renderManage paints the picker, fetch-free');
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
test('the picker\'s script: the preview planet and four radiogroups built from the sky\'s own names; a pick in progress survives the tick and a late catalog; the save is the settings PATCH and restarts nothing', () => {
|
|
1696
|
+
const rp = (SCRIPT.match(/function renderPick\(inst\) \{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
1697
|
+
assert.match(rp, /var pl = planetOf\(inst\);/, 'one resolution — the head, the row and the preview can never disagree');
|
|
1698
|
+
assert.match(rp, /<span class="worldOrb"' \+ planetAttrs\(preview\) \+ ' style="--acc:' \+ preview\.acc \+ ';--d:132px"/, 'the preview is the sky\'s object with the template attributes');
|
|
1699
|
+
assert.match(rp, /pickBand\('Shape', 'mPickShape', PICK_SHAPES, s\.shape\)[\s\S]*pickBand\('Material', 'mPickMaterial', PICK_MATERIALS, s\.material\)[\s\S]*pickBand\('Style', 'mPickStyle', PICK_STYLES, s\.style\)/, 'three bands of cells');
|
|
1700
|
+
assert.match(rp, /<div class="pickSwatches" role="radiogroup" aria-label="Colour">/, 'the colour as a radiogroup of swatches');
|
|
1701
|
+
assert.match(rp, /style="--sw:' \+ accentVar\(a\[0\]\) \+ '"/, 'a swatch is its token, never a literal');
|
|
1702
|
+
assert.match(rp, /pl\.chosen \? '<button type="button" class="btn small ghost" data-pick-reset>Let the platform choose again<\/button>' : ''/, 'the way back to the platform\'s pick, only once a choice is stored');
|
|
1703
|
+
assert.match(rp, /else if \(\$\('mPick'\)\.querySelector\('\.pick'\)\) return;/, 'the tick does not rebuild a pick in progress');
|
|
1704
|
+
assert.match(SCRIPT, /if \(manageInst && !mPickTouched && JSON\.stringify\(pickFrom\(planetOf\(manageInst\)\)\) !== mPickSeed\) \{ mPick = null; renderPick\(manageInst\); \}/, 'a late catalog reseeds only an untouched pick whose resolution changed');
|
|
1705
|
+
const save = (SCRIPT.match(/function doPickSave\(reset\) \{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
1706
|
+
assert.match(save, /var body = reset \? \{ planet_template: null, planet_accent: null \} : \{ planet_template: pickTemplate\(mPick\), planet_accent: mPick\.accent \};/, 'null hands the pick back; a choice sends the name and the accent by name');
|
|
1707
|
+
assert.match(save, /\/settings', \{\s*method: 'PATCH'/, 'the settings PATCH the two sections above use');
|
|
1708
|
+
assert.match(save, /manageInst\.planet_template = planet\.planet_template \|\| null;\s*manageInst\.planet_accent = planet\.planet_accent \|\| null;/, 'the row now holds the choice');
|
|
1709
|
+
assert.match(save, /repaintPlanets\(\);/, 'the head, the ledger row and the list repaint from the row');
|
|
1710
|
+
assert.match(save, /loadCatalog\(\);/, 'and the sky re-reads the feed that carries it');
|
|
1711
|
+
assert.doesNotMatch(save, /loadManage\(/, 'no re-read for an open intent: a planet change queues none');
|
|
1712
|
+
assert.match(save, /\(data\.push && data\.push\.message\) \|\| 'Saved\.'/, 'the receipt is shown verbatim');
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
test('the picker wears the world by scope: the preview left and four bands right on hairlines, cells with the dot, swatches as their tokens; under 900 the cells are the list\'s pills; no box shadow, no literal colour', () => {
|
|
1716
|
+
const block = (COSMOS.match(/\/\* ── its planet \(task 1003347([\s\S]*?)\/\* this block stays HERE/) || [, ''])[1];
|
|
1717
|
+
assert.ok(block, 'the block sits in the manage page\'s scope');
|
|
1718
|
+
assert.match(block, /#view-manage \.pick\{display:grid;grid-template-columns:168px 1fr;gap:0 34px;align-items:start;\}/, 'the preview beside the bands');
|
|
1719
|
+
assert.match(block, /#view-manage \.pickBand\{display:grid;grid-template-columns:88px 1fr;[^}]*border-top:1px solid var\(--rule\);\}/, 'each band on a hairline, its label in a column');
|
|
1720
|
+
assert.match(block, /#view-manage \.pickCells\[role="radiogroup"\]\{display:grid;grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:1fr;border:0;\}/, 'the cells outrank the generic radiogroup rule');
|
|
1721
|
+
assert.match(block, /#view-manage \.pickCell::before\{content:"";width:7px;height:7px;border-radius:50%;/, 'the dot');
|
|
1722
|
+
assert.match(block, /#view-manage \.pickCell\.on::before\{background:var\(--accent\);border-color:var\(--accent\);outline:4px solid color-mix\(in srgb,var\(--accent\) 22%,transparent\);\}/, 'the chosen cell\'s dot on the accent with its halo');
|
|
1723
|
+
assert.match(block, /#view-manage \.pickSw\{[^}]*background:var\(--sw\);/, 'a swatch paints its token');
|
|
1724
|
+
assert.match(block, /#view-manage \.pickSw\.on\{outline:1px solid var\(--ink\);outline-offset:3px;\}/, 'the chosen swatch ringed by a hairline of the ink');
|
|
1725
|
+
assert.doesNotMatch(block, /box-shadow/, 'hairlines only');
|
|
1726
|
+
assert.doesNotMatch(block, /#[0-9a-fA-F]{3,8}\b/, 'no literal colour (the Fifteen Rule)');
|
|
1727
|
+
const fold = (COSMOS.match(/@media \(max-width:900px\)\{[\s\S]*?#view-manage \.pick\{grid-template-columns:1fr;gap:22px 0;\}([\s\S]*?)\n\}/) || [, ''])[1];
|
|
1728
|
+
assert.match(fold, /#view-manage \.pickCells\[role="radiogroup"\]\{display:flex;flex-wrap:wrap;gap:8px;padding:0 0 14px;\}/, 'the cells flow as pills');
|
|
1729
|
+
assert.match(fold, /#view-manage \.pickCell\.on\{background:var\(--ink\);color:var\(--bg\);border-color:var\(--ink\);\}/, 'the chosen one inverted to ink, the filter pills\' grammar');
|
|
1730
|
+
assert.match(fold, /#view-manage \.pickCell::before\{display:none;\}/, 'no dot on a pill');
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
test('the kit proves the picker: a state renders it on the owner\'s feed-carried choice, three probes save / hand back / fold, and both fixtures carry the choice (task 1003347)', () => {
|
|
1734
|
+
const states = JSON.parse(fs.readFileSync(path.join(ROOT, 'public-landing', 'projects.states.json'), 'utf8'));
|
|
1735
|
+
const st = states.states['in-manage-planet'];
|
|
1736
|
+
assert.ok(st && st.auth && st.url === '/projects?view=manage&id=101', 'the signed-in owner on Demo');
|
|
1737
|
+
assert.ok(st.expect.visible.includes('#mPlanetPick') && st.expect.visible.includes('#mPick [data-pick-reset]'));
|
|
1738
|
+
assert.deepEqual(st.expect.count.find((c) => c[0] === '#mPick .pickCell'), ['#mPick .pickCell', 10], 'three + four + three cells');
|
|
1739
|
+
assert.deepEqual(st.expect.count.find((c) => c[0] === '#mPick .pickSw'), ['#mPick .pickSw', 5]);
|
|
1740
|
+
assert.ok(st.expect.attr.some((a) => a[0] === '#mOrb .worldOrb' && a[2] === 'pair-obsidian-tint'), 'the head wears the owner\'s choice over the curated template');
|
|
1741
|
+
assert.ok(st.expect.text.some((t) => t[0] === '#mPlanet .v small' && t[1] === 'your choice.'));
|
|
1742
|
+
const mine = states.states['in-mine'].expect.count.find((c) => c[0] === '#projectsList .proj .worldOrb[data-template="blob-frost-tint"]');
|
|
1743
|
+
assert.deepEqual(mine, ['#projectsList .proj .worldOrb[data-template="blob-frost-tint"]', 1], 'an owned-only row draws the choice off its own row');
|
|
1744
|
+
const inst = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', 'provisioning-instances.json'), 'utf8'));
|
|
1745
|
+
const harbor = inst.instances.find((i) => i.slug === 'harbor');
|
|
1746
|
+
assert.equal(harbor.planet_template, 'blob-frost-tint');
|
|
1747
|
+
assert.equal(harbor.planet_accent, 'sun-lit');
|
|
1748
|
+
const probes = JSON.parse(fs.readFileSync(path.join(ROOT, 'public-landing', 'projects.probes.json'), 'utf8'));
|
|
1749
|
+
const names = probes.probes.map((p) => p.name);
|
|
1750
|
+
assert.ok(names.some((n) => n.startsWith('manage flows: its planet (task 1003347)')), 'the save probe');
|
|
1751
|
+
assert.ok(names.some((n) => n.includes('handed back to the platform')), 'the reset probe');
|
|
1752
|
+
assert.ok(names.some((n) => n.includes('its planet at 390')), 'the fold probe');
|
|
1753
|
+
const saveProbe = probes.probes.find((p) => p.name.startsWith('manage flows: its planet (task 1003347)'));
|
|
1754
|
+
assert.ok(saveProbe.expect.text.some((t) => t[1] === 'Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it.'), 'the route\'s no-restart receipt, verbatim');
|
|
1755
|
+
const stub = fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'serve.js'), 'utf8');
|
|
1756
|
+
assert.match(stub, /const PLANETS = new Map\(\);/, 'the stub remembers a saved planet for the run');
|
|
1757
|
+
assert.match(stub, /planet: PLANETS\.get\(String\(id\)\) \|\| NO_PLANET/, 'and answers it on the settings read and the receipt');
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
test('the feed carries the owner\'s choice through a seam: platform-identity resolves provisioning.projectPlanets optionally and spreads both keys on every project (task 1003347)', () => {
|
|
1761
|
+
const route = fs.readFileSync(path.join(ROOT, 'modules', 'platform-identity', 'routes', 'projects.js'), 'utf8');
|
|
1762
|
+
assert.match(route, /api\.resolveOptional\('provisioning\.projectPlanets', async \(\) => \(\{\}\)\)\(projects\.map\(\(p\) => hostOfOrigin\(p\.origin\)\)\)/, 'optional — an instance without provisioning has no choices to carry');
|
|
1763
|
+
assert.match(route, /return \{ planet_template: c\.planet_template \|\| null, planet_accent: c\.planet_accent \|\| null \};/, 'the wire always carries both keys');
|
|
1764
|
+
assert.match(route, /\.\.\.planetOf\(p\),/, 'spread beside the stats');
|
|
1765
|
+
assert.doesNotMatch(route, /require\(['"][^'"]*provisioning/, 'never an import across modules');
|
|
1766
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'provisioning', 'module.json'), 'utf8'));
|
|
1767
|
+
assert.ok(manifest.provides.includes('provisioning.projectPlanets'), 'the port is declared');
|
|
1768
|
+
const pi = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'platform-identity', 'module.json'), 'utf8'));
|
|
1769
|
+
assert.ok(!(pi.consumes || []).includes('provisioning.projectPlanets'), 'not a required capability — the hub must boot without provisioning');
|
|
1770
|
+
const prov = fs.readFileSync(path.join(ROOT, 'modules', 'provisioning', 'routes', 'provisioning.js'), 'utf8');
|
|
1771
|
+
assert.match(prov, /api\.registerProvider\('provisioning\.projectPlanets', \(hosts\) => provisioning\.planetChoicesForHosts\(pool, hosts\)\);/, 'registered beside instanceState');
|
|
1657
1772
|
});
|
|
1658
1773
|
|
|
1659
1774
|
/* ── the table's columns on FIXED tracks (task 1003348; the owner, 2026-08-29: "the
|
|
@@ -44,7 +44,8 @@ provisioning.recordEvent = async (_db, e) => { events.push(e); };
|
|
|
44
44
|
provisioning.getOpenIntent = async () => null;
|
|
45
45
|
// The live push (task 1003140) — stubbed so the reachable path is deterministic here;
|
|
46
46
|
// its own outcomes are covered in tests/provisioning_settings_apply.mjs.
|
|
47
|
-
|
|
47
|
+
let enqueueCalls = 0; // the live push — a planet change must never queue one (task 1003347)
|
|
48
|
+
provisioning.enqueueSettingsApply = async () => { enqueueCalls++; return { intent: { id: 9, action: 'settings-apply', state: 'pending' }, created: true }; };
|
|
48
49
|
let manifest = null; // the latest manifest attempt the read surfaces (task 1003235)
|
|
49
50
|
provisioning.getLatestOAuthManifestForInstance = async () => manifest;
|
|
50
51
|
|
|
@@ -100,6 +101,7 @@ function reset({ inst, as } = {}) {
|
|
|
100
101
|
builder = as || { id: 7, github_login: 'owner', rank: 'thetes' };
|
|
101
102
|
updateCalls = [];
|
|
102
103
|
events = [];
|
|
104
|
+
enqueueCalls = 0;
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
console.log('GET /provisioning/instances/:id (the manage page\'s read — task 1003041):');
|
|
@@ -163,7 +165,20 @@ await ta('owner reads DEFAULTS (public / apply) on a row with empty settings —
|
|
|
163
165
|
const r = await get('/provisioning/instances/18/settings');
|
|
164
166
|
assert.equal(r.status, 200, r.raw);
|
|
165
167
|
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply' });
|
|
166
|
-
|
|
168
|
+
// the planet rides beside the settings (task 1003347): no default — null = the platform's own pick
|
|
169
|
+
assert.deepEqual(r.body.planet, { planet_template: null, planet_accent: null });
|
|
170
|
+
assert.deepEqual(r.body.options, {
|
|
171
|
+
platform_visibility: ['public', 'gated'], joinability: ['open', 'apply', 'invite_only'],
|
|
172
|
+
planet_template: [...provisioning.PLANET_TEMPLATES], planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await ta('a stored planet reads back; a name that left the vocabulary reads null (task 1003347)', async () => {
|
|
177
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', settings: { planet_template: 'bead-obsidian-tint', planet_accent: 'purple' } } });
|
|
178
|
+
const r = await get('/provisioning/instances/18/settings');
|
|
179
|
+
assert.equal(r.status, 200, r.raw);
|
|
180
|
+
assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: null });
|
|
181
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply' }, 'the planet is not a setting: the policy read is untouched');
|
|
167
182
|
});
|
|
168
183
|
|
|
169
184
|
await ta('stored keys overlay the defaults', async () => {
|
|
@@ -247,6 +262,70 @@ await ta('an unknown key → 400 (strict: a typo fails loudly, never silently dr
|
|
|
247
262
|
assert.equal(updateCalls.length, 0);
|
|
248
263
|
});
|
|
249
264
|
|
|
265
|
+
console.log('\nPATCH …/settings — the planet (task 1003347):');
|
|
266
|
+
|
|
267
|
+
await ta('the owner picks a planet: saved, echoed as `planet`, audited — and NO push, NO restart (the platform draws it)', async () => {
|
|
268
|
+
reset();
|
|
269
|
+
const r = await patch('/provisioning/instances/18/settings', { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' });
|
|
270
|
+
assert.equal(r.status, 200, r.raw);
|
|
271
|
+
assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' });
|
|
272
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply' }, 'the policy half is untouched');
|
|
273
|
+
assert.deepEqual(updateCalls, [{ id: 18, patch: { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' } }]);
|
|
274
|
+
assert.equal(r.body.push.queued, false);
|
|
275
|
+
assert.equal(r.body.push.restart, false, 'an active co-tenant would get a push for a POLICY change — never for the planet');
|
|
276
|
+
assert.match(r.body.push.message, /nothing on your project restarts/);
|
|
277
|
+
assert.equal(enqueueCalls, 0, 'no settings-apply intent for a planet change');
|
|
278
|
+
assert.equal(events.length, 1);
|
|
279
|
+
assert.match(events[0].detail, /planet_template=bead-obsidian-tint, planet_accent=sea/);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await ta('null hands the pick back to the platform (stored as null, read back as null)', async () => {
|
|
283
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', hosting_shape: 'co-tenant', settings: { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' } } });
|
|
284
|
+
const r = await patch('/provisioning/instances/18/settings', { planet_template: null, planet_accent: null });
|
|
285
|
+
assert.equal(r.status, 200, r.raw);
|
|
286
|
+
assert.deepEqual(r.body.planet, { planet_template: null, planet_accent: null });
|
|
287
|
+
assert.deepEqual(updateCalls, [{ id: 18, patch: { planet_template: null, planet_accent: null } }]);
|
|
288
|
+
assert.equal(enqueueCalls, 0);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await ta('the same planet re-sent → "No change", nothing written', async () => {
|
|
292
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', hosting_shape: 'co-tenant', settings: { planet_template: 'bead-obsidian-tint' } } });
|
|
293
|
+
const r = await patch('/provisioning/instances/18/settings', { planet_template: 'bead-obsidian-tint' });
|
|
294
|
+
assert.equal(r.status, 200, r.raw);
|
|
295
|
+
assert.match(r.body.push.message, /^No change/);
|
|
296
|
+
assert.equal(updateCalls.length, 0);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await ta('a planet re-sent over a retired push\'s error_note does NOT re-kick the push (only a policy key does)', async () => {
|
|
300
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', hosting_shape: 'co-tenant', error_note: 'settings-apply failed', settings: { planet_template: 'bead-obsidian-tint' } } });
|
|
301
|
+
const r = await patch('/provisioning/instances/18/settings', { planet_template: 'bead-obsidian-tint' });
|
|
302
|
+
assert.equal(r.status, 200, r.raw);
|
|
303
|
+
assert.equal(enqueueCalls, 0, 'the planet never restarts anything');
|
|
304
|
+
const r2 = await patch('/provisioning/instances/18/settings', { joinability: 'apply' });
|
|
305
|
+
assert.equal(r2.status, 200, r2.raw);
|
|
306
|
+
assert.equal(enqueueCalls, 1, 'the policy re-kick (the domain route\'s F19 shape) still works');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await ta('a policy change and a planet change in one save: the push queues for the policy, the planet rides along', async () => {
|
|
310
|
+
reset();
|
|
311
|
+
const r = await patch('/provisioning/instances/18/settings', { joinability: 'open', planet_accent: 'gold-soft' });
|
|
312
|
+
assert.equal(r.status, 200, r.raw);
|
|
313
|
+
assert.equal(r.body.push.queued, true);
|
|
314
|
+
assert.equal(r.body.planet.planet_accent, 'gold-soft');
|
|
315
|
+
assert.equal(r.body.saved.joinability, 'open');
|
|
316
|
+
assert.equal(enqueueCalls, 1);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
await ta('an off-vocabulary planet → 400 validation_failed (a colour is a token NAME, never a hex)', async () => {
|
|
320
|
+
reset();
|
|
321
|
+
for (const body of [{ planet_accent: '#fb9569' }, { planet_accent: 'purple' }, { planet_template: 'cube-gold-wash' }]) {
|
|
322
|
+
const r = await patch('/provisioning/instances/18/settings', body);
|
|
323
|
+
assert.equal(r.status, 400, r.raw);
|
|
324
|
+
assert.equal(r.body.error.code, 'validation_failed');
|
|
325
|
+
}
|
|
326
|
+
assert.equal(updateCalls.length, 0);
|
|
327
|
+
});
|
|
328
|
+
|
|
250
329
|
await ta('an empty body → 400 bad_settings with a plain sentence naming the keys', async () => {
|
|
251
330
|
reset();
|
|
252
331
|
const r = await patch('/provisioning/instances/18/settings', {});
|
|
@@ -71,6 +71,12 @@ t('the prefix is the pack\'s, not a guess — a wrong prefix silently never fire
|
|
|
71
71
|
assert.ok(!('CLOUDBONGOS_PLATFORM_VISIBILITY' in v));
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
t('the planet never rides the env transport (task 1003347): it is the platform\'s to draw, not the instance\'s to run', () => {
|
|
75
|
+
const vars = provisioning.settingsEnvVars({ settings: { platform_visibility: 'gated', planet_template: 'bead-obsidian-tint', planet_accent: 'sea' } }, 'CB');
|
|
76
|
+
for (const k of Object.keys(vars)) assert.doesNotMatch(k, /PLANET/, k);
|
|
77
|
+
assert.doesNotMatch(provisioning.settingsEnvLines({ settings: { planet_template: 'bead-obsidian-tint' } }, 'CB'), /planet|PLANET/);
|
|
78
|
+
});
|
|
79
|
+
|
|
74
80
|
t('every emitted key is in SETTINGS_VOCAB — the two lists cannot drift', () => {
|
|
75
81
|
const keys = linesOf(provisioning.settingsEnvLines({}, 'X')).map((l) => l.split('=')[0].replace(/^X_/, '').toLowerCase());
|
|
76
82
|
assert.deepEqual(keys.sort(), Object.keys(provisioning.SETTINGS_VOCAB).sort(),
|