@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
|
@@ -124,6 +124,26 @@ const SETTINGS_VOCAB = Object.freeze({
|
|
|
124
124
|
});
|
|
125
125
|
const DEFAULT_SETTINGS = Object.freeze({ platform_visibility: 'public', joinability: 'apply' });
|
|
126
126
|
|
|
127
|
+
// The project's PLANET (task 1003347, direction record §16): how the platform
|
|
128
|
+
// DRAWS it on the sky, in the owner's list and on the card — its template
|
|
129
|
+
// (shape × material × style, the thirty-six names platform-identity's
|
|
130
|
+
// PROJECT_TEMPLATES pins; mirrored here so provisioning never imports the hub's
|
|
131
|
+
// module, the PROJECT_TYPES precedent) and its accent (one of the world's five
|
|
132
|
+
// TOKEN NAMES — a name, never a colour: the Fifteen Rule). Stored in the same
|
|
133
|
+
// jsonb `settings` column, but deliberately NOT in SETTINGS_VOCAB: a setting is a
|
|
134
|
+
// policy the instance obeys (it rides web.env and a restart applies it); the
|
|
135
|
+
// planet is the platform's to draw, so it has no default (null = the platform's
|
|
136
|
+
// own pick: a curated template, else the walk), never rides the env transport,
|
|
137
|
+
// and never restarts anything.
|
|
138
|
+
const PLANET_SHAPES = ['blob', 'pair', 'bead'];
|
|
139
|
+
const PLANET_MATERIALS = ['frost', 'gold', 'obsidian'];
|
|
140
|
+
const PLANET_TEMPLATES = Object.freeze([
|
|
141
|
+
...PLANET_SHAPES.flatMap((s) => ['tint', 'wash', 'chrome'].map((y) => `${s}-${y}`)),
|
|
142
|
+
...PLANET_SHAPES.flatMap((s) => PLANET_MATERIALS.flatMap((m) => ['tint', 'wash', 'plain'].map((y) => `${s}-${m}-${y}`))),
|
|
143
|
+
]);
|
|
144
|
+
const PLANET_ACCENTS = Object.freeze(['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent']);
|
|
145
|
+
const PLANET_VOCAB = Object.freeze({ planet_template: PLANET_TEMPLATES, planet_accent: PLANET_ACCENTS });
|
|
146
|
+
|
|
127
147
|
// The optional project-detail interview (BV1.R15, task 1002334) — the "tell us
|
|
128
148
|
// about your project" step every creation form offers and every owner may skip.
|
|
129
149
|
// The interview asks two things, and the vocabulary is the contract for the
|
|
@@ -285,6 +305,10 @@ function publicInstance(row) {
|
|
|
285
305
|
// instances so a declaration shows before the catalog carries it. Nullable:
|
|
286
306
|
// rows predating provisioning_007, or a declined answer, project null.
|
|
287
307
|
type: row.type || null,
|
|
308
|
+
// The owner's planet (task 1003347): how the platform draws this project —
|
|
309
|
+
// null = the platform's own pick. Public by nature (the sky shows it), so
|
|
310
|
+
// the list and the manage page read it off the row, no second read.
|
|
311
|
+
...planetChoice(row),
|
|
288
312
|
// The optional project-detail interview (task 1002334): what the owner said
|
|
289
313
|
// they are building, and the answers that feed R17's recommendations. Both
|
|
290
314
|
// are legitimately empty — the step is skippable by design — and `description`
|
|
@@ -366,6 +390,18 @@ function effectiveSettings(row) {
|
|
|
366
390
|
return out;
|
|
367
391
|
}
|
|
368
392
|
|
|
393
|
+
// The owner's planet choice as READ (task 1003347): each key its stored
|
|
394
|
+
// on-vocabulary value or null — unset, or a name that has since left the
|
|
395
|
+
// vocabulary, which never leaks. PURE; the shape is constant so a wire never
|
|
396
|
+
// drops a key.
|
|
397
|
+
function planetChoice(row) {
|
|
398
|
+
const stored = row && row.settings && typeof row.settings === 'object' && !Array.isArray(row.settings)
|
|
399
|
+
? row.settings : {};
|
|
400
|
+
const out = {};
|
|
401
|
+
for (const [key, vocab] of Object.entries(PLANET_VOCAB)) out[key] = vocab.includes(stored[key]) ? stored[key] : null;
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
|
|
369
405
|
// A description as it may be STORED: trimmed, collapsed of runaway whitespace,
|
|
370
406
|
// capped, and empty-means-absent. Returns null for anything that isn't real prose,
|
|
371
407
|
// because null is what leaves the project dark-matter — an empty string stored in a
|
|
@@ -1033,6 +1069,30 @@ async function instanceStateForSlug(db, slug) {
|
|
|
1033
1069
|
};
|
|
1034
1070
|
}
|
|
1035
1071
|
|
|
1072
|
+
// The `provisioning.projectPlanets` port (task 1003347): the owners' planet
|
|
1073
|
+
// choices for a set of HOSTS (the catalog's origins, host-matched — the join the
|
|
1074
|
+
// hub's sky makes), so platform-identity's public featured feed can carry them
|
|
1075
|
+
// without importing this module. The newest row per host wins; a host with no
|
|
1076
|
+
// row, or no choice, is absent. Read-only.
|
|
1077
|
+
async function planetChoicesForHosts(db, hosts) {
|
|
1078
|
+
const wanted = [...new Set((hosts || []).map((h) => String(h || '').toLowerCase()).filter(Boolean))];
|
|
1079
|
+
if (!wanted.length) return {};
|
|
1080
|
+
const { rows } = await db.query(
|
|
1081
|
+
`SELECT lower(domain) AS host, settings
|
|
1082
|
+
FROM provisioning_instances
|
|
1083
|
+
WHERE domain IS NOT NULL AND lower(domain) = ANY($1)
|
|
1084
|
+
ORDER BY updated_at DESC`,
|
|
1085
|
+
[wanted],
|
|
1086
|
+
);
|
|
1087
|
+
const out = {};
|
|
1088
|
+
for (const r of rows) {
|
|
1089
|
+
if (out[r.host]) continue;
|
|
1090
|
+
const choice = planetChoice(r);
|
|
1091
|
+
if (choice.planet_template || choice.planet_accent) out[r.host] = choice;
|
|
1092
|
+
}
|
|
1093
|
+
return out;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1036
1096
|
// Fleet cost ledger (Archon): per-instance monthly estimate + a total, over the
|
|
1037
1097
|
// instances that still cost something (not torn_down). VISIBILITY only (ADR 0111 §6).
|
|
1038
1098
|
async function costLedger(db, { limit = 500 } = {}) {
|
|
@@ -1066,9 +1126,10 @@ module.exports = {
|
|
|
1066
1126
|
RESTART_FROM, RESTART_SHAPES,
|
|
1067
1127
|
TIER_MONTHLY_USD, CERT_ELIGIBLE_STATES,
|
|
1068
1128
|
SETTINGS_VOCAB, DEFAULT_SETTINGS, PROJECT_DETAIL_VOCAB, DESCRIPTION_MAX,
|
|
1129
|
+
PLANET_TEMPLATES, PLANET_ACCENTS, PLANET_VOCAB,
|
|
1069
1130
|
// pure
|
|
1070
1131
|
isValidSlug, costEstimateUsd, publicInstance, normalizeHostname,
|
|
1071
|
-
defaultDomainForSlug, isReservedHostname, effectiveSettings,
|
|
1132
|
+
defaultDomainForSlug, isReservedHostname, effectiveSettings, planetChoice,
|
|
1072
1133
|
normalizeDescription, normalizeProjectDetail, projectDetail,
|
|
1073
1134
|
// pure — GitHub App Manifest flow (task 2080)
|
|
1074
1135
|
githubAppName, buildGithubAppManifest, manifestPostUrl, publicGithubApp,
|
|
@@ -1080,7 +1141,7 @@ module.exports = {
|
|
|
1080
1141
|
createInstance, setInstanceStatus, updateInstanceDomain, updateInstanceSettings, updateInstanceDetail,
|
|
1081
1142
|
createInstance, setInstanceStatus, updateInstanceDomain, clearReleasedDomains, updateInstanceSettings,
|
|
1082
1143
|
enqueueIntent, enqueueTeardown, enqueueSettingsApply, enqueueRestart, getOpenIntent, claimNextIntent, resolveIntent,
|
|
1083
|
-
recordEvent, instanceStateForSlug, costLedger,
|
|
1144
|
+
recordEvent, instanceStateForSlug, planetChoicesForHosts, costLedger,
|
|
1084
1145
|
// data access — GitHub App Manifest attempts (task 2080)
|
|
1085
1146
|
createOAuthManifest, getOAuthManifestByState, attachManifestCode,
|
|
1086
1147
|
getLatestOAuthManifestForInstance, claimNextOAuthManifest, resolveOAuthManifest,
|
|
@@ -253,9 +253,13 @@ function callbackPage({ ok, title, body, state, domain }) {
|
|
|
253
253
|
// • PROVIDES port `provisioning.instanceState` (slug) → { status, … } | null.
|
|
254
254
|
// Lets other code read an instance's provisioning status via
|
|
255
255
|
// resolveOptional without importing this module (box.hasEverConnected pattern).
|
|
256
|
+
// • PROVIDES port `provisioning.projectPlanets` (hosts[]) → { host: { planet_template,
|
|
257
|
+
// planet_accent } } — the owners' planet choices (task 1003347), so the hub's
|
|
258
|
+
// public featured feed can carry them without importing this module.
|
|
256
259
|
function registerProvisioningSeams() {
|
|
257
260
|
if (api.hasProvider('provisioning.instanceState')) return;
|
|
258
261
|
api.registerProvider('provisioning.instanceState', (slug) => provisioning.instanceStateForSlug(pool, slug));
|
|
262
|
+
api.registerProvider('provisioning.projectPlanets', (hosts) => provisioning.planetChoicesForHosts(pool, hosts));
|
|
259
263
|
}
|
|
260
264
|
|
|
261
265
|
// Project an instance row for the fleet view — the owner identity + the same
|
|
@@ -764,7 +768,10 @@ module.exports = function provisioningRoutes() {
|
|
|
764
768
|
try {
|
|
765
769
|
const inst = await provisioning.getInstanceById(pool, id);
|
|
766
770
|
if (instanceAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
|
|
767
|
-
|
|
771
|
+
// `planet` rides beside `settings` (task 1003347): the same row, read by a
|
|
772
|
+
// different rule — no defaults, null = the platform's own pick.
|
|
773
|
+
res.json({ ok: true, settings: provisioning.effectiveSettings(inst), planet: provisioning.planetChoice(inst),
|
|
774
|
+
options: { ...provisioning.SETTINGS_VOCAB, ...provisioning.PLANET_VOCAB } });
|
|
768
775
|
} catch (err) {
|
|
769
776
|
log.error('[provisioning] GET /provisioning/instances/:id/settings', err);
|
|
770
777
|
res.fail('settings_read_failed', { status: 500, message: 'internal error' });
|
|
@@ -792,14 +799,18 @@ module.exports = function provisioningRoutes() {
|
|
|
792
799
|
if (validateOrRespond(req, res, {
|
|
793
800
|
platform_visibility: { type: 'string', enum: provisioning.SETTINGS_VOCAB.platform_visibility },
|
|
794
801
|
joinability: { type: 'string', enum: provisioning.SETTINGS_VOCAB.joinability },
|
|
802
|
+
// the planet (task 1003347): nullable on purpose — null hands the pick back to the platform
|
|
803
|
+
planet_template: { type: 'string', enum: provisioning.PLANET_VOCAB.planet_template },
|
|
804
|
+
planet_accent: { type: 'string', enum: provisioning.PLANET_VOCAB.planet_accent },
|
|
795
805
|
})) return;
|
|
806
|
+
const settingKeys = [...Object.keys(provisioning.SETTINGS_VOCAB), ...Object.keys(provisioning.PLANET_VOCAB)];
|
|
796
807
|
const patch = {};
|
|
797
|
-
for (const key of
|
|
808
|
+
for (const key of settingKeys) {
|
|
798
809
|
if ((req.body || {})[key] !== undefined) patch[key] = req.body[key];
|
|
799
810
|
}
|
|
800
811
|
if (!Object.keys(patch).length) {
|
|
801
812
|
return res.fail('bad_settings', { status: 400,
|
|
802
|
-
message: `The body carries no settings to change — send any of: ${
|
|
813
|
+
message: `The body carries no settings to change — send any of: ${settingKeys.join(', ')}.` });
|
|
803
814
|
}
|
|
804
815
|
try {
|
|
805
816
|
const inst = await provisioning.getInstanceById(pool, id);
|
|
@@ -811,8 +822,17 @@ module.exports = function provisioningRoutes() {
|
|
|
811
822
|
// whole-form save or a retried request must not bounce a live project. The
|
|
812
823
|
// one exception is the owner re-kick (the domain route's F19 shape): the same
|
|
813
824
|
// values re-sent while a retired push left its error_note re-queue the push.
|
|
814
|
-
const before = provisioning.effectiveSettings(inst);
|
|
825
|
+
const before = { ...provisioning.effectiveSettings(inst), ...provisioning.planetChoice(inst) };
|
|
815
826
|
const changed = Object.entries(patch).some(([k, v]) => before[k] !== v);
|
|
827
|
+
// The planet is the platform's to draw (task 1003347): a change to it
|
|
828
|
+
// alone is applied the moment it is read back — the sky, the list and the
|
|
829
|
+
// card repaint from the row — so it queues no push and restarts nothing.
|
|
830
|
+
// Only a POLICY key (SETTINGS_VOCAB) reaches the instance, and only a
|
|
831
|
+
// policy key re-sent over a retired push's error_note re-kicks that push.
|
|
832
|
+
const policyKeys = Object.keys(provisioning.SETTINGS_VOCAB);
|
|
833
|
+
const policyTouched = policyKeys.some((k) => patch[k] !== undefined);
|
|
834
|
+
const policyChanged = policyKeys.some((k) => patch[k] !== undefined && before[k] !== patch[k]);
|
|
835
|
+
const policyResent = !!inst.error_note && policyTouched;
|
|
816
836
|
const updated = changed ? await provisioning.updateInstanceSettings(pool, id, patch) : inst;
|
|
817
837
|
if (changed) {
|
|
818
838
|
await provisioning.recordEvent(pool, {
|
|
@@ -834,8 +854,10 @@ module.exports = function provisioningRoutes() {
|
|
|
834
854
|
const reachable = provisioning.SETTINGS_APPLY_SHAPES.has(inst.hosting_shape);
|
|
835
855
|
const receipt = (fields) => ({ reachable, ...fields });
|
|
836
856
|
let push;
|
|
837
|
-
if (!changed && !
|
|
857
|
+
if (!changed && !policyResent) {
|
|
838
858
|
push = receipt({ queued: false, restart: false, message: 'No change — this project already has these settings.' });
|
|
859
|
+
} else if (!policyChanged && !policyResent) {
|
|
860
|
+
push = receipt({ queued: false, restart: false, message: 'Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it.' });
|
|
839
861
|
} else if (!provisioning.SETTINGS_APPLY_FROM.has(inst.status)) {
|
|
840
862
|
const offline = inst.status === 'torn_down' || inst.status === 'tearing_down';
|
|
841
863
|
push = receipt({ queued: false, restart: false, message: offline
|
|
@@ -867,7 +889,7 @@ module.exports = function provisioningRoutes() {
|
|
|
867
889
|
: 'Saved. A restart is already queued for this project and will carry this change.' });
|
|
868
890
|
}
|
|
869
891
|
}
|
|
870
|
-
res.json({ ok: true, saved: provisioning.effectiveSettings(updated), push });
|
|
892
|
+
res.json({ ok: true, saved: provisioning.effectiveSettings(updated), planet: provisioning.planetChoice(updated), push });
|
|
871
893
|
} catch (err) {
|
|
872
894
|
log.error('[provisioning] PATCH /provisioning/instances/:id/settings', err);
|
|
873
895
|
res.fail('settings_update_failed', { status: 500, message: 'internal error' });
|
|
@@ -330,6 +330,40 @@ t('projectDetail: a stored value that has since LEFT the vocabulary never leaks
|
|
|
330
330
|
assert.deepEqual(P.projectDetail(null), {});
|
|
331
331
|
});
|
|
332
332
|
|
|
333
|
+
t('the planet (task 1003347): PLANET_TEMPLATES mirrors platform-identity\'s PROJECT_TEMPLATES name for name; the accents are the world\'s five token names', () => {
|
|
334
|
+
const pi = require('../../platform-identity/platform-identity.js');
|
|
335
|
+
assert.deepEqual([...P.PLANET_TEMPLATES], [...pi.PROJECT_TEMPLATES], 'the two modules never import each other — the mirror is pinned instead (the PROJECT_TYPES precedent)');
|
|
336
|
+
assert.equal(P.PLANET_TEMPLATES.length, 36);
|
|
337
|
+
assert.deepEqual([...P.PLANET_ACCENTS], ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent']);
|
|
338
|
+
for (const a of P.PLANET_ACCENTS) assert.doesNotMatch(a, /^#|rgb|hsl/, 'a name, never a colour (the Fifteen Rule)');
|
|
339
|
+
assert.deepEqual(Object.keys(P.PLANET_VOCAB), ['planet_template', 'planet_accent']);
|
|
340
|
+
// the deliberate asymmetry: the planet is NOT a setting — no default, never in SETTINGS_VOCAB
|
|
341
|
+
for (const k of Object.keys(P.PLANET_VOCAB)) {
|
|
342
|
+
assert.ok(!(k in P.SETTINGS_VOCAB), `${k} is not a policy the instance obeys`);
|
|
343
|
+
assert.ok(!(k in P.DEFAULT_SETTINGS), `${k} has no default — null is the platform's own pick`);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
t('planetChoice: null for a bare row or an unset key; a stored on-vocabulary value reads back; junk reads null, never leaks', () => {
|
|
348
|
+
assert.deepEqual(P.planetChoice(null), { planet_template: null, planet_accent: null });
|
|
349
|
+
assert.deepEqual(P.planetChoice({}), { planet_template: null, planet_accent: null });
|
|
350
|
+
assert.deepEqual(P.planetChoice({ settings: { platform_visibility: 'gated' } }), { planet_template: null, planet_accent: null });
|
|
351
|
+
assert.deepEqual(P.planetChoice({ settings: { planet_template: 'pair-frost-tint', planet_accent: 'sun-lit' } }), { planet_template: 'pair-frost-tint', planet_accent: 'sun-lit' });
|
|
352
|
+
assert.deepEqual(P.planetChoice({ settings: { planet_template: 'cube', planet_accent: '#fff' } }), { planet_template: null, planet_accent: null });
|
|
353
|
+
assert.deepEqual(P.planetChoice({ settings: ['nope'] }), { planet_template: null, planet_accent: null });
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
t('publicInstance carries the planet — both keys, null when unset — so the list and the manage page read it off the row', () => {
|
|
357
|
+
const chosen = P.publicInstance({ id: 1, slug: 'x', status: 'active', settings: { planet_template: 'bead-gold-tint', planet_accent: 'sea' } });
|
|
358
|
+
assert.equal(chosen.planet_template, 'bead-gold-tint');
|
|
359
|
+
assert.equal(chosen.planet_accent, 'sea');
|
|
360
|
+
const unset = P.publicInstance({ id: 2, slug: 'y', status: 'active' });
|
|
361
|
+
assert.ok('planet_template' in unset && 'planet_accent' in unset, 'the keys are always present');
|
|
362
|
+
assert.equal(unset.planet_template, null);
|
|
363
|
+
assert.equal(unset.planet_accent, null);
|
|
364
|
+
assert.ok(!('settings' in unset), 'the raw jsonb never rides the projection');
|
|
365
|
+
});
|
|
366
|
+
|
|
333
367
|
t('PROJECT_DETAIL_VOCAB carries NO defaults — an unanswered question stays unanswered', () => {
|
|
334
368
|
// The deliberate asymmetry with SETTINGS_VOCAB: a setting is a policy the
|
|
335
369
|
// instance must obey, so it needs a default; a detail answer is something the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"_":"The hub's catalog (task 1003345): the landing's featured.json plus Tidewater, an all-null row nobody owns — the card-floor state opens ITS card, because Demo is owned by the signed-in stub (provisioning-instances.json) and an owned orb's plain click opens the manage view, never the card. Served under the name \"featured\" by projects.states.json's stub.fixtures alias; the landing keeps featured.json.","projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","template":"bead-wash","created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"template":"pair-gold-wash","created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"template":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null},{"origin":"https://tidewater.cloudbongos.com","name":"Tidewater","tagline":"A project that has declared nothing yet.","art_url":null,"type":null,"template":null,"created_at":"2026-07-20T08:00:00.000Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null}]}
|
|
1
|
+
{"_":"The hub's catalog (task 1003345): the landing's featured.json plus Tidewater, an all-null row nobody owns — the card-floor state opens ITS card, because Demo is owned by the signed-in stub (provisioning-instances.json) and an owned orb's plain click opens the manage view, never the card. Served under the name \"featured\" by projects.states.json's stub.fixtures alias; the landing keeps featured.json.","projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","template":"bead-wash","planet_template":null,"planet_accent":null,"created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"template":"pair-gold-wash","planet_template":"pair-obsidian-tint","planet_accent":"sea","created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null},{"origin":"https://tidewater.cloudbongos.com","name":"Tidewater","tagline":"A project that has declared nothing yet.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-20T08:00:00.000Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null}]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","template":"bead-wash","created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"template":"pair-gold-wash","created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"template":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null}]}
|
|
1
|
+
{"projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","template":"bead-wash","planet_template":null,"planet_accent":null,"created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"template":"pair-gold-wash","planet_template":"pair-obsidian-tint","planet_accent":"sea","created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null}]}
|
|
@@ -133,11 +133,33 @@ const FLOW_RECEIPTS = {
|
|
|
133
133
|
// after a receipt (doDelete → loadManage), and what it must then see is the queued intent —
|
|
134
134
|
// the same open_intent shape the route stamps — not the untouched fixture row
|
|
135
135
|
const ASKED = new Map();
|
|
136
|
+
// the planet picker (task 1003347): a settings PATCH that carries planet_* is REMEMBERED for
|
|
137
|
+
// the run — the page repaints from the row it re-reads, so the row must carry the choice; a
|
|
138
|
+
// PATCH with only planet keys gets the route's no-restart receipt, a policy key the push
|
|
139
|
+
const PLANETS = new Map();
|
|
140
|
+
const PLANET_KEYS = ['planet_template', 'planet_accent'];
|
|
141
|
+
const NO_PLANET = { planet_template: null, planet_accent: null };
|
|
142
|
+
function readBody(req, cb) {
|
|
143
|
+
let d = '';
|
|
144
|
+
req.on('data', (c) => { d += c; });
|
|
145
|
+
req.on('end', () => { let b = {}; try { b = JSON.parse(d || '{}'); } catch { b = {}; } cb(b); });
|
|
146
|
+
}
|
|
147
|
+
function settingsPatch(res, id, body) {
|
|
148
|
+
if (PLANET_KEYS.some((k) => k in body)) PLANETS.set(String(id), { planet_template: body.planet_template || null, planet_accent: body.planet_accent || null });
|
|
149
|
+
const policy = ['platform_visibility', 'joinability'].some((k) => k in body);
|
|
150
|
+
const push = policy ? FLOW_RECEIPTS.settings.push
|
|
151
|
+
: { queued: false, restart: false, message: 'Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it.' };
|
|
152
|
+
return json(res, 200, { ok: true, saved: FLOW_RECEIPTS.settings.settings, planet: PLANETS.get(String(id)) || NO_PLANET, push });
|
|
153
|
+
}
|
|
154
|
+
// the row carries its planet: a saved pick for the run, else the fixture's own (or null)
|
|
155
|
+
function withPlanet(inst) {
|
|
156
|
+
const saved = PLANETS.get(String(inst.id));
|
|
157
|
+
return { ...inst, planet_template: inst.planet_template || null, planet_accent: inst.planet_accent || null, ...(saved || {}) };
|
|
158
|
+
}
|
|
136
159
|
function flowReceipt(rest, method) {
|
|
137
160
|
if (method === 'POST' && rest === '/provisioning/instances') return FLOW_RECEIPTS.provision;
|
|
138
|
-
const m = rest.match(/^\/provisioning\/instances\/(\d+)\/(teardown|restart|domain
|
|
161
|
+
const m = rest.match(/^\/provisioning\/instances\/(\d+)\/(teardown|restart|domain)$/);
|
|
139
162
|
if (!m) return null;
|
|
140
|
-
if (m[2] === 'settings') return method === 'PATCH' ? FLOW_RECEIPTS.settings : null;
|
|
141
163
|
if (method !== 'POST') return null;
|
|
142
164
|
if (m[2] === 'teardown' || m[2] === 'restart') ASKED.set(m[1], m[2]);
|
|
143
165
|
return FLOW_RECEIPTS[m[2]];
|
|
@@ -151,11 +173,11 @@ function instances(res, id, settings) {
|
|
|
151
173
|
if (FAIL.has(name)) return json(res, 500, { error: { code: 'stub_failure', message: `FAIL=${name}` } });
|
|
152
174
|
const file = path.join(FIXTURES, (ALIAS[name] || name) + '.json');
|
|
153
175
|
if (!fs.existsSync(file)) return json(res, 404, { error: { code: 'no_fixture', message: name } });
|
|
154
|
-
const list = stamp((JSON.parse(fs.readFileSync(file, 'utf8')).instances || []).map(withAsk));
|
|
176
|
+
const list = stamp((JSON.parse(fs.readFileSync(file, 'utf8')).instances || []).map(withAsk).map(withPlanet));
|
|
155
177
|
if (id === null) return json(res, 200, { instances: list });
|
|
156
178
|
const inst = list.find((i) => String(i.id) === String(id));
|
|
157
179
|
if (!inst) return json(res, 404, { error: { code: 'not_found', message: 'no such instance' } });
|
|
158
|
-
if (settings) return json(res, 200, { settings: { platform_visibility: 'public', joinability: 'open' }, options: { platform_visibility: ['public', 'gated'], joinability: ['open', 'apply', 'invite_only'] } });
|
|
180
|
+
if (settings) return json(res, 200, { settings: { platform_visibility: 'public', joinability: 'open' }, planet: PLANETS.get(String(id)) || NO_PLANET, options: { platform_visibility: ['public', 'gated'], joinability: ['open', 'apply', 'invite_only'] } });
|
|
159
181
|
return json(res, 200, { instance: inst, open_intent: inst.open_intent || null });
|
|
160
182
|
}
|
|
161
183
|
// A project's art in the featured fixture may be root-relative (the surface's
|
|
@@ -215,6 +237,8 @@ const server = http.createServer((req, res) => {
|
|
|
215
237
|
// (ADR 0111) — the stub answers each ask with the route's receipt so the probes can drive
|
|
216
238
|
// attach / restart / teardown / revive / join end to end; nothing changes in the fixture
|
|
217
239
|
if (req.method !== 'GET') {
|
|
240
|
+
const ps = rest.match(/^\/provisioning\/instances\/(\d+)\/settings$/);
|
|
241
|
+
if (ps && req.method === 'PATCH') return AUTH ? readBody(req, (body) => settingsPatch(res, ps[1], body)) : json(res, 401, UNAUTH);
|
|
218
242
|
const flow = flowReceipt(rest, req.method);
|
|
219
243
|
if (flow) return AUTH ? json(res, 200, flow) : json(res, 401, UNAUTH);
|
|
220
244
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.430",
|
|
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.430",
|
|
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.430",
|
|
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",
|
|
@@ -1357,6 +1357,29 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
1357
1357
|
#view-manage .visOpt:has(input:checked) b::before{background:var(--accent);border-color:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
|
|
1358
1358
|
#view-manage .visOpt:focus-within{outline:2px solid var(--accent);outline-offset:3px;}
|
|
1359
1359
|
#view-manage #mVisRuns b,#view-manage #mJoinRuns b{color:var(--ok);font-weight:500;}
|
|
1360
|
+
/* ── its planet (task 1003347; direction record §16, v1 the bands): the preview
|
|
1361
|
+
planet left; four bands right — shape · material · style as hairline cells of
|
|
1362
|
+
one band, a word each, the chosen one carrying the dot; the colour as five
|
|
1363
|
+
swatches, the chosen one ringed by a hairline of the ink. The cells outrank
|
|
1364
|
+
the section's generic radiogroup rule by their class. ── */
|
|
1365
|
+
#view-manage .pick{display:grid;grid-template-columns:168px 1fr;gap:0 34px;align-items:start;}
|
|
1366
|
+
#view-manage .pickPreview{display:grid;gap:12px;justify-items:center;padding-top:6px;}
|
|
1367
|
+
#view-manage .pickWords{margin:0;font-size:14px;font-weight:400;line-height:1.45;color:var(--ink);text-align:center;max-width:168px;}
|
|
1368
|
+
#view-manage .pickWords small{display:block;font-size:12.5px;color:var(--ink-soft);}
|
|
1369
|
+
#view-manage .pickBands{display:grid;border-bottom:1px solid var(--rule);}
|
|
1370
|
+
#view-manage .pickBand{display:grid;grid-template-columns:88px 1fr;gap:0 18px;align-items:center;border-top:1px solid var(--rule);}
|
|
1371
|
+
#view-manage .pickLabel{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);padding:14px 0;}
|
|
1372
|
+
#view-manage .pickCells[role="radiogroup"]{display:grid;grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:1fr;border:0;}
|
|
1373
|
+
#view-manage .pickCell{position:relative;display:flex;align-items:center;gap:10px;min-height:48px;margin:0;padding:10px 16px;border:0;border-left:1px solid var(--rule-soft);border-radius:0;cursor:pointer;font-family:var(--font-body);font-weight:400;font-size:14.5px;color:var(--ink-soft);}
|
|
1374
|
+
#view-manage .pickCell::before{content:"";width:7px;height:7px;border-radius:50%;border:1px solid var(--rule);background:var(--bg);flex:none;}
|
|
1375
|
+
#view-manage .pickCell:hover,#view-manage .pickCell.on{color:var(--ink);}
|
|
1376
|
+
#view-manage .pickCell.on::before{background:var(--accent);border-color:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
|
|
1377
|
+
#view-manage .pickCell input,#view-manage .pickSw input,#view-manage .pickSw span{position:absolute;width:1px;height:1px;margin:0;overflow:hidden;clip-path:inset(50%);}
|
|
1378
|
+
#view-manage .pickCell:focus-within,#view-manage .pickSw:focus-within{outline:2px solid var(--accent);outline-offset:3px;}
|
|
1379
|
+
#view-manage .pickSwatches[role="radiogroup"]{display:flex;gap:14px;align-items:center;padding:12px 0 12px 16px;border:0;border-left:1px solid var(--rule-soft);}
|
|
1380
|
+
#view-manage .pickSw{position:relative;display:block;width:28px;height:28px;margin:0;padding:0;border-radius:50%;border:1px solid var(--rule);background:var(--sw);cursor:pointer;}
|
|
1381
|
+
#view-manage .pickSw.on{outline:1px solid var(--ink);outline-offset:3px;}
|
|
1382
|
+
#view-manage #mPick .actions{gap:14px;align-items:center;}
|
|
1360
1383
|
|
|
1361
1384
|
/* this block stays HERE, after the rules it narrows: a same-specificity mobile
|
|
1362
1385
|
override only wins by source order, so folding it into the hub's earlier
|
|
@@ -1382,6 +1405,20 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
1382
1405
|
#view-manage [role="radiogroup"]{grid-template-columns:1fr;}
|
|
1383
1406
|
#view-manage .visOpt{border-left:0;padding-left:0;border-top:1px solid var(--rule-soft);}
|
|
1384
1407
|
#view-manage .visOpt:first-child{border-top:0;}
|
|
1408
|
+
/* the picker stacks: the preview above; each band's label above its choices; the
|
|
1409
|
+
cells become the list's filter pills (the chosen one inverted to ink), so the
|
|
1410
|
+
four bands stay one screen */
|
|
1411
|
+
#view-manage .pick{grid-template-columns:1fr;gap:22px 0;}
|
|
1412
|
+
#view-manage .pickPreview{justify-items:start;}
|
|
1413
|
+
#view-manage .pickWords{text-align:left;max-width:none;}
|
|
1414
|
+
#view-manage .pickBand{grid-template-columns:1fr;gap:0;}
|
|
1415
|
+
#view-manage .pickLabel{padding:14px 0 6px;}
|
|
1416
|
+
#view-manage .pickCells[role="radiogroup"]{display:flex;flex-wrap:wrap;gap:8px;padding:0 0 14px;}
|
|
1417
|
+
#view-manage .pickCell{min-height:36px;padding:0 14px;border:1px solid var(--rule);border-radius:var(--radius-pill);gap:0;font-size:14px;}
|
|
1418
|
+
#view-manage .pickCell::before{display:none;}
|
|
1419
|
+
#view-manage .pickCell.on{background:var(--ink);color:var(--bg);border-color:var(--ink);}
|
|
1420
|
+
#view-manage .pickSwatches[role="radiogroup"]{padding:6px 0 14px;border-left:0;}
|
|
1421
|
+
#view-manage #mPick .actions{justify-content:flex-start;}
|
|
1385
1422
|
}
|
|
1386
1423
|
@media (prefers-reduced-motion:reduce){
|
|
1387
1424
|
*,*::before,*::after{animation:none !important;transition:none !important;}
|