@bongos/core 1.19.208 → 1.19.210
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 +23 -18
- package/docs/api/openapi.json +1 -1
- package/docs/api-reference.md +1 -1
- package/modules/provisioning/routes/provisioning.js +18 -14
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +5 -2
- package/public-landing/projects.html +206 -32
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +121 -0
- package/src/module-api.js +5 -1
- package/tests/projects_hub.mjs +99 -9
- package/tests/provisioning_settings.mjs +30 -1
- package/tests/public_landing_projects.mjs +14 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.210",
|
|
6
|
+
"core_contract": "1.19.210",
|
|
7
|
+
"source_commit": "00146d83dff6ca94febcce329b5677b8d7dcb25c",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-17T22:55:17.955Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 310,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1300,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1631,
|
|
20
|
+
"tree_sha256": "101f3cedf1222f6a1a14cc53957baf132e2586e9061e196dcff4550d43033a10",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1367,12 +1367,12 @@
|
|
|
1367
1367
|
{
|
|
1368
1368
|
"path": "docs/api-reference.md",
|
|
1369
1369
|
"mode": "0000644",
|
|
1370
|
-
"sha256": "
|
|
1370
|
+
"sha256": "7b61c830bb85b66e1d6d5dad672fd4b90166e01d07be0f5db7677fc38cc24817"
|
|
1371
1371
|
},
|
|
1372
1372
|
{
|
|
1373
1373
|
"path": "docs/api/openapi.json",
|
|
1374
1374
|
"mode": "0000644",
|
|
1375
|
-
"sha256": "
|
|
1375
|
+
"sha256": "260f4413742edbb61d7695a1912ecae67383c9787a7f6f40211c5421668da480"
|
|
1376
1376
|
},
|
|
1377
1377
|
{
|
|
1378
1378
|
"path": "docs/architecture.md",
|
|
@@ -3982,7 +3982,7 @@
|
|
|
3982
3982
|
{
|
|
3983
3983
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
3984
3984
|
"mode": "0000644",
|
|
3985
|
-
"sha256": "
|
|
3985
|
+
"sha256": "a4419bdd0693d0e1168051bd640721b615bf400b25f26478d786b9aac908ab5e"
|
|
3986
3986
|
},
|
|
3987
3987
|
{
|
|
3988
3988
|
"path": "modules/provisioning/tests/liveness.mjs",
|
|
@@ -4282,12 +4282,12 @@
|
|
|
4282
4282
|
{
|
|
4283
4283
|
"path": "package-lock.json",
|
|
4284
4284
|
"mode": "0000644",
|
|
4285
|
-
"sha256": "
|
|
4285
|
+
"sha256": "4cfce69a22e52a9c0a89cd376458a32616167c2c6ca387f9cfc376212fd630db"
|
|
4286
4286
|
},
|
|
4287
4287
|
{
|
|
4288
4288
|
"path": "package.json",
|
|
4289
4289
|
"mode": "0000644",
|
|
4290
|
-
"sha256": "
|
|
4290
|
+
"sha256": "6c5ee2445f8c94f3ccbcee5c2387ac2029e34607601ff6146c6a932dc6282d40"
|
|
4291
4291
|
},
|
|
4292
4292
|
{
|
|
4293
4293
|
"path": "public-docs/index.html",
|
|
@@ -4527,7 +4527,7 @@
|
|
|
4527
4527
|
{
|
|
4528
4528
|
"path": "public-landing/assets/cosmos.css",
|
|
4529
4529
|
"mode": "0000644",
|
|
4530
|
-
"sha256": "
|
|
4530
|
+
"sha256": "bb8a74eaadebf0adf8e59dd03cab87f9c3cbd7faf21d1d6a5d47b460c032c28b"
|
|
4531
4531
|
},
|
|
4532
4532
|
{
|
|
4533
4533
|
"path": "public-landing/assets/verro-medallion.png",
|
|
@@ -4547,7 +4547,7 @@
|
|
|
4547
4547
|
{
|
|
4548
4548
|
"path": "public-landing/projects.html",
|
|
4549
4549
|
"mode": "0000644",
|
|
4550
|
-
"sha256": "
|
|
4550
|
+
"sha256": "e19cbe7db15331509cb1f09c5e8cbcf0379bb55ddb5bc405d4ebf386186f016c"
|
|
4551
4551
|
},
|
|
4552
4552
|
{
|
|
4553
4553
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -5339,6 +5339,11 @@
|
|
|
5339
5339
|
"mode": "0000644",
|
|
5340
5340
|
"sha256": "c5393ea0633f1fd60b1f6a1e86f7f86be489b74c56d2a0e947f8b262eaeab2cb"
|
|
5341
5341
|
},
|
|
5342
|
+
{
|
|
5343
|
+
"path": "scripts/gds/seed-bongos-v1-idea-routing-tasks.js",
|
|
5344
|
+
"mode": "0000644",
|
|
5345
|
+
"sha256": "1756751e8c32d9667c82173ac7ba78f98b8416aa946c747036502c5853cbf77f"
|
|
5346
|
+
},
|
|
5342
5347
|
{
|
|
5343
5348
|
"path": "scripts/gds/seed-bongos-v1-seo-tasks.js",
|
|
5344
5349
|
"mode": "0000644",
|
|
@@ -5897,7 +5902,7 @@
|
|
|
5897
5902
|
{
|
|
5898
5903
|
"path": "src/module-api.js",
|
|
5899
5904
|
"mode": "0000644",
|
|
5900
|
-
"sha256": "
|
|
5905
|
+
"sha256": "8f9beea649114a7133ef56008878f45197bb97c29bdddf6e5c6c5d786ca30981"
|
|
5901
5906
|
},
|
|
5902
5907
|
{
|
|
5903
5908
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7517,7 +7522,7 @@
|
|
|
7517
7522
|
{
|
|
7518
7523
|
"path": "tests/projects_hub.mjs",
|
|
7519
7524
|
"mode": "0000644",
|
|
7520
|
-
"sha256": "
|
|
7525
|
+
"sha256": "3c31493c1b2a804b52f34310dc31f10b3bf67387ed9266c06fd7295aac2f0d5b"
|
|
7521
7526
|
},
|
|
7522
7527
|
{
|
|
7523
7528
|
"path": "tests/provision.mjs",
|
|
@@ -7547,7 +7552,7 @@
|
|
|
7547
7552
|
{
|
|
7548
7553
|
"path": "tests/provisioning_settings.mjs",
|
|
7549
7554
|
"mode": "0000644",
|
|
7550
|
-
"sha256": "
|
|
7555
|
+
"sha256": "a525fdcfa2fde8edd2be3198ca7213b9b6bc43a12f748a718a570b7f993972b6"
|
|
7551
7556
|
},
|
|
7552
7557
|
{
|
|
7553
7558
|
"path": "tests/provisioning_slug_available.mjs",
|
|
@@ -7567,7 +7572,7 @@
|
|
|
7567
7572
|
{
|
|
7568
7573
|
"path": "tests/public_landing_projects.mjs",
|
|
7569
7574
|
"mode": "0000644",
|
|
7570
|
-
"sha256": "
|
|
7575
|
+
"sha256": "d3e132bd85382b1a9ff4fa2d582a5fc72dabc91b2dc50828b5f622f6c112b2f1"
|
|
7571
7576
|
},
|
|
7572
7577
|
{
|
|
7573
7578
|
"path": "tests/public_mirror_export.mjs",
|
package/docs/api/openapi.json
CHANGED
|
@@ -8725,7 +8725,7 @@
|
|
|
8725
8725
|
"provisioning"
|
|
8726
8726
|
],
|
|
8727
8727
|
"summary": "GET /provisioning/instances/:id",
|
|
8728
|
-
"description": "GET /provisioning/instances/:id — read one
|
|
8728
|
+
"description": "GET /provisioning/instances/:id — read one instance (the manage page's read). rank: any authenticated builder (own resource; archon may read any — task 1003041). 404 otherwise — does not leak another owner's instance existence.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
8729
8729
|
"x-rank": "any-builder",
|
|
8730
8730
|
"x-source": "modules/provisioning/routes/provisioning.js",
|
|
8731
8731
|
"parameters": [
|
package/docs/api-reference.md
CHANGED
|
@@ -380,7 +380,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
380
380
|
| GET | `/api/bongos/provisioning/health` | `public` | — | GET /provisioning/health — liveness + the core version this module mounted against. |
|
|
381
381
|
| GET | `/api/bongos/provisioning/instances` | `any-builder` | — | GET /provisioning/instances — the caller's own instances + each open intent. |
|
|
382
382
|
| POST | `/api/bongos/provisioning/instances` | `any-builder` | `slug`, `target_ref`, `hosting_shape`, `domain`, `no_address`, `tier`, `onboard_mode`, `type` | POST /provisioning/instances — request a new instance. |
|
|
383
|
-
| GET | `/api/bongos/provisioning/instances/:id` | `any-builder` | — | GET /provisioning/instances/:id — read one
|
|
383
|
+
| GET | `/api/bongos/provisioning/instances/:id` | `any-builder` | — | GET /provisioning/instances/:id — read one instance (the manage page's read). |
|
|
384
384
|
| POST | `/api/bongos/provisioning/instances/:id/domain` | `any-builder` | `domain` | POST /provisioning/instances/:id/domain — give the caller's OWN instance its address (task 1002692, gap 5: no path existed to add a domai… |
|
|
385
385
|
| POST | `/api/bongos/provisioning/instances/:id/force-teardown` | `metic+archon` | — | POST /provisioning/instances/:id/force-teardown — Archon tears down ANY instance. |
|
|
386
386
|
| POST | `/api/bongos/provisioning/instances/:id/github-app` | `any-builder` | `account` | POST /provisioning/instances/:id/github-app — mint a GitHub-App-Manifest onboarding link for the caller's OWN instance (task 2080, ADR 01… |
|
|
@@ -569,15 +569,25 @@ module.exports = function provisioningRoutes() {
|
|
|
569
569
|
}
|
|
570
570
|
});
|
|
571
571
|
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
572
|
+
// Own-scoped with an Archon override: an owner manages their own project; an
|
|
573
|
+
// Archon may act on any (fleet support). 404 for everyone else — no existence
|
|
574
|
+
// leak. Guards the per-id read (the manage page, task 1003041) and both
|
|
575
|
+
// settings routes (task 1003040).
|
|
576
|
+
function instanceAccessDenied(inst, builder) {
|
|
577
|
+
if (!inst) return true;
|
|
578
|
+
if (builder.rank === 'archon') return false;
|
|
579
|
+
return String(inst.owner_builder_id) !== String(builder.id);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// GET /provisioning/instances/:id — read one instance (the manage page's read).
|
|
583
|
+
// rank: any authenticated builder (own resource; archon may read any — task
|
|
584
|
+
// 1003041). 404 otherwise — does not leak another owner's instance existence.
|
|
575
585
|
router.get('/provisioning/instances/:id', auth.requireBuilder, async (req, res) => {
|
|
576
586
|
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
577
587
|
if (!id) return;
|
|
578
588
|
try {
|
|
579
589
|
const inst = await provisioning.getInstanceById(pool, id);
|
|
580
|
-
if (
|
|
590
|
+
if (instanceAccessDenied(inst, req.builder)) {
|
|
581
591
|
return res.fail('instance_not_found', 404);
|
|
582
592
|
}
|
|
583
593
|
const open = await provisioning.getOpenIntent(pool, id);
|
|
@@ -594,14 +604,8 @@ module.exports = function provisioningRoutes() {
|
|
|
594
604
|
// ── Per-project settings (task 1003040, goal 1000058) ─────────────────────
|
|
595
605
|
// The foundation the manage-page wave hangs off: effective settings are the
|
|
596
606
|
// DEFAULTS (today's behavior — public surfaces, apply-to-join) overlaid with
|
|
597
|
-
// the instance's stored keys.
|
|
598
|
-
//
|
|
599
|
-
// for everyone else — no existence leak (the GET-by-id precedent).
|
|
600
|
-
function settingsAccessDenied(inst, builder) {
|
|
601
|
-
if (!inst) return true;
|
|
602
|
-
if (builder.rank === 'archon') return false;
|
|
603
|
-
return String(inst.owner_builder_id) !== String(builder.id);
|
|
604
|
-
}
|
|
607
|
+
// the instance's stored keys. Access: instanceAccessDenied above (own-scoped,
|
|
608
|
+
// Archon override, 404 otherwise — no existence leak).
|
|
605
609
|
|
|
606
610
|
// GET /provisioning/instances/:id/settings — read a project's effective
|
|
607
611
|
// settings + the allowed values (so a form can render its choices).
|
|
@@ -611,7 +615,7 @@ module.exports = function provisioningRoutes() {
|
|
|
611
615
|
if (!id) return;
|
|
612
616
|
try {
|
|
613
617
|
const inst = await provisioning.getInstanceById(pool, id);
|
|
614
|
-
if (
|
|
618
|
+
if (instanceAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
|
|
615
619
|
res.json({ ok: true, settings: provisioning.effectiveSettings(inst), options: provisioning.SETTINGS_VOCAB });
|
|
616
620
|
} catch (err) {
|
|
617
621
|
console.error('[provisioning] GET /provisioning/instances/:id/settings', err);
|
|
@@ -640,7 +644,7 @@ module.exports = function provisioningRoutes() {
|
|
|
640
644
|
}
|
|
641
645
|
try {
|
|
642
646
|
const inst = await provisioning.getInstanceById(pool, id);
|
|
643
|
-
if (
|
|
647
|
+
if (instanceAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
|
|
644
648
|
const updated = await provisioning.updateInstanceSettings(pool, id, patch);
|
|
645
649
|
await provisioning.recordEvent(pool, {
|
|
646
650
|
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'settings',
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.210",
|
|
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.210",
|
|
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.210",
|
|
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",
|
|
@@ -299,8 +299,6 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
299
299
|
card is there for — and the ghosted .orb stays the graphic cue. */
|
|
300
300
|
.proj.gone{border-color:color-mix(in srgb,var(--line) 55%,var(--bg-card));}
|
|
301
301
|
.proj.gone .projName{color:var(--ink-soft);}
|
|
302
|
-
/* click-to-manage landing glow: the sky's owned orb hands focus to this card */
|
|
303
|
-
.proj.anchored{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 30%,transparent);}
|
|
304
302
|
.projBody{flex:1;min-width:0;}
|
|
305
303
|
.projHead{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
|
|
306
304
|
.projName{font-family:var(--font-display);font-weight:700;font-size:19px;letter-spacing:-0.3px;}
|
|
@@ -319,6 +317,11 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
319
317
|
.projMsg.ok{color:var(--ok);}
|
|
320
318
|
.projMsg.err{color:var(--err);}
|
|
321
319
|
.projMsg a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
320
|
+
/* the manage view (task 1003041): its back link, meta repo link and the
|
|
321
|
+
honest empty-state links speak the same link ink as every other surface */
|
|
322
|
+
.projMeta a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
323
|
+
.backRow a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
324
|
+
.empty p a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
322
325
|
.projMsg a:hover{text-decoration:underline;}
|
|
323
326
|
.projMsg details{margin-top:8px;}
|
|
324
327
|
.projMsg summary{cursor:pointer;font-size:12.5px;font-weight:700;color:var(--ink-faint);}
|
|
@@ -43,6 +43,12 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
43
43
|
.viewWrap{padding-bottom:40px;}
|
|
44
44
|
.card{margin-bottom:26px;}
|
|
45
45
|
|
|
46
|
+
/* ── the manage view (task 1003041): container rhythm only — every component
|
|
47
|
+
it draws (card, chip, addr, empty) is cosmos vocabulary ── */
|
|
48
|
+
.backRow{margin:14px 0;font-weight:700;}
|
|
49
|
+
#view-manage .empty{text-align:left;}
|
|
50
|
+
#view-manage .empty p{margin-bottom:0;}
|
|
51
|
+
|
|
46
52
|
/* The bongo pair on the New-project view. Component-scoped custom properties,
|
|
47
53
|
never :root (C1). The drum head is SKIN — using --bg-card would turn it black
|
|
48
54
|
in dark mode — and --drum-shadow is ink in light but PLAIN BLACK in dark,
|
|
@@ -557,6 +563,74 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
557
563
|
</div>
|
|
558
564
|
</section>
|
|
559
565
|
|
|
566
|
+
<!-- ══════════════════ VIEW · MANAGE ═══════════════════════════════════════
|
|
567
|
+
One owned project's management home (task 1003041, goal-1000058 wave).
|
|
568
|
+
Deep link: ?view=manage&id=N — goToManage() routes the map's owned
|
|
569
|
+
orbs/rows and the mine cards here. The API is the wall: the per-id read
|
|
570
|
+
is own-scoped (owner, or an archon for fleet support) and answers 404
|
|
571
|
+
for anyone else, so a stranger's deep link renders the same clean
|
|
572
|
+
"no project here" as a wrong id — never a permission hint. The settings
|
|
573
|
+
sections are the homes the sibling manage-wave tasks fill in; until each
|
|
574
|
+
ships, its section says so honestly instead of faking a control. -->
|
|
575
|
+
<section id="view-manage" hidden>
|
|
576
|
+
<div class="wrap viewWrap">
|
|
577
|
+
<p class="backRow"><a href="?view=mine" data-nav-to="mine">← Your projects</a></p>
|
|
578
|
+
|
|
579
|
+
<!-- signed out: the same identity-only door the mine view uses (ADR 0143) -->
|
|
580
|
+
<section id="mGate" class="card" hidden>
|
|
581
|
+
<p class="lede">This is a project’s management page — it answers only to the person who owns it. Sign in with GitHub to open yours.</p>
|
|
582
|
+
<a class="btn" id="mSignin" href="/api/bongos/auth/web/start?return=/projects%3Fview%3Dmine">
|
|
583
|
+
<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
584
|
+
Sign in with GitHub
|
|
585
|
+
</a>
|
|
586
|
+
</section>
|
|
587
|
+
|
|
588
|
+
<!-- 404: one clean state for "not yours" and "doesn't exist" — the API
|
|
589
|
+
answers both identically on purpose, and so does this copy -->
|
|
590
|
+
<section id="mMissing" class="card" hidden>
|
|
591
|
+
<p class="lede">No project here. It may have been removed, or the link may be wrong.</p>
|
|
592
|
+
<a class="btn small ghost" href="?view=mine" data-nav-to="mine">Back to your projects</a>
|
|
593
|
+
</section>
|
|
594
|
+
|
|
595
|
+
<div class="alert" id="mAlert" role="alert"></div>
|
|
596
|
+
<div class="loading" id="mLoading">Reading the project…</div>
|
|
597
|
+
|
|
598
|
+
<section id="mMain" hidden>
|
|
599
|
+
<div class="hero" style="padding-left:0;padding-right:0">
|
|
600
|
+
<h1>Manage</h1>
|
|
601
|
+
<p class="lede">One project, up close — its address, its repository, and what the platform last <b>verified</b> about it, with a timestamp. The controls land here piece by piece; each section says what it can do today.</p>
|
|
602
|
+
</div>
|
|
603
|
+
|
|
604
|
+
<div class="card">
|
|
605
|
+
<div class="projHead"><span class="projName" id="mName">…</span><span id="mChip"></span></div>
|
|
606
|
+
<div class="projMeta" id="mMeta" hidden></div>
|
|
607
|
+
<div class="addr" id="mAddr" hidden></div>
|
|
608
|
+
<p class="finePrint" id="mCreated" hidden></p>
|
|
609
|
+
</div>
|
|
610
|
+
|
|
611
|
+
<div class="card" id="mVisibility">
|
|
612
|
+
<h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Who can see it</h2>
|
|
613
|
+
<div class="empty"><p>Choosing who can open this project’s pages — everyone, or only people signed in to it — isn’t wired up here yet. It lands in one of the next updates; until then every project keeps the behavior it already has.</p></div>
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
<div class="card" id="mJoinability">
|
|
617
|
+
<h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">How people join</h2>
|
|
618
|
+
<div class="empty"><p>Choosing how new people join — walk right in, apply first, or invite only — isn’t wired up here yet. It lands in one of the next updates.</p></div>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
<div class="card" id="mRepoVisibility">
|
|
622
|
+
<h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Repository visibility</h2>
|
|
623
|
+
<div class="empty"><p>Making the project’s code repository public or private from here isn’t wired up yet. Until it lands, GitHub’s own settings page is the place to change that.</p></div>
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
<div class="card" id="mDelete">
|
|
627
|
+
<h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Delete the project</h2>
|
|
628
|
+
<div class="empty"><p>Deleting a project from this page isn’t wired up yet. A running project can still be torn down from its card under <a href="?view=mine" data-nav-to="mine">Your projects</a> — that stops the server and keeps your repository.</p></div>
|
|
629
|
+
</div>
|
|
630
|
+
</section>
|
|
631
|
+
</div>
|
|
632
|
+
</section>
|
|
633
|
+
|
|
560
634
|
<!-- ══════════════════ VIEW · NEW PROJECT ══════════════════════════════════
|
|
561
635
|
The whole create wizard, absorbed from /create (task 1002763; direction
|
|
562
636
|
§4 — that route now 302s here). Seven panels behind the five-beat rail; the
|
|
@@ -912,12 +986,23 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
912
986
|
pointed at /projects?view=new) actually arrive somewhere.
|
|
913
987
|
══════════════════════════════════════════════════════════════════════════ */
|
|
914
988
|
|
|
915
|
-
var VIEWS = { map: 1, mine: 1, new: 1 };
|
|
989
|
+
var VIEWS = { map: 1, mine: 1, new: 1, manage: 1 };
|
|
916
990
|
var currentView = 'map';
|
|
991
|
+
/* the manage view's target project (task 1003041) — set by goToManage() and
|
|
992
|
+
the ?view=manage&id=N deep link, read by loadManage() */
|
|
993
|
+
var manageId = null;
|
|
994
|
+
var manageInst = null;
|
|
917
995
|
|
|
918
996
|
function viewFromUrl() {
|
|
919
997
|
var v;
|
|
920
998
|
try { v = new URLSearchParams(location.search).get('view'); } catch (e) { v = null; }
|
|
999
|
+
if (v === 'manage') {
|
|
1000
|
+
/* per-project: no id names no project — the list is the honest landing */
|
|
1001
|
+
var id = null;
|
|
1002
|
+
try { id = new URLSearchParams(location.search).get('id'); } catch (e) {}
|
|
1003
|
+
if (id) { manageId = String(id); return 'manage'; }
|
|
1004
|
+
return 'mine';
|
|
1005
|
+
}
|
|
921
1006
|
return VIEWS[v] ? v : 'map';
|
|
922
1007
|
}
|
|
923
1008
|
|
|
@@ -930,14 +1015,20 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
930
1015
|
if (a.dataset.nav !== 'home' && a.dataset.nav === v) a.setAttribute('aria-current', 'page');
|
|
931
1016
|
else a.removeAttribute('aria-current');
|
|
932
1017
|
});
|
|
933
|
-
|
|
1018
|
+
/* the manage view's URL names its project — the deep-link contract grew
|
|
1019
|
+
an id in task 1003041, so a manage tab is shareable and reload-safe */
|
|
1020
|
+
var idPart = (v === 'manage' && manageId) ? '&id=' + encodeURIComponent(manageId) : '';
|
|
1021
|
+
try { history.replaceState(null, '', '?view=' + v + idPart); } catch (e) {}
|
|
934
1022
|
/* the shell's sign-in door returns the visitor to the view they left, and
|
|
935
1023
|
carries the repo scope when that view is the wizard (ADR 0143: the
|
|
936
1024
|
create path asks once up front instead of a second consent at Home) */
|
|
937
|
-
$('signinTop').href = '/api/bongos/auth/web/start?return=/projects%3Fview%3D' + v +
|
|
1025
|
+
$('signinTop').href = '/api/bongos/auth/web/start?return=/projects%3Fview%3D' + v +
|
|
1026
|
+
idPart.replace('&', '%26').replace('=', '%3D') + (v === 'new' ? '&repos=1' : '');
|
|
938
1027
|
/* the mine view is the only one that polls; it re-reads on entry so a
|
|
939
1028
|
card that changed while you were on the map is not stale */
|
|
940
1029
|
if (v === 'mine' && me) refreshList();
|
|
1030
|
+
/* manage re-reads its project on entry — no timer lives here (C8) */
|
|
1031
|
+
if (v === 'manage') loadManage();
|
|
941
1032
|
/* owned chips refresh on re-entry, never on a timer — C8 keeps the map quiet */
|
|
942
1033
|
if (v === 'map' && me) loadOwnedInstances().then(applyMapFilter);
|
|
943
1034
|
/* the living sky runs only on stage (C8) — start and stop with the view;
|
|
@@ -1169,11 +1260,11 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
1169
1260
|
var href = safeHttpUrl(p.origin);
|
|
1170
1261
|
if (inst) {
|
|
1171
1262
|
/* an OWNED entry — catalog-annotated or owned-only, one code path.
|
|
1172
|
-
Its job is MANAGE (direction §5):
|
|
1173
|
-
project
|
|
1174
|
-
middle-click lands a fresh tab on the
|
|
1175
|
-
|
|
1176
|
-
a.href = '?view=
|
|
1263
|
+
Its job is MANAGE (direction §5): a plain click opens that
|
|
1264
|
+
project's manage view (task 1003041), and the href IS that view's
|
|
1265
|
+
deep link, so a middle/ctrl-click lands a fresh tab on the same
|
|
1266
|
+
honest destination. The public-site door stays on the manage page. */
|
|
1267
|
+
a.href = '?view=manage&id=' + encodeURIComponent(String(inst.id));
|
|
1177
1268
|
a.setAttribute('data-manage', String(inst.id));
|
|
1178
1269
|
} else {
|
|
1179
1270
|
/* The href STAYS, and stays honest, even though a plain click now opens
|
|
@@ -1441,16 +1532,14 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
1441
1532
|
if (p) openCard(p, relOf(p), a);
|
|
1442
1533
|
});
|
|
1443
1534
|
|
|
1444
|
-
/* ── Click-to-manage (task 1002744): an owned orb
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
var ANCHOR_GLOW_MS = 2200;
|
|
1449
|
-
var pendingAnchor = null;
|
|
1535
|
+
/* ── Click-to-manage (task 1002744 → 1003041): an owned orb, row or card
|
|
1536
|
+
opens that project's own manage view — the per-project page the
|
|
1537
|
+
manage-wave tasks fill in. It replaced the scroll-anchor-into-the-list
|
|
1538
|
+
this function used to do. ── */
|
|
1450
1539
|
function goToManage(instId) {
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1540
|
+
manageId = String(instId || '');
|
|
1541
|
+
manageInst = null;
|
|
1542
|
+
setView('manage');
|
|
1454
1543
|
}
|
|
1455
1544
|
/* Card-shell lookup by instance id. CSS.escape, not esc(): the data-inst
|
|
1456
1545
|
attribute holds the DECODED value, so an HTML-escaped id would never match
|
|
@@ -1459,18 +1548,6 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
1459
1548
|
function liFor(id) {
|
|
1460
1549
|
return document.querySelector('li[data-inst="' + CSS.escape(String(id == null ? '' : id)) + '"]');
|
|
1461
1550
|
}
|
|
1462
|
-
function tryAnchor() {
|
|
1463
|
-
if (!pendingAnchor) return;
|
|
1464
|
-
var li = liFor(pendingAnchor);
|
|
1465
|
-
if (!li) return;
|
|
1466
|
-
pendingAnchor = null;
|
|
1467
|
-
/* a search or state filter that hides the target would turn the anchor
|
|
1468
|
-
into a silent no-op — clear them the same way the visitor would */
|
|
1469
|
-
if (li.hidden) $('mineClear').click();
|
|
1470
|
-
li.scrollIntoView({ block: 'center' });
|
|
1471
|
-
li.classList.add('anchored');
|
|
1472
|
-
setTimeout(function () { li.classList.remove('anchored'); }, ANCHOR_GLOW_MS);
|
|
1473
|
-
}
|
|
1474
1551
|
|
|
1475
1552
|
$('mapRows').addEventListener('click', function (ev) {
|
|
1476
1553
|
/* the row's own "Open →" keeps its job — the card is for the rest of the row */
|
|
@@ -3630,7 +3707,9 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3630
3707
|
}
|
|
3631
3708
|
var lm = lastMsg[inst.id];
|
|
3632
3709
|
body += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '">' + (lm ? lm.text : '') + '</div>';
|
|
3633
|
-
|
|
3710
|
+
/* every owned card opens its manage view (task 1003041) — a real link, so
|
|
3711
|
+
middle/ctrl-click works; the delegated handler upgrades a plain click */
|
|
3712
|
+
var acts = ['<a class="btn small ghost" data-manage="' + iid + '" href="?view=manage&id=' + iid + '">Manage</a>'];
|
|
3634
3713
|
if (canRetry(inst)) acts.push('<button type="button" class="btn small" data-retry="' + iid + '">Try again</button>');
|
|
3635
3714
|
if (canRevive(inst)) acts.push('<button type="button" class="btn small" data-revive="' + iid + '">Bring it back</button>');
|
|
3636
3715
|
if (canTeardown(inst)) acts.push('<button type="button" class="btn small danger" data-teardown="' + iid + '">Tear it down</button>');
|
|
@@ -3663,7 +3742,6 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3663
3742
|
});
|
|
3664
3743
|
recountMine();
|
|
3665
3744
|
applyMine();
|
|
3666
|
-
tryAnchor(); /* a click-to-manage jump may be waiting on this render */
|
|
3667
3745
|
}
|
|
3668
3746
|
|
|
3669
3747
|
function msg(id, text, cls) {
|
|
@@ -3753,7 +3831,6 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3753
3831
|
var sameSet = instances.length === known && instances.every(function (i) { return byId[i.id]; });
|
|
3754
3832
|
if (!sameSet) { render(instances); return; }
|
|
3755
3833
|
instances.forEach(function (inst) { takeUpdate(inst.id, inst); });
|
|
3756
|
-
tryAnchor(); /* same-set refresh renders no shells — retry here too */
|
|
3757
3834
|
})
|
|
3758
3835
|
.catch(function () { /* transient — next tick will try again */ });
|
|
3759
3836
|
}
|
|
@@ -3767,6 +3844,8 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3767
3844
|
/* returning to a MAP tab re-reads the owned overlay once — event-driven,
|
|
3768
3845
|
like the mine view's entry refresh, not a timer */
|
|
3769
3846
|
if (!document.hidden && me && currentView === 'map') loadOwnedInstances().then(applyMapFilter);
|
|
3847
|
+
/* returning to a MANAGE tab re-reads its one project the same way */
|
|
3848
|
+
if (!document.hidden && currentView === 'manage' && manageId) loadManage(true);
|
|
3770
3849
|
});
|
|
3771
3850
|
|
|
3772
3851
|
/* C6 freshness without C8's cost: once a minute the visible MAP refreshes
|
|
@@ -3793,6 +3872,93 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3793
3872
|
}
|
|
3794
3873
|
setInterval(mapChipTick, REFRESH_MS);
|
|
3795
3874
|
|
|
3875
|
+
/* ── the manage view (task 1003041): one owned project's management home.
|
|
3876
|
+
The API is the wall — the per-id read is own-scoped with an archon
|
|
3877
|
+
override and 404s for anyone else — so this code only renders what it
|
|
3878
|
+
answers. The data re-reads on entry and tab return, never on a fetch
|
|
3879
|
+
timer (C8); the once-a-minute tick below re-renders from data already
|
|
3880
|
+
in hand so "checked Xm ago" keeps telling the truth (C6). ── */
|
|
3881
|
+
|
|
3882
|
+
function manageSignin() {
|
|
3883
|
+
return API + '/auth/web/start?return=' +
|
|
3884
|
+
encodeURIComponent('/projects?view=manage&id=' + manageId);
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
function renderManage(inst) {
|
|
3888
|
+
manageInst = inst;
|
|
3889
|
+
$('mLoading').hidden = true;
|
|
3890
|
+
$('mGate').hidden = true;
|
|
3891
|
+
$('mMissing').hidden = true;
|
|
3892
|
+
$('mMain').hidden = false;
|
|
3893
|
+
$('mName').textContent = inst.slug || '';
|
|
3894
|
+
/* the ONE chip site of this view — chipFor() or nothing, so the manage
|
|
3895
|
+
page can never tell a different liveness story than the card (C6) */
|
|
3896
|
+
$('mChip').innerHTML = chipFor(inst);
|
|
3897
|
+
var meta = [];
|
|
3898
|
+
if (inst.target_ref) {
|
|
3899
|
+
meta.push('repo <a href="https://github.com/' + esc(inst.target_ref) +
|
|
3900
|
+
'" target="_blank" rel="noopener"><code>' + esc(inst.target_ref) + '</code></a>');
|
|
3901
|
+
}
|
|
3902
|
+
if (inst.hosting_shape) meta.push(esc(SHAPE_LABEL[inst.hosting_shape] || inst.hosting_shape));
|
|
3903
|
+
if (inst.onboard_mode === 'adopt') meta.push('adopted repo');
|
|
3904
|
+
$('mMeta').innerHTML = meta.join('<span aria-hidden="true">·</span>');
|
|
3905
|
+
$('mMeta').hidden = !meta.length;
|
|
3906
|
+
var addr = addressRow(inst);
|
|
3907
|
+
$('mAddr').innerHTML = addr;
|
|
3908
|
+
$('mAddr').hidden = !addr;
|
|
3909
|
+
/* a fact with no answer hides — never a dash (the card rule) */
|
|
3910
|
+
$('mCreated').textContent = inst.created_at ? 'Created ' + listedDate(inst.created_at) : '';
|
|
3911
|
+
$('mCreated').hidden = !inst.created_at;
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
function loadManage(quiet) {
|
|
3915
|
+
if (!manageId) return;
|
|
3916
|
+
$('mAlert').classList.remove('on');
|
|
3917
|
+
$('mSignin').href = manageSignin();
|
|
3918
|
+
if (!quiet) {
|
|
3919
|
+
$('mMain').hidden = true;
|
|
3920
|
+
$('mGate').hidden = true;
|
|
3921
|
+
$('mMissing').hidden = true;
|
|
3922
|
+
$('mLoading').hidden = false;
|
|
3923
|
+
}
|
|
3924
|
+
var asked = manageId; /* a stale answer must never paint a newer project */
|
|
3925
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(manageId), { credentials: 'same-origin' })
|
|
3926
|
+
.then(function (res) {
|
|
3927
|
+
if (asked !== manageId) throw new Error('stale');
|
|
3928
|
+
if (res.status === 401) {
|
|
3929
|
+
$('mLoading').hidden = true; $('mMain').hidden = true; $('mGate').hidden = false;
|
|
3930
|
+
throw new Error('handled');
|
|
3931
|
+
}
|
|
3932
|
+
/* 404 is ONE clean answer for "not yours" and "doesn't exist" — the
|
|
3933
|
+
API refuses both identically, so this page cannot leak existence */
|
|
3934
|
+
if (res.status === 404) {
|
|
3935
|
+
$('mLoading').hidden = true; $('mMain').hidden = true; $('mMissing').hidden = false;
|
|
3936
|
+
throw new Error('handled');
|
|
3937
|
+
}
|
|
3938
|
+
if (!res.ok) throw new Error('soft');
|
|
3939
|
+
return res.json();
|
|
3940
|
+
})
|
|
3941
|
+
.then(function (data) {
|
|
3942
|
+
var inst = data.instance || {};
|
|
3943
|
+
inst.open_intent = data.open_intent || null;
|
|
3944
|
+
renderManage(inst);
|
|
3945
|
+
})
|
|
3946
|
+
.catch(function (err) {
|
|
3947
|
+
var m = String(err && err.message);
|
|
3948
|
+
if (m === 'handled' || m === 'stale') return;
|
|
3949
|
+
$('mLoading').hidden = true;
|
|
3950
|
+
var a = $('mAlert');
|
|
3951
|
+
a.textContent = 'Couldn’t read this project right now — refresh in a moment.';
|
|
3952
|
+
a.classList.add('on');
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
function manageChipTick() {
|
|
3957
|
+
if (document.hidden || currentView !== 'manage' || !manageInst) return;
|
|
3958
|
+
renderManage(manageInst); /* zero requests — re-dates "checked Xm ago" */
|
|
3959
|
+
}
|
|
3960
|
+
setInterval(manageChipTick, REFRESH_MS);
|
|
3961
|
+
|
|
3796
3962
|
/* ── teardown: ask, confirm (the truth, itemized — direction §5.4), then
|
|
3797
3963
|
SAY exactly what the server said ── */
|
|
3798
3964
|
|
|
@@ -3923,6 +4089,14 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3923
4089
|
|
|
3924
4090
|
/* one delegated listener for all card actions */
|
|
3925
4091
|
$('projectsList').addEventListener('click', function (ev) {
|
|
4092
|
+
/* the Manage link is a real anchor — a modified click keeps its browser
|
|
4093
|
+
meaning, a plain one stays in-page (the orb-click contract) */
|
|
4094
|
+
var m = ev.target.closest && ev.target.closest('a[data-manage]');
|
|
4095
|
+
if (m) {
|
|
4096
|
+
if (!isPlainClick(ev)) return;
|
|
4097
|
+
ev.preventDefault();
|
|
4098
|
+
return goToManage(m.getAttribute('data-manage'));
|
|
4099
|
+
}
|
|
3926
4100
|
var t = ev.target.closest('button');
|
|
3927
4101
|
if (!t) return;
|
|
3928
4102
|
if (t.hasAttribute('data-teardown')) return askTeardown(t.getAttribute('data-teardown'));
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Seed the tasks for goal 1000071 "Idea routing — work that has a home lands in it" (BONGOS-V1).
|
|
2
|
+
// Source of truth: docs/specs/bongos-v1-idea-routing.md (status: final).
|
|
3
|
+
// Idempotent on (source, source_ref). Re-running resolves to the existing rows.
|
|
4
|
+
//
|
|
5
|
+
// Run: node scripts/gds/seed-bongos-v1-idea-routing-tasks.js
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const sess = require(os.homedir() + '/.config/cloudbongos/gds-session.json');
|
|
8
|
+
const BASE = sess.api_base.replace(/\/$/, '');
|
|
9
|
+
const TOKEN = sess.token;
|
|
10
|
+
const VER = 'BONGOS-V1';
|
|
11
|
+
const GOAL = 1000071;
|
|
12
|
+
const SRC = 'planning-session';
|
|
13
|
+
const META_TASK = 1003059; // this planning session's meta task — moved into the goal
|
|
14
|
+
const DISCORD_BUGS_IDEA = 1000771; // subsumed by R11; marked promoted against it
|
|
15
|
+
const H = { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' };
|
|
16
|
+
|
|
17
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
18
|
+
async function api(method, path, body, _tries = 0) {
|
|
19
|
+
await sleep(400); // base throttle to stay under the per-IP rate limit
|
|
20
|
+
const res = await fetch(`${BASE}/api/gds${path}`, {
|
|
21
|
+
method, headers: H, body: body ? JSON.stringify(body) : undefined,
|
|
22
|
+
});
|
|
23
|
+
const text = await res.text();
|
|
24
|
+
let json; try { json = JSON.parse(text); } catch { json = { _raw: text.slice(0, 300) }; }
|
|
25
|
+
if (res.status === 429 && _tries < 8) {
|
|
26
|
+
const wait = ((json.error && json.error.details && json.error.details.retry_after_seconds) || 30) + 1;
|
|
27
|
+
console.log(` …rate-limited, waiting ${wait}s`);
|
|
28
|
+
await sleep(wait * 1000);
|
|
29
|
+
return api(method, path, body, _tries + 1);
|
|
30
|
+
}
|
|
31
|
+
return { status: res.status, ok: res.ok, json };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Landing matrix (interview-decided): bugs hard-land (Metic+ filer → ready; sub-Metic → backlog);
|
|
35
|
+
// every other goal-bound kind soft-lands (backlog, one Metic+ promote away); homeless → inbox.
|
|
36
|
+
//
|
|
37
|
+
// crit uses the goal's criterion SLUGS via CRIT — positional "Cn" tokens resolve against the
|
|
38
|
+
// whole VERSION's criterion order (and even matched an old criterion literally slugged "C1"),
|
|
39
|
+
// which mislinked the first seed run; slugs are position-independent.
|
|
40
|
+
const CRIT = {
|
|
41
|
+
C1: 'bug-lands-in-its-goal', C2: 'inbox-holds-only-homeless', C3: 'no-route-loses-the-author',
|
|
42
|
+
C4: 'filing-cannot-outrun-rank', C5: 'system-offers-the-home', C6: 'triage-shrinks-to-judgment',
|
|
43
|
+
};
|
|
44
|
+
const TASKS = [
|
|
45
|
+
// T1 — zero-dep layer — P5
|
|
46
|
+
{ ref:'R02', kind:'infra', disc:'engineer', prio:5, est:90, crit:['C1','C3'], deps:[], title:'R02 · Schema: idea_inbox.suggested_goal_id + lineage origin \'route\'', desc:'Core migration: nullable suggested_goal_id FK on idea_inbox (routing hint for soft-landed/Discord/homeless ideas). Module migration (ideas_*): extend ideas_task_lineage.origin CHECK with \'route\'. See docs/specs/bongos-v1-idea-routing.md.' },
|
|
47
|
+
{ ref:'R03', kind:'bug', disc:'engineer', prio:5, est:90, crit:['C1'], deps:[], title:'R03 · Catch-all fallback never resolves to a non-building version', desc:'generalGoalIdForVersion (modules/lifecycle/db.js:1343) + the null-goal fallback (:1414): a task create must never land in a shipped/frozen version\'s catch-all goal (the 1000771 failure shape). Routed creates with no goal go to the inbox instead. Done-when: test proves a create against a shipped version cannot resolve a catch-all.' },
|
|
48
|
+
{ ref:'R07', kind:'feature', disc:'engineer', prio:5, est:120, crit:['C5'], deps:[], title:'R07 · Capture-time goal suggester (advisory consulted BEFORE creation)', desc:'Expose candidate goals for a draft capture — reuse buildGoalAdvisory\'s candidate logic (modules/lifecycle/goal-advisory.js:89) as a pre-creation read (e.g. GET /inbox/goal-suggestions?title=&kind=&version=). Feeds the hall picker (R08), CLI (R09), and Discord suggestion (R11). Done-when: candidates returned for a draft title/kind without creating anything.' },
|
|
49
|
+
{ ref:'R10', kind:'feature', disc:'ui', prio:5, est:120, crit:['C5'], deps:[], title:'R10 · Hall move-a-task-to-goal affordance', desc:'PATCH /tasks/:id {goal_id} (task 1763) exists but has no hall UI. Add move-to-goal on the task overlay (Metic+, task.edit) so a wrong routing pick is one reassignment, never a re-file. Done-when: a Metic+ moves a task between same-version goals from the hall.' },
|
|
50
|
+
{ ref:'R14', kind:'decision', disc:'engineer', prio:5, est:60, crit:['C2'], deps:[], title:'R14 · ADR: idea routing — capture-time promotion, the landing matrix, the homeless-only inbox', desc:'Write the ADR recording: routing mechanism is capture-time promotion (never inbox bypass — the Discord #bugs lesson), the landing matrix (bugs hard-land by rank; other kinds soft-land; Full Ideas/criterion-proposals/decisions never auto-route), and the inbox\'s new identity (holds only work without a home). Source: docs/specs/bongos-v1-idea-routing.md interview decisions.' },
|
|
51
|
+
|
|
52
|
+
// T2 — the core mechanism — P5
|
|
53
|
+
{ ref:'R01', kind:'feature', disc:'engineer', prio:5, est:240, crit:['C1','C3'], deps:['R02'], title:'R01 · Capture-time routing core: POST /inbox accepts goal_id and converts in one transaction', desc:'Extend inbox.captureIdea/route: when goal_id present and the landing matrix says route, one transaction writes the idea row (status=promoted, promoted_to_task_id), the task (in the goal, matrix-decided status), and ideas_task_lineage (origin=route). Validations: goal open + belongs to a building version; Full-grade and criterion-proposal/decision kinds never auto-route; goal-less capture behaves exactly as today. Deps R02.' },
|
|
54
|
+
|
|
55
|
+
// T3 — enforcement + surfaces — P4
|
|
56
|
+
{ ref:'R05', kind:'feature', disc:'engineer', prio:4, est:150, crit:['C4'], deps:['R01'], title:'R05 · Rank-aware landing: matrix enforcement server-side', desc:'Metic+ filer\'s bug → status=ready; sub-Metic filer / any non-bug kind → backlog (awaiting any Metic+ promote via the existing task.promote, surfaced in the Needs-your-attention slice). Auto-derived requires_rank floors (ADR 0084) and the goal scope wall run on routed tasks exactly as on authored ones. Deps R01.' },
|
|
57
|
+
{ ref:'R11', kind:'feature', disc:'engineer', prio:4, est:180, crit:['C1','C3'], deps:['R01'], title:'R11 · Discord #bugs retired onto the routing path', desc:'Replace discord-bugs.js\'s direct createTask bypass (hardcoded GDS-V3 → goal 1000002 catch-all; destroys attribution) with capture-time routing: idea row + classifier-suggested goal (suggested_goal_id), soft-landing into the pending queue for a one-click Metic+ route. Unlinked authors still yield an attributable idea row (captured_by null). Closes inbox idea 1000771. Deps R01.' },
|
|
58
|
+
{ ref:'R12', kind:'feature', disc:'engineer', prio:4, est:120, crit:['C5'], deps:['R02'], title:'R12 · Discord #ideas carries routing hints', desc:'The #ideas inbound (discord-inbound.js) files with no suggested_version at all today. Add: default suggested_version (most-recent building), classifier-suggested suggested_goal_id. Deps R02.' },
|
|
59
|
+
{ ref:'R08', kind:'feature', disc:'ui', prio:4, est:240, crit:['C1','C5'], deps:['R01','R07'], title:'R08 · Hall filing surface: file a bug/idea with a goal picker', desc:'The hall has NO capture surface today (triage only). Add file-a-bug/idea: title, body, kind, goal picker fed by the suggester (R07); submits to POST /inbox with goal_id so goal-bound work routes at filing time. Grade capture untouched. Deps R01,R07.' },
|
|
60
|
+
{ ref:'R09', kind:'feature', disc:'engineer', prio:4, est:120, crit:['C1','C5'], deps:['R01','R07'], title:'R09 · CLI capture routing: --goal, suggester prompt, active-claim default', desc:'capture.js gains --goal; when the builder holds a claim the suggested goal defaults to the claim\'s goal (interview decision 4); suggester candidates offered otherwise. Deps R01,R07.' },
|
|
61
|
+
{ ref:'R13', kind:'feature', disc:'engineer', prio:4, est:120, crit:['C2','C6'], deps:['R07'], title:'R13 · Promote path passes goal_id; /idea-triage goes homeless-only', desc:'task-detail.js (:320) and triage.js (:160) finally send goal_id (suggester-fed default) so even triaged promotions land in a real goal; the /idea-triage skill doc is updated to the homeless-only contract. Deps R07.' },
|
|
62
|
+
|
|
63
|
+
// T4 — proofs — P3
|
|
64
|
+
{ ref:'R04', kind:'verify', disc:'engineer', prio:3, est:90, crit:['C3'], deps:['R01'], title:'R04 · PROOF: attribution survives the route end-to-end', desc:'A routed bug\'s task ships; sourceIdeaForTask returns the filer; ideas_task_lineage (origin=route) + captured_by intact — the trail goal 1000067\'s ideator credit will pay against (test). Deps R01.' },
|
|
65
|
+
{ ref:'R06', kind:'verify', disc:'engineer', prio:3, est:120, crit:['C4'], deps:['R05'], title:'R06 · PROOF: abuse matrix — filing cannot escalate', desc:'A Xenos mass-filing bugs never lands claimable work, never reaches protected scope, rate limits hold; a sub-Metic cannot reach ready via any capture vector (test suite). Deps R05.' },
|
|
66
|
+
|
|
67
|
+
// T5 — the measured close — P2
|
|
68
|
+
{ ref:'R15', kind:'verify', disc:'engineer', prio:2, est:120, crit:['C2','C6'], deps:['R01','R05','R13'], title:'R15 · PROOF: reclassification sweep of the open inbox', desc:'Walk the open idea_inbox (~50 rows at planning time; 14 bugs) under the new rules: every goal-bound bug homed in a real goal; residual inbox is homeless-only; before/after counts recorded in this task\'s notes as the goal\'s measured evidence. Deps R01,R05,R13.' },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const ROOTS = ['R02', 'R03', 'R07', 'R10', 'R14']; // zero-dep, non-spike → open for claiming
|
|
72
|
+
|
|
73
|
+
(async () => {
|
|
74
|
+
const idByRef = {};
|
|
75
|
+
let created = 0, existing = 0;
|
|
76
|
+
console.log(`Seeding ${TASKS.length} tasks under goal ${GOAL} (${VER})…`);
|
|
77
|
+
for (const t of TASKS) {
|
|
78
|
+
const r = await api('POST', '/tasks', {
|
|
79
|
+
version_id: VER, goal_id: GOAL, title: t.title, description: t.desc,
|
|
80
|
+
kind: t.kind, discipline: t.disc, priority: t.prio, est_minutes: t.est,
|
|
81
|
+
criterion_ids: t.crit.map((c) => CRIT[c]), source: SRC, source_ref: `bongos-v1-idea-routing-${t.ref}`,
|
|
82
|
+
});
|
|
83
|
+
if (!r.ok) { console.log(` ${t.ref}: FAILED ${r.status} ${JSON.stringify(r.json)}`); continue; }
|
|
84
|
+
const task = r.json.task || r.json;
|
|
85
|
+
idByRef[t.ref] = task.id;
|
|
86
|
+
if (r.json.idempotent || r.json.existing) existing++; else created++;
|
|
87
|
+
console.log(` ${t.ref} → task ${task.id} (${t.kind}/${t.disc}, P${t.prio}, [${t.crit.join(',')}])`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log('Wiring dependencies…');
|
|
91
|
+
let edges = 0;
|
|
92
|
+
for (const t of TASKS) {
|
|
93
|
+
if (!t.deps.length) continue;
|
|
94
|
+
const id = idByRef[t.ref];
|
|
95
|
+
const depIds = t.deps.map((d) => idByRef[d]).filter(Boolean);
|
|
96
|
+
if (!id || depIds.length !== t.deps.length) { console.log(` ${t.ref}: dep resolve gap, skipping`); continue; }
|
|
97
|
+
const r = await api('POST', `/tasks/${id}/dependencies`, { depends_on_task_ids: depIds });
|
|
98
|
+
if (!r.ok) { console.log(` ${t.ref}: dep FAILED ${r.status} ${JSON.stringify(r.json)}`); continue; }
|
|
99
|
+
edges += (r.json.added || depIds.length);
|
|
100
|
+
console.log(` ${t.ref} depends on ${t.deps.join(',')}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log('Promoting the zero-dep layer to ready…');
|
|
104
|
+
for (const ref of ROOTS) {
|
|
105
|
+
const id = idByRef[ref];
|
|
106
|
+
if (!id) continue;
|
|
107
|
+
const r = await api('POST', `/tasks/${id}/promote`, {});
|
|
108
|
+
console.log(` ${ref} (task ${id}): ${r.ok ? 'ready' : 'promote note ' + JSON.stringify(r.json).slice(0, 120)}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log('Linking provenance…');
|
|
112
|
+
if (idByRef.R11) {
|
|
113
|
+
const r = await api('PATCH', `/inbox/${DISCORD_BUGS_IDEA}`, { status: 'promoted', promoted_to_task_id: Number(idByRef.R11) });
|
|
114
|
+
console.log(` idea ${DISCORD_BUGS_IDEA} → ${r.ok ? `promoted against R11 (task ${idByRef.R11})` : 'PATCH note ' + JSON.stringify(r.json).slice(0, 160)}`);
|
|
115
|
+
}
|
|
116
|
+
const meta = await api('PATCH', `/tasks/${META_TASK}`, { goal_id: GOAL });
|
|
117
|
+
console.log(` meta task ${META_TASK} → ${meta.ok ? `goal ${GOAL}` : 'PATCH note ' + JSON.stringify(meta.json).slice(0, 160)}`);
|
|
118
|
+
|
|
119
|
+
console.log(`\nDONE. created=${created} existing=${existing} depEdges=${edges}. Roots opened: ${ROOTS.join(', ')}.`);
|
|
120
|
+
console.log('ref→id:', JSON.stringify(idByRef));
|
|
121
|
+
})();
|
package/src/module-api.js
CHANGED
|
@@ -843,8 +843,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
843
843
|
// landed since 1.19.206 with no explicit bump. run 32065995444. (task 1002620)
|
|
844
844
|
// 1.19.208 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
845
845
|
// landed since 1.19.207 with no explicit bump. run 32068332007. (task 1002620)
|
|
846
|
+
// 1.19.209 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
847
|
+
// landed since 1.19.208 with no explicit bump. run 32072227661. (task 1002620)
|
|
848
|
+
// 1.19.210 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
849
|
+
// landed since 1.19.209 with no explicit bump. run 32078135414. (task 1002620)
|
|
846
850
|
// ---------------------------------------------------------------------------
|
|
847
|
-
const CORE_VERSION = '1.19.
|
|
851
|
+
const CORE_VERSION = '1.19.210'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
848
852
|
|
|
849
853
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
850
854
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -51,7 +51,11 @@ test('the nav carries Home plus the three views, and Home is not a view', () =>
|
|
|
51
51
|
|
|
52
52
|
test('?view= is the deep-link contract, read on boot and written with replaceState', () => {
|
|
53
53
|
assert.match(SCRIPT, /new URLSearchParams\(location\.search\)\.get\('view'\)/);
|
|
54
|
-
assert.match(SCRIPT, /history\.replaceState\(null, '', '\?view=' \+ v\)/);
|
|
54
|
+
assert.match(SCRIPT, /history\.replaceState\(null, '', '\?view=' \+ v \+ idPart\)/);
|
|
55
|
+
// the manage view is per-project, so its deep link carries the id too
|
|
56
|
+
// (task 1003041) — and the id is encoded, never raw-interpolated
|
|
57
|
+
assert.match(SCRIPT, /'&id=' \+ encodeURIComponent\(manageId\)/,
|
|
58
|
+
'the URL writer must carry the manage id, encoded');
|
|
55
59
|
// the landing page's CTAs have always pointed here — this is the bug they hit
|
|
56
60
|
const landing = fs.readFileSync(path.join(ROOT, 'public-landing', 'index.html'), 'utf8');
|
|
57
61
|
assert.match(landing, /href="\/projects\?view=new"/,
|
|
@@ -753,7 +757,8 @@ test('owned truth reaches the TABLE too — same list, same chip, same manage jo
|
|
|
753
757
|
});
|
|
754
758
|
|
|
755
759
|
test('an owned orb\'s job is manage, not the public site', () => {
|
|
756
|
-
assert.match(SCRIPT, /'\?view=
|
|
760
|
+
assert.match(SCRIPT, /'\?view=manage&id=' \+ encodeURIComponent\(String\(inst\.id\)\)/,
|
|
761
|
+
'the owned href is the manage deep-link, real and middle-clickable');
|
|
757
762
|
assert.match(SCRIPT, /hasAttribute\('data-manage'\)\) return goToManage/,
|
|
758
763
|
'a plain click on an owned orb routes to manage before the card path');
|
|
759
764
|
const orbs = (SCRIPT.match(/function renderOrbs\(([\s\S]*?)\n \}/) || [, ''])[1];
|
|
@@ -794,13 +799,15 @@ test('the orb chip is the manage list\'s truth, never a second story (C6)', () =
|
|
|
794
799
|
'no instance chip may be literal-classed live — chipMarkup() decides from the overlay');
|
|
795
800
|
});
|
|
796
801
|
|
|
797
|
-
test('click-to-manage
|
|
798
|
-
|
|
799
|
-
assert.
|
|
800
|
-
assert.match(
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
802
|
+
test('click-to-manage opens the per-project manage view (task 1003041)', () => {
|
|
803
|
+
const fn = (SCRIPT.match(/function goToManage\(instId\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
804
|
+
assert.ok(fn, 'goToManage() must be extractable');
|
|
805
|
+
assert.match(fn, /manageId = String\(instId/, 'the target project rides the module state');
|
|
806
|
+
assert.match(fn, /setView\('manage'\)/, 'the destination is the manage view, not a scroll anchor');
|
|
807
|
+
// the scroll-anchor era ended with its caller — a half-removed pendingAnchor
|
|
808
|
+
// would silently no-op every manage click
|
|
809
|
+
assert.doesNotMatch(SCRIPT, /pendingAnchor/, 'the anchor machinery must be fully gone');
|
|
810
|
+
assert.doesNotMatch(COSMOS, /\.proj\.anchored/, 'and so must its landing-glow rule');
|
|
804
811
|
});
|
|
805
812
|
|
|
806
813
|
test('the map does not poll for owned truth — it re-reads on entry (C8)', () => {
|
|
@@ -887,3 +894,86 @@ test('zoom heads for the cursor and survives line-mode wheels', () => {
|
|
|
887
894
|
assert.ok(COSMOS.includes('transform-origin:0 0'),
|
|
888
895
|
'the camera algebra assumes the layer scales about its origin');
|
|
889
896
|
});
|
|
897
|
+
|
|
898
|
+
/* ── The manage view (task 1003041, goal-1000058 wave) ──────────────────────
|
|
899
|
+
One owned project's management home: ?view=manage&id=N. The identity block
|
|
900
|
+
renders the own-scoped per-id read (with the verification overlay); the
|
|
901
|
+
settings sections are the homes the sibling manage-wave tasks fill in, each
|
|
902
|
+
an honest not-wired-yet until its task ships. The API is the wall: 404 for
|
|
903
|
+
strangers (owner or archon only), so the page renders one clean not-found
|
|
904
|
+
and never a permission hint. ── */
|
|
905
|
+
|
|
906
|
+
test('the manage view ships as real markup with the four settings homes (C7)', () => {
|
|
907
|
+
assert.match(HUB, /id="view-manage"/);
|
|
908
|
+
for (const id of ['mGate', 'mSignin', 'mMissing', 'mAlert', 'mLoading', 'mMain',
|
|
909
|
+
'mName', 'mChip', 'mMeta', 'mAddr', 'mCreated',
|
|
910
|
+
'mVisibility', 'mJoinability', 'mRepoVisibility', 'mDelete']) {
|
|
911
|
+
assert.match(HUB, new RegExp(`id="${id}"`), `${id} must be real markup`);
|
|
912
|
+
}
|
|
913
|
+
// and the not-yet copy a crawler reads is in the document, not a template string
|
|
914
|
+
assert.match(HUB, /isn’t wired up/, 'the honest empty states live in the markup');
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
test('the manage view balances its own tree, like the wizard', () => {
|
|
918
|
+
const start = HUB.indexOf('<section id="view-manage"');
|
|
919
|
+
const end = HUB.indexOf('<section id="view-new"');
|
|
920
|
+
assert.ok(start !== -1 && end > start, 'need the view-manage span');
|
|
921
|
+
const manage = HUB.slice(start, end);
|
|
922
|
+
assert.equal((manage.match(/<div\b/g) || []).length, (manage.match(/<\/div>/g) || []).length,
|
|
923
|
+
'every div opened in #view-manage closes in #view-manage');
|
|
924
|
+
assert.equal((manage.match(/<section\b/g) || []).length, (manage.match(/<\/section>/g) || []).length,
|
|
925
|
+
'every section opened in #view-manage closes in #view-manage');
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
test('?view=manage&id=N is the deep link — read on boot, and no id means the list', () => {
|
|
929
|
+
assert.match(SCRIPT, /if \(v === 'manage'\)/, 'viewFromUrl must know the manage view');
|
|
930
|
+
assert.match(SCRIPT, /\.get\('id'\)/, 'and read the project id off the URL');
|
|
931
|
+
assert.match(SCRIPT, /manageId = String\(id\); return 'manage';/,
|
|
932
|
+
'an id lands on the manage view');
|
|
933
|
+
assert.match(SCRIPT, /return 'mine';/,
|
|
934
|
+
'a bare ?view=manage names no project — the list is the honest landing');
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
test('a stranger\'s deep link renders one clean not-found — never a permission hint', () => {
|
|
938
|
+
const missing = (HUB.match(/<section id="mMissing"[\s\S]*?<\/section>/) || [''])[0];
|
|
939
|
+
assert.ok(missing, 'the not-found state must be extractable');
|
|
940
|
+
assert.doesNotMatch(missing, /permission|allowed|access|owner/i,
|
|
941
|
+
'the 404 copy must not hint that the project exists behind a wall');
|
|
942
|
+
assert.match(SCRIPT, /res\.status === 404[\s\S]{0,220}mMissing/,
|
|
943
|
+
'a 404 lands on the clean not-found state');
|
|
944
|
+
assert.match(SCRIPT, /res\.status === 401[\s\S]{0,220}mGate/,
|
|
945
|
+
'a 401 stays a sign-in gate, distinct from not-found');
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
test('the manage chip defers to chipFor — one site, no second story (C6)', () => {
|
|
949
|
+
const fn = (SCRIPT.match(/function renderManage\(inst\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
950
|
+
assert.ok(fn, 'renderManage() must be extractable');
|
|
951
|
+
assert.equal((fn.match(/chipFor\(inst\)/g) || []).length, 1,
|
|
952
|
+
'renderManage must have exactly one chip call site');
|
|
953
|
+
assert.match(fn, /addressRow\(inst\)/, 'the address sentence is the shared, verified one');
|
|
954
|
+
assert.match(fn, /SHAPE_LABEL/, 'the hosting shape speaks the card\'s vocabulary');
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
test('the manage view spends no timer requests — refresh is entry and tab return (C8)', () => {
|
|
958
|
+
const tick = (SCRIPT.match(/function manageChipTick\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
959
|
+
assert.ok(tick, 'manageChipTick() must exist');
|
|
960
|
+
assert.match(tick, /currentView !== 'manage'/, 'the tick is view-guarded');
|
|
961
|
+
assert.match(tick, /document\.hidden/, 'and does nothing in a hidden tab');
|
|
962
|
+
assert.doesNotMatch(tick, /fetch\(/, 'zero requests — recency re-renders from data in hand');
|
|
963
|
+
assert.match(SCRIPT, /if \(v === 'manage'\) loadManage\(\);/, 'entry re-reads once');
|
|
964
|
+
assert.match(SCRIPT, /currentView === 'manage' && manageId\) loadManage\(true\)/,
|
|
965
|
+
'tab return re-reads once, quietly');
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
test('the repo link is built on github.com and escaped — never a raw href', () => {
|
|
969
|
+
assert.match(SCRIPT, /https:\/\/github\.com\/' \+ esc\(inst\.target_ref\)/,
|
|
970
|
+
'the identity block links the repo through the page escaper on a fixed origin');
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
test('every owned card carries its Manage door, and a modified click keeps its meaning', () => {
|
|
974
|
+
assert.match(SCRIPT, /data-manage="' \+ iid \+ '" href="\?view=manage&id=' \+ iid/,
|
|
975
|
+
'the card affordance is a real deep-link anchor');
|
|
976
|
+
assert.match(SCRIPT, /closest\('a\[data-manage\]'\)/, 'the delegated handler upgrades it');
|
|
977
|
+
assert.match(SCRIPT, /if \(!isPlainClick\(ev\)\) return;/,
|
|
978
|
+
'ctrl/middle-click falls through to the browser');
|
|
979
|
+
});
|
|
@@ -97,7 +97,36 @@ function reset({ inst, as } = {}) {
|
|
|
97
97
|
events = [];
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
console.log('GET /provisioning/instances/:id
|
|
100
|
+
console.log('GET /provisioning/instances/:id (the manage page\'s read — task 1003041):');
|
|
101
|
+
|
|
102
|
+
await ta('owner reads their own instance', async () => {
|
|
103
|
+
reset();
|
|
104
|
+
const r = await get('/provisioning/instances/18');
|
|
105
|
+
assert.equal(r.status, 200, r.raw);
|
|
106
|
+
assert.equal(r.body.instance.slug, 'spike');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await ta('ANOTHER builder\'s GET → 404 (no existence leak)', async () => {
|
|
110
|
+
reset({ as: { id: 8, github_login: 'stranger', rank: 'thetes' } });
|
|
111
|
+
const r = await get('/provisioning/instances/18');
|
|
112
|
+
assert.equal(r.status, 404, r.raw);
|
|
113
|
+
assert.equal(r.body.error.code, 'instance_not_found');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await ta('an Archon may read ANY instance — the manage-page override (task 1003041)', async () => {
|
|
117
|
+
reset({ as: { id: 999, github_login: 'archon', rank: 'archon' } });
|
|
118
|
+
const r = await get('/provisioning/instances/18');
|
|
119
|
+
assert.equal(r.status, 200, r.raw);
|
|
120
|
+
assert.equal(r.body.instance.slug, 'spike');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await ta('unknown instance id → 404 for everyone, archon included', async () => {
|
|
124
|
+
reset({ inst: null, as: { id: 999, github_login: 'archon', rank: 'archon' } });
|
|
125
|
+
const r = await get('/provisioning/instances/999');
|
|
126
|
+
assert.equal(r.status, 404, r.raw);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
console.log('\nGET /provisioning/instances/:id/settings:');
|
|
101
130
|
|
|
102
131
|
await ta('owner reads DEFAULTS (public / apply) on a row with empty settings — today\'s behavior', async () => {
|
|
103
132
|
reset();
|
|
@@ -107,6 +107,20 @@ test('a domainless instance gets an honest no-address state', async () => {
|
|
|
107
107
|
assert.match(body, /[Nn]o address yet/);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
test('the manage view is served: identity block + honest not-yet sections (task 1003041)', async () => {
|
|
111
|
+
const { body } = await request('/projects', 'game.test.local');
|
|
112
|
+
assert.match(body, /id="view-manage"/);
|
|
113
|
+
// the four settings homes the sibling manage-wave tasks fill in
|
|
114
|
+
for (const id of ['mVisibility', 'mJoinability', 'mRepoVisibility', 'mDelete']) {
|
|
115
|
+
assert.match(body, new RegExp(`id="${id}"`), `${id} must be served`);
|
|
116
|
+
}
|
|
117
|
+
assert.match(body, /isn’t wired up/, 'each unshipped section says so honestly');
|
|
118
|
+
// the not-found state is one clean answer — never a permission hint
|
|
119
|
+
const missing = (body.match(/<section id="mMissing"[\s\S]*?<\/section>/) || [''])[0];
|
|
120
|
+
assert.ok(missing, 'the not-found state must be served');
|
|
121
|
+
assert.doesNotMatch(missing, /permission|allowed|access|owner/i);
|
|
122
|
+
});
|
|
123
|
+
|
|
110
124
|
test('the empty state points a new owner at the wizard view — never at a hall', async () => {
|
|
111
125
|
const { body } = await request('/projects', 'game.test.local');
|
|
112
126
|
// /create was absorbed into ?view=new (task 1002763); the CTA moves in-page
|