@bongos/core 1.19.209 → 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 +16 -16
- 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/src/module-api.js +3 -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,11 +2,11 @@
|
|
|
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,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1631,
|
|
20
|
-
"tree_sha256": "
|
|
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",
|
|
@@ -5902,7 +5902,7 @@
|
|
|
5902
5902
|
{
|
|
5903
5903
|
"path": "src/module-api.js",
|
|
5904
5904
|
"mode": "0000644",
|
|
5905
|
-
"sha256": "
|
|
5905
|
+
"sha256": "8f9beea649114a7133ef56008878f45197bb97c29bdddf6e5c6c5d786ca30981"
|
|
5906
5906
|
},
|
|
5907
5907
|
{
|
|
5908
5908
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7522,7 +7522,7 @@
|
|
|
7522
7522
|
{
|
|
7523
7523
|
"path": "tests/projects_hub.mjs",
|
|
7524
7524
|
"mode": "0000644",
|
|
7525
|
-
"sha256": "
|
|
7525
|
+
"sha256": "3c31493c1b2a804b52f34310dc31f10b3bf67387ed9266c06fd7295aac2f0d5b"
|
|
7526
7526
|
},
|
|
7527
7527
|
{
|
|
7528
7528
|
"path": "tests/provision.mjs",
|
|
@@ -7552,7 +7552,7 @@
|
|
|
7552
7552
|
{
|
|
7553
7553
|
"path": "tests/provisioning_settings.mjs",
|
|
7554
7554
|
"mode": "0000644",
|
|
7555
|
-
"sha256": "
|
|
7555
|
+
"sha256": "a525fdcfa2fde8edd2be3198ca7213b9b6bc43a12f748a718a570b7f993972b6"
|
|
7556
7556
|
},
|
|
7557
7557
|
{
|
|
7558
7558
|
"path": "tests/provisioning_slug_available.mjs",
|
|
@@ -7572,7 +7572,7 @@
|
|
|
7572
7572
|
{
|
|
7573
7573
|
"path": "tests/public_landing_projects.mjs",
|
|
7574
7574
|
"mode": "0000644",
|
|
7575
|
-
"sha256": "
|
|
7575
|
+
"sha256": "d3e132bd85382b1a9ff4fa2d582a5fc72dabc91b2dc50828b5f622f6c112b2f1"
|
|
7576
7576
|
},
|
|
7577
7577
|
{
|
|
7578
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'));
|
package/src/module-api.js
CHANGED
|
@@ -845,8 +845,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
845
845
|
// landed since 1.19.207 with no explicit bump. run 32068332007. (task 1002620)
|
|
846
846
|
// 1.19.209 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
847
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)
|
|
848
850
|
// ---------------------------------------------------------------------------
|
|
849
|
-
const CORE_VERSION = '1.19.
|
|
851
|
+
const CORE_VERSION = '1.19.210'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
850
852
|
|
|
851
853
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
852
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
|