@bongos/core 1.19.65 → 1.19.67
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 +30 -20
- package/.claude/skills/new-project/SKILL.md +7 -0
- package/docs/api/openapi.json +1 -1
- package/docs/api-reference.md +1 -1
- package/modules/provisioning/onboard-plan.js +15 -6
- package/modules/provisioning/routes/provisioning.js +10 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +14 -2
- package/public-landing/projects.html +591 -0
- package/scripts/gds/onboard.js +9 -0
- package/src/module-api.js +5 -1
- package/src/platform-server.js +9 -0
- package/tests/onboard.mjs +44 -3
- package/tests/provisioning_onboard_mode.mjs +26 -0
- package/tests/public_landing_projects.mjs +121 -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.67",
|
|
6
|
+
"core_contract": "1.19.67",
|
|
7
|
+
"source_commit": "cd54b1b3803fb68412c34eabaf969761c6b22728",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-12T14:13:54.700Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 292,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1222,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1535,
|
|
20
|
+
"tree_sha256": "6689b03f0138061c04be8b757cd0a1a13b43f4c64e6d766df97906e306378cf9",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
{
|
|
188
188
|
"path": ".claude/skills/new-project/SKILL.md",
|
|
189
189
|
"mode": "0000644",
|
|
190
|
-
"sha256": "
|
|
190
|
+
"sha256": "55b86978a60e33eefe024d7c7d521e4093b82235eeb7e5d4a145fbb0e4db0b06"
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
193
|
"path": ".claude/skills/otb-character-review/SKILL.md",
|
|
@@ -1312,12 +1312,12 @@
|
|
|
1312
1312
|
{
|
|
1313
1313
|
"path": "docs/api-reference.md",
|
|
1314
1314
|
"mode": "0000644",
|
|
1315
|
-
"sha256": "
|
|
1315
|
+
"sha256": "1a4562c2e0575f8505a22851c63f1cc3ecfd485ae5de6e725810786f692e6457"
|
|
1316
1316
|
},
|
|
1317
1317
|
{
|
|
1318
1318
|
"path": "docs/api/openapi.json",
|
|
1319
1319
|
"mode": "0000644",
|
|
1320
|
-
"sha256": "
|
|
1320
|
+
"sha256": "9a551d5c62209f2c1b935194cc37e86ca5c44b317901499da84126e3868df1fa"
|
|
1321
1321
|
},
|
|
1322
1322
|
{
|
|
1323
1323
|
"path": "docs/architecture.md",
|
|
@@ -3737,7 +3737,7 @@
|
|
|
3737
3737
|
{
|
|
3738
3738
|
"path": "modules/provisioning/onboard-plan.js",
|
|
3739
3739
|
"mode": "0000644",
|
|
3740
|
-
"sha256": "
|
|
3740
|
+
"sha256": "19224b44f22a18db651cfefa87c90c250e911dc1b1d00b76ed7c9361b505c037"
|
|
3741
3741
|
},
|
|
3742
3742
|
{
|
|
3743
3743
|
"path": "modules/provisioning/provisioning.js",
|
|
@@ -3752,7 +3752,7 @@
|
|
|
3752
3752
|
{
|
|
3753
3753
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
3754
3754
|
"mode": "0000644",
|
|
3755
|
-
"sha256": "
|
|
3755
|
+
"sha256": "a8e2fa66eb19d119e117b80ca49832bec240cb71d167719e2a86f0032823b506"
|
|
3756
3756
|
},
|
|
3757
3757
|
{
|
|
3758
3758
|
"path": "modules/provisioning/tests/provisioning.mjs",
|
|
@@ -4042,12 +4042,12 @@
|
|
|
4042
4042
|
{
|
|
4043
4043
|
"path": "package-lock.json",
|
|
4044
4044
|
"mode": "0000644",
|
|
4045
|
-
"sha256": "
|
|
4045
|
+
"sha256": "2e4247ac977969f19efdfa6e7e4c111f131e8e6200e393b7e19393d65b57d6ae"
|
|
4046
4046
|
},
|
|
4047
4047
|
{
|
|
4048
4048
|
"path": "package.json",
|
|
4049
4049
|
"mode": "0000644",
|
|
4050
|
-
"sha256": "
|
|
4050
|
+
"sha256": "a840fe8d34e5195d246bfb20de03cf4864bec50d21ba5d3abe4eb17f8ed47275"
|
|
4051
4051
|
},
|
|
4052
4052
|
{
|
|
4053
4053
|
"path": "public-docs/index.html",
|
|
@@ -4292,7 +4292,7 @@
|
|
|
4292
4292
|
{
|
|
4293
4293
|
"path": "public-landing/create.html",
|
|
4294
4294
|
"mode": "0000644",
|
|
4295
|
-
"sha256": "
|
|
4295
|
+
"sha256": "a90e1cfe70592338dd9e3bb733a5655f8b772594f74a09b0b2a3075ff90c5487"
|
|
4296
4296
|
},
|
|
4297
4297
|
{
|
|
4298
4298
|
"path": "public-landing/gate/README.md",
|
|
@@ -4364,6 +4364,11 @@
|
|
|
4364
4364
|
"mode": "0000644",
|
|
4365
4365
|
"sha256": "e43daead0661cbae65c579dd9fbd6a70826c19bc444204dde6fa7dbc82d04e45"
|
|
4366
4366
|
},
|
|
4367
|
+
{
|
|
4368
|
+
"path": "public-landing/projects.html",
|
|
4369
|
+
"mode": "0000644",
|
|
4370
|
+
"sha256": "5cddb3577caebe5fa41e0782a5b79e9f7cefae79eff195a5379f20cdc92bfce9"
|
|
4371
|
+
},
|
|
4367
4372
|
{
|
|
4368
4373
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
4369
4374
|
"mode": "0000644",
|
|
@@ -4972,7 +4977,7 @@
|
|
|
4972
4977
|
{
|
|
4973
4978
|
"path": "scripts/gds/onboard.js",
|
|
4974
4979
|
"mode": "0000644",
|
|
4975
|
-
"sha256": "
|
|
4980
|
+
"sha256": "11472bb450751330e865cbd8f6dba0276005f120db6438e058cd148dbeb580fb"
|
|
4976
4981
|
},
|
|
4977
4982
|
{
|
|
4978
4983
|
"path": "scripts/gds/onboarding-analyze.js",
|
|
@@ -5667,7 +5672,7 @@
|
|
|
5667
5672
|
{
|
|
5668
5673
|
"path": "src/module-api.js",
|
|
5669
5674
|
"mode": "0000644",
|
|
5670
|
-
"sha256": "
|
|
5675
|
+
"sha256": "7f872cf877475c7b89a389c016abb6cdcdbc34a7847b8bf882dd67e8dc49048c"
|
|
5671
5676
|
},
|
|
5672
5677
|
{
|
|
5673
5678
|
"path": "src/module-loader/loader.js",
|
|
@@ -5702,7 +5707,7 @@
|
|
|
5702
5707
|
{
|
|
5703
5708
|
"path": "src/platform-server.js",
|
|
5704
5709
|
"mode": "0000644",
|
|
5705
|
-
"sha256": "
|
|
5710
|
+
"sha256": "04204149610d3b7ebc087fb6feb9dc86f25bb08b1ef52003dddc378861557d46"
|
|
5706
5711
|
},
|
|
5707
5712
|
{
|
|
5708
5713
|
"path": "src/ui/adapters/claude-design/index.js",
|
|
@@ -7032,7 +7037,7 @@
|
|
|
7032
7037
|
{
|
|
7033
7038
|
"path": "tests/onboard.mjs",
|
|
7034
7039
|
"mode": "0000644",
|
|
7035
|
-
"sha256": "
|
|
7040
|
+
"sha256": "93125254eed5a46e933484062942e43966e3cac898e642d546cd467da635223f"
|
|
7036
7041
|
},
|
|
7037
7042
|
{
|
|
7038
7043
|
"path": "tests/onboarding_gate.mjs",
|
|
@@ -7162,7 +7167,7 @@
|
|
|
7162
7167
|
{
|
|
7163
7168
|
"path": "tests/provisioning_onboard_mode.mjs",
|
|
7164
7169
|
"mode": "0000644",
|
|
7165
|
-
"sha256": "
|
|
7170
|
+
"sha256": "7749df454ac3cca7c7e5ea1acc94ead15d1c9f87b38fe2028a1c93c8adf31bcf"
|
|
7166
7171
|
},
|
|
7167
7172
|
{
|
|
7168
7173
|
"path": "tests/provisioning_teardown_intent.mjs",
|
|
@@ -7174,6 +7179,11 @@
|
|
|
7174
7179
|
"mode": "0000644",
|
|
7175
7180
|
"sha256": "a2e29941b5275751d37004bbe01a7123f0d11edcd1d34f5a2ee4079f91cd711a"
|
|
7176
7181
|
},
|
|
7182
|
+
{
|
|
7183
|
+
"path": "tests/public_landing_projects.mjs",
|
|
7184
|
+
"mode": "0000644",
|
|
7185
|
+
"sha256": "489ad330f7fe118ffd66a22b0a437743a2f3d3610a5324ba764abc4fc88b0c74"
|
|
7186
|
+
},
|
|
7177
7187
|
{
|
|
7178
7188
|
"path": "tests/public_mirror_export.mjs",
|
|
7179
7189
|
"mode": "0000644",
|
|
@@ -99,6 +99,13 @@ Confirm the owner is **Archon** (their profile / the hall roster). If `auth_conf
|
|
|
99
99
|
|
|
100
100
|
Fill the instance **look** — theme / palette / fonts in `config/branding.json` (see `docs/branding-fill-prompt.md`). The identity strings were set at init; this is the visual layer. "Brand with your own LLM."
|
|
101
101
|
|
|
102
|
+
### 9. Enter your builders hall
|
|
103
|
+
|
|
104
|
+
The hall is the point of everything above — end there, not at brand (task 1002703):
|
|
105
|
+
|
|
106
|
+
- **With a domain:** open `https://<domain>/builders` — that's the project's home.
|
|
107
|
+
- **No domain yet:** run `bongos dev` in the repo checkout — it stands the instance up locally and serves the hall on the owner's machine ([ADR 0149](../../../docs/adr/<redacted>.md)).
|
|
108
|
+
|
|
102
109
|
## Done-when
|
|
103
110
|
|
|
104
111
|
A new owner has gone zero → live: their own standalone repo on a pinned core, provisioned with no hand-editing of servers, the four human-only steps done UI-first + verified, and they are signed in as the founding Archon on their own domain. That is goal 35's `project-creation-flow` criterion.
|
package/docs/api/openapi.json
CHANGED
|
@@ -8733,7 +8733,7 @@
|
|
|
8733
8733
|
"provisioning"
|
|
8734
8734
|
],
|
|
8735
8735
|
"summary": "GET /provisioning/onboard-plan",
|
|
8736
|
-
"description": "GET /provisioning/onboard-plan?mode=greenfield|adopt — the CANONICAL onboarding step sequence (task 2085; goal 35 / criterion 165: \"the canonical step sequence is the source of truth; skill/CLI/hall are forms over it\"). The branded /create page + the hall wizard fetch it so their human-steps checklist can't drift from the CLI's (scripts/gds/onboard.js reads the same modules/provisioning/onboard-plan.js). rank: public — the signed-out /create landing page reads it; it is the same public runbook the /new-project skill documents (no write, no secret).\n\n**Rank:** `public` — No authentication — any caller.",
|
|
8736
|
+
"description": "GET /provisioning/onboard-plan?mode=greenfield|adopt&domain=<host> — the CANONICAL onboarding step sequence (task 2085; goal 35 / criterion 165: \"the canonical step sequence is the source of truth; skill/CLI/hall are forms over it\"). The branded /create page + the hall wizard fetch it so their human-steps checklist can't drift from the CLI's (scripts/gds/onboard.js reads the same modules/provisioning/onboard-plan.js). `domain` relabels the terminal enter-hall step with the instance's own hall address (task 1002703); a non-hostname is dropped — clients render these labels, so unvalidated input must never be echoed back. rank: public — the signed-out /create landing page reads it; it is the same public runbook the /new-project skill documents (no write, no secret).\n\n**Rank:** `public` — No authentication — any caller.",
|
|
8737
8737
|
"x-rank": "public",
|
|
8738
8738
|
"x-source": "modules/provisioning/routes/provisioning.js",
|
|
8739
8739
|
"responses": {
|
package/docs/api-reference.md
CHANGED
|
@@ -374,7 +374,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
374
374
|
| POST | `/api/bongos/provisioning/instances/:id/force-teardown` | `metic+archon` | — | POST /provisioning/instances/:id/force-teardown — Archon tears down ANY instance. |
|
|
375
375
|
| 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… |
|
|
376
376
|
| POST | `/api/bongos/provisioning/instances/:id/teardown` | `any-builder` | — | POST /provisioning/instances/:id/teardown — request teardown of the caller's OWN instance. |
|
|
377
|
-
| GET | `/api/bongos/provisioning/onboard-plan` | `public` | — | GET /provisioning/onboard-plan?mode=greenfield\|adopt — the CANONICAL onboarding step sequence (task 2085; goal 35 /
|
|
377
|
+
| GET | `/api/bongos/provisioning/onboard-plan` | `public` | — | GET /provisioning/onboard-plan?mode=greenfield\|adopt&domain=<host> — the CANONICAL onboarding step sequence (task 2085; goal 35 / criteri… |
|
|
378
378
|
| GET | `/api/bongos/provisioning/tls-check` | `public` | — | GET /provisioning/tls-check?domain=<host> — the Caddy on-demand-TLS ALLOW-LIST (ADR 0111 §4, task P4 task 1945). |
|
|
379
379
|
|
|
380
380
|
## `public` (18)
|
|
@@ -11,13 +11,19 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Each step: { key, kind: 'auto' | 'human', label }. `kind: 'human'` marks the
|
|
13
13
|
// irreducibly-manual owner actions (create the GitHub repo, wire GitHub sign-in, first
|
|
14
|
-
// sign-in) that every form renders UI-first; `kind: 'auto'` steps the
|
|
15
|
-
// The sequence branches at exactly ONE leg — scaffold — into greenfield
|
|
16
|
-
// a fresh empty repo) vs brownfield (`bongos init --adopt`, LAYER onto
|
|
17
|
-
// ADR 0121 §Decision 1 / task 2046). Every other leg is identical, so
|
|
18
|
-
// relabels the github-repo / init / scaffold steps.
|
|
19
|
-
|
|
14
|
+
// sign-in, enter the hall) that every form renders UI-first; `kind: 'auto'` steps the
|
|
15
|
+
// flow performs. The sequence branches at exactly ONE leg — scaffold — into greenfield
|
|
16
|
+
// (`bongos init`, a fresh empty repo) vs brownfield (`bongos init --adopt`, LAYER onto
|
|
17
|
+
// an existing repo; ADR 0121 §Decision 1 / task 2046). Every other leg is identical, so
|
|
18
|
+
// `mode` only relabels the github-repo / init / scaffold steps.
|
|
19
|
+
//
|
|
20
|
+
// `opts.domain` (task 1002703): the terminal enter-hall step names the instance's own
|
|
21
|
+
// hall address when the renderer knows the domain, and falls back to the `bongos dev`
|
|
22
|
+
// local-hall one-liner when it doesn't — the hall is the point of creating a project,
|
|
23
|
+
// so the sequence must end there, not at brand.
|
|
24
|
+
function onboardPlan(mode = 'greenfield', opts = {}) {
|
|
20
25
|
const adopt = mode === 'adopt';
|
|
26
|
+
const domain = typeof opts.domain === 'string' && opts.domain.trim() ? opts.domain.trim() : null;
|
|
21
27
|
return [
|
|
22
28
|
{ key: 'name-address', kind: 'auto', label: 'Decide name + address (interview)' },
|
|
23
29
|
{ key: 'github-repo', kind: 'human', label: adopt ? 'Use your EXISTING GitHub repository' : 'Create the empty GitHub repository' },
|
|
@@ -31,6 +37,9 @@ function onboardPlan(mode = 'greenfield') {
|
|
|
31
37
|
{ key: 'first-sign-in', kind: 'human', label: 'First sign-in (seats the founding Archon)' },
|
|
32
38
|
{ key: 'verify', kind: 'auto', label: 'Verify /healthz auth_configured' },
|
|
33
39
|
{ key: 'brand', kind: 'auto', label: 'Brand the instance look' },
|
|
40
|
+
{ key: 'enter-hall', kind: 'human', label: domain
|
|
41
|
+
? `Enter your builders hall — https://${domain}/builders`
|
|
42
|
+
: 'Enter your builders hall — run `bongos dev` for the local hall (no domain attached yet)' },
|
|
34
43
|
];
|
|
35
44
|
}
|
|
36
45
|
|
|
@@ -609,16 +609,20 @@ module.exports = function provisioningRoutes() {
|
|
|
609
609
|
res.json({ module: 'provisioning', ok: true, coreVersion: api.CORE_VERSION });
|
|
610
610
|
});
|
|
611
611
|
|
|
612
|
-
// GET /provisioning/onboard-plan?mode=greenfield|adopt — the CANONICAL
|
|
613
|
-
// step sequence (task 2085; goal 35 / criterion 165: "the canonical step
|
|
614
|
-
// the source of truth; skill/CLI/hall are forms over it"). The branded
|
|
615
|
-
// the hall wizard fetch it so their human-steps checklist can't drift
|
|
616
|
-
// (scripts/gds/onboard.js reads the same
|
|
612
|
+
// GET /provisioning/onboard-plan?mode=greenfield|adopt&domain=<host> — the CANONICAL
|
|
613
|
+
// onboarding step sequence (task 2085; goal 35 / criterion 165: "the canonical step
|
|
614
|
+
// sequence is the source of truth; skill/CLI/hall are forms over it"). The branded
|
|
615
|
+
// /create page + the hall wizard fetch it so their human-steps checklist can't drift
|
|
616
|
+
// from the CLI's (scripts/gds/onboard.js reads the same
|
|
617
|
+
// modules/provisioning/onboard-plan.js). `domain` relabels the terminal enter-hall
|
|
618
|
+
// step with the instance's own hall address (task 1002703); a non-hostname is dropped
|
|
619
|
+
// — clients render these labels, so unvalidated input must never be echoed back.
|
|
617
620
|
// rank: public — the signed-out /create landing page reads it; it is the same public
|
|
618
621
|
// runbook the /new-project skill documents (no write, no secret).
|
|
619
622
|
router.get('/provisioning/onboard-plan', (req, res) => {
|
|
620
623
|
const mode = req.query.mode === 'adopt' ? 'adopt' : 'greenfield';
|
|
621
|
-
|
|
624
|
+
const domain = provisioning.normalizeHostname(req.query.domain) || null;
|
|
625
|
+
res.json({ mode, steps: onboardPlan(mode, { domain }) });
|
|
622
626
|
});
|
|
623
627
|
|
|
624
628
|
// GET /provisioning/tls-check?domain=<host> — the Caddy on-demand-TLS ALLOW-LIST
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.67",
|
|
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.67",
|
|
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.67",
|
|
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",
|
|
@@ -1007,7 +1007,7 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
1007
1007
|
: '<li data-onboard-step="github-repo"><b>Create the empty GitHub repo</b><p>Do this first — the build needs the repo to exist. Click below (GitHub opens with the name already filled in), press <strong>Create repository</strong>, and leave it completely empty. Seeing “repository not found” above? That just means the repo isn’t created yet — make it here.</p>' +
|
|
1008
1008
|
'<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + ghIcon + 'Create ' + repoLabel + ' on GitHub</a></li>'));
|
|
1009
1009
|
if (!state.domain) {
|
|
1010
|
-
steps.push('<li><b>Add a domain you own</b><p>Sign-in on your new instance needs a real domain. When you have one,
|
|
1010
|
+
steps.push('<li><b>Add a domain you own</b><p>Sign-in on your new instance needs a real domain. When you have one, attach it from <a href="/projects">your projects page</a> — the address gets wired in minutes.</p></li>');
|
|
1011
1011
|
}
|
|
1012
1012
|
if (oa) {
|
|
1013
1013
|
steps.push('<li data-onboard-step="oauth-app"><b>Create the GitHub OAuth app</b><p>GitHub → <em>your account’s</em> Settings → Developer settings → OAuth Apps → New OAuth App. (Developer settings live under your account, not the repo.)</p>' +
|
|
@@ -1019,6 +1019,9 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
1019
1019
|
steps.push('<li data-onboard-step="first-sign-in"><b>First sign-in</b><p>Open <code style="font-family:var(--font-mono)">' + esc(oa.homepageUrl) + '</code> and sign in with GitHub once. That seats you as the owner — the founding Archon.</p></li>');
|
|
1020
1020
|
steps.push('<li><b>Verify</b><p><code style="font-family:var(--font-mono)">' + esc(oa.homepageUrl) + '/healthz</code> should report <code style="font-family:var(--font-mono)">auth_configured: true</code>. Then the hall is yours.</p></li>');
|
|
1021
1021
|
}
|
|
1022
|
+
steps.push(state.domain
|
|
1023
|
+
? '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>Your project’s home — open <a href="https://' + esc(state.domain) + '/builders" target="_blank" rel="noopener"><code style="font-family:var(--font-mono)">' + esc(state.domain) + '/builders</code></a> and start building.</p></li>'
|
|
1024
|
+
: '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>No address yet — run <code style="font-family:var(--font-mono)">bongos dev</code> in your repo checkout to start the local hall on your machine.</p></li>');
|
|
1022
1025
|
$('humanSteps').innerHTML = steps.join('');
|
|
1023
1026
|
$('humanSteps').addEventListener('click', function (ev) {
|
|
1024
1027
|
var t = ev.target.closest('.copy');
|
|
@@ -1079,7 +1082,16 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
1079
1082
|
el.innerHTML = progressMarkup(0, 1, 'Filed. Watching for the standup to start…');
|
|
1080
1083
|
function tick() {
|
|
1081
1084
|
ticks += 1;
|
|
1082
|
-
if (ticks > 400) {
|
|
1085
|
+
if (ticks > 400) {
|
|
1086
|
+
/* ~33 min: this tab stops watching, but the standup keeps going — hand
|
|
1087
|
+
the long watch to /projects, the owner's durable surface (task
|
|
1088
|
+
1002693). The old "stop quietly" here was a dead-end lie: the hall
|
|
1089
|
+
card it assumed was removed in task 1002675. */
|
|
1090
|
+
$('liveProgress').innerHTML = progressMarkup(1, 1,
|
|
1091
|
+
'Still building — this tab will stop watching now. Follow it any time on <a href="/projects">your projects page</a>.');
|
|
1092
|
+
stopPolling();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1083
1095
|
fetch(API + '/provisioning/instances/' + encodeURIComponent(id), { credentials: 'same-origin' })
|
|
1084
1096
|
.then(function (res) {
|
|
1085
1097
|
if (res.status === 401) { throw new Error('gone:Your session expired — <a href="' + API + '/auth/web/start?return=/create">sign in again</a> to keep watching.'); }
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Your projects — Cloud Bongos</title>
|
|
7
|
+
<meta name="description" content="Every Cloud Bongos project you own — status, address, and the controls that matter.">
|
|
8
|
+
<!-- Fonts: the production instance also injects these from the branding pack
|
|
9
|
+
(config/branding.json theme.ui.fonts.links); kept here so the page reads
|
|
10
|
+
correctly when viewed standalone. -->
|
|
11
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
12
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
13
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700&family=Quicksand:wght@400;500;600;700&display=swap">
|
|
14
|
+
<style>
|
|
15
|
+
/* ── Cloud Bongos vanilla look (ADR 0064) — same baked defaults as index.html /
|
|
16
|
+
create.html; the server re-injects the branding vars after this stylesheet so
|
|
17
|
+
the pack stays authoritative. /projects is the owner's my-projects surface
|
|
18
|
+
(goal 1000059, task 1002693): the durable place to find, watch, and manage
|
|
19
|
+
your instances after the /create tab closes. Functional-first against the
|
|
20
|
+
current look (the R07 redesign applies later). Same trust boundary as
|
|
21
|
+
/create — every button here only ASKS (enqueues an intent); the control-plane
|
|
22
|
+
runner does the real infra work (ADR 0111 §2). ── */
|
|
23
|
+
:root{
|
|
24
|
+
--bg:#fff7ed; --bg-deep:#ffedd5;
|
|
25
|
+
--ink:#2a1f17; --ink-soft:#6b5d52; --ink-faint:#9c8d80;
|
|
26
|
+
--accent:#f97316; --accent-deep:#ea580c; --sea:#38bdf8; --terracotta:#fb7185;
|
|
27
|
+
--card:#fffdfa; --line:#f3e1c9; --ok:#16a34a; --err:#dc2626;
|
|
28
|
+
--font-display:"Baloo 2","Quicksand",system-ui,"Arial Rounded MT Bold",sans-serif;
|
|
29
|
+
--font-body:"Quicksand",system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
30
|
+
--font-mono:"Fira Code",ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
31
|
+
}
|
|
32
|
+
*{box-sizing:border-box;margin:0;padding:0;}
|
|
33
|
+
[hidden]{display:none !important;}
|
|
34
|
+
html{scroll-behavior:smooth;}
|
|
35
|
+
body{
|
|
36
|
+
font-family:var(--font-body); color:var(--ink);
|
|
37
|
+
background:var(--bg);
|
|
38
|
+
min-height:100vh; -webkit-font-smoothing:antialiased; overflow-x:hidden;
|
|
39
|
+
}
|
|
40
|
+
/* the sky — same fixed dawn layer as /create (gradient here, not on body:
|
|
41
|
+
fixed-attachment backgrounds black out on scrolled composited pages) */
|
|
42
|
+
.sky{
|
|
43
|
+
position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden;
|
|
44
|
+
background:linear-gradient(180deg,#e9f5fd 0%,#fde8c8 34%,#fff4e6 62%,#fffdfa 100%);
|
|
45
|
+
}
|
|
46
|
+
.cloud{position:absolute;border-radius:50%;background:rgba(255,255,255,0.55);filter:blur(38px);}
|
|
47
|
+
.cloud.a{width:380px;height:380px;top:-120px;left:-80px;animation:drift 26s ease-in-out infinite alternate;}
|
|
48
|
+
.cloud.b{width:300px;height:300px;top:4%;right:-60px;background:rgba(255,224,178,0.5);animation:drift 32s ease-in-out infinite alternate-reverse;}
|
|
49
|
+
.cloud.c{width:460px;height:460px;bottom:-200px;left:24%;background:rgba(255,255,255,0.4);animation:drift 40s ease-in-out infinite alternate;}
|
|
50
|
+
@keyframes drift{from{transform:translateX(-18px)}to{transform:translateX(26px)}}
|
|
51
|
+
|
|
52
|
+
/* ── header ── */
|
|
53
|
+
.top{
|
|
54
|
+
position:relative;z-index:2;display:flex;align-items:center;justify-content:space-between;
|
|
55
|
+
padding:20px clamp(18px,4vw,40px);
|
|
56
|
+
}
|
|
57
|
+
.top .mark{
|
|
58
|
+
font-family:var(--font-display);font-weight:700;font-size:22px;letter-spacing:-0.5px;
|
|
59
|
+
text-transform:lowercase;color:var(--ink);text-decoration:none;
|
|
60
|
+
}
|
|
61
|
+
.top .mark span{color:var(--accent);}
|
|
62
|
+
.top-right{display:flex;align-items:center;gap:clamp(10px,2vw,18px);}
|
|
63
|
+
.who{display:flex;align-items:center;gap:10px;font-size:14px;font-weight:600;color:var(--ink-soft);min-height:34px;}
|
|
64
|
+
.who img{width:30px;height:30px;border-radius:50%;border:2px solid #fff;box-shadow:0 2px 8px rgba(42,31,23,0.12);}
|
|
65
|
+
|
|
66
|
+
/* ── layout ── */
|
|
67
|
+
.wrap{
|
|
68
|
+
position:relative;z-index:1;max-width:880px;margin:0 auto;
|
|
69
|
+
padding:clamp(8px,2vw,28px) clamp(16px,4vw,40px) 80px;
|
|
70
|
+
}
|
|
71
|
+
.card{
|
|
72
|
+
background:var(--card);border-radius:26px;
|
|
73
|
+
box-shadow:0 24px 60px -24px rgba(234,88,12,0.22), 0 6px 22px -8px rgba(42,31,23,0.10);
|
|
74
|
+
padding:clamp(22px,3.6vw,40px);
|
|
75
|
+
animation:rise .7s ease-out both;
|
|
76
|
+
}
|
|
77
|
+
@keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
|
|
78
|
+
h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32px);letter-spacing:-0.5px;line-height:1.15;margin-bottom:8px;}
|
|
79
|
+
.lede{font-size:15.5px;color:var(--ink-soft);font-weight:500;line-height:1.55;margin-bottom:24px;max-width:60ch;}
|
|
80
|
+
.lede a,.projMsg a,.pnote a{color:var(--accent-deep);font-weight:700;text-decoration:none;}
|
|
81
|
+
.lede a:hover,.projMsg a:hover,.pnote a:hover{text-decoration:underline;}
|
|
82
|
+
|
|
83
|
+
/* ── buttons ── */
|
|
84
|
+
.btn{
|
|
85
|
+
display:inline-flex;align-items:center;justify-content:center;gap:9px;cursor:pointer;
|
|
86
|
+
font-family:var(--font-display);font-weight:700;font-size:15px;color:#fff;
|
|
87
|
+
background:var(--accent);border:none;border-radius:999px;padding:11px 22px;text-decoration:none;
|
|
88
|
+
box-shadow:0 10px 22px -8px rgba(249,115,22,0.7);
|
|
89
|
+
transition:transform .15s ease, box-shadow .15s ease, background .15s ease;
|
|
90
|
+
}
|
|
91
|
+
.btn:hover{transform:translateY(-1px);box-shadow:0 14px 28px -8px rgba(249,115,22,0.8);}
|
|
92
|
+
.btn:disabled{opacity:0.6;cursor:default;transform:none;}
|
|
93
|
+
.btn.ghost{background:transparent;color:var(--ink-soft);box-shadow:none;border:2px solid var(--line);}
|
|
94
|
+
.btn.ghost:hover{border-color:var(--accent);color:var(--accent-deep);}
|
|
95
|
+
.btn.small{font-size:13.5px;padding:8px 16px;}
|
|
96
|
+
.btn.danger{background:transparent;color:var(--err);box-shadow:none;border:2px solid rgba(220,38,38,0.35);}
|
|
97
|
+
.btn.danger:hover{background:rgba(220,38,38,0.06);border-color:var(--err);}
|
|
98
|
+
.btn .gh{width:17px;height:17px;fill:currentColor;}
|
|
99
|
+
.linklike{background:none;border:none;cursor:pointer;font-family:var(--font-body);font-weight:700;font-size:13.5px;color:var(--accent-deep);text-decoration:underline;padding:6px 4px;}
|
|
100
|
+
|
|
101
|
+
/* ── project cards ── */
|
|
102
|
+
.projects{list-style:none;display:flex;flex-direction:column;gap:18px;}
|
|
103
|
+
.proj{
|
|
104
|
+
border:2px solid var(--line);border-radius:18px;padding:18px 20px;background:#fff;
|
|
105
|
+
}
|
|
106
|
+
.proj.gone{opacity:0.62;}
|
|
107
|
+
.projHead{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
|
|
108
|
+
.projName{font-family:var(--font-display);font-weight:700;font-size:19px;letter-spacing:-0.3px;}
|
|
109
|
+
.chip{
|
|
110
|
+
font-size:12px;font-weight:700;letter-spacing:0.3px;border-radius:999px;padding:3px 11px;
|
|
111
|
+
background:var(--bg-deep);color:var(--ink-soft);
|
|
112
|
+
}
|
|
113
|
+
.chip.live{background:rgba(22,163,74,0.13);color:var(--ok);}
|
|
114
|
+
.chip.busy{background:rgba(249,115,22,0.14);color:var(--accent-deep);}
|
|
115
|
+
.chip.bad{background:rgba(220,38,38,0.11);color:var(--err);}
|
|
116
|
+
.projMeta{font-size:13px;font-weight:600;color:var(--ink-faint);margin-top:6px;display:flex;gap:14px;flex-wrap:wrap;}
|
|
117
|
+
.projMeta code{font-family:var(--font-mono);font-size:12.5px;background:var(--bg-deep);border-radius:6px;padding:1px 6px;color:var(--accent-deep);}
|
|
118
|
+
.addr{margin-top:12px;font-size:14.5px;font-weight:600;color:var(--ink-soft);}
|
|
119
|
+
.addr a{color:var(--accent-deep);font-weight:700;text-decoration:none;}
|
|
120
|
+
.addr a:hover{text-decoration:underline;}
|
|
121
|
+
.addr .none{color:var(--ink-faint);font-style:italic;}
|
|
122
|
+
.attach{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap;}
|
|
123
|
+
.attach input[type=text]{
|
|
124
|
+
flex:1;min-width:220px;font-family:var(--font-body);font-size:15px;font-weight:600;color:var(--ink);
|
|
125
|
+
background:#fff;border:2px solid var(--line);border-radius:12px;padding:9px 14px;outline:none;
|
|
126
|
+
transition:border-color .15s ease, box-shadow .15s ease;
|
|
127
|
+
}
|
|
128
|
+
.attach input[type=text]::placeholder{color:#c9b8a6;font-weight:500;}
|
|
129
|
+
.attach input[type=text]:focus{border-color:var(--accent);box-shadow:0 0 0 4px rgba(249,115,22,0.13);}
|
|
130
|
+
.projMsg{font-size:13.5px;font-weight:600;margin-top:10px;min-height:18px;color:var(--ink-faint);}
|
|
131
|
+
.projMsg.ok{color:var(--ok);}
|
|
132
|
+
.projMsg.err{color:var(--err);}
|
|
133
|
+
.actions{display:flex;gap:10px;margin-top:14px;flex-wrap:wrap;align-items:center;}
|
|
134
|
+
.confirmRow{display:flex;gap:10px;align-items:center;flex-wrap:wrap;font-size:13.5px;font-weight:700;color:var(--err);}
|
|
135
|
+
|
|
136
|
+
/* live standup watch — the /create stage line, per card */
|
|
137
|
+
.prog{margin-top:12px;border:2px solid var(--line);border-radius:14px;padding:12px 16px;background:var(--bg);}
|
|
138
|
+
.prog.isLive{border-color:rgba(22,163,74,0.4);}
|
|
139
|
+
.prog.isErr{border-color:rgba(220,38,38,0.35);}
|
|
140
|
+
.stages{list-style:none;display:flex;gap:0;margin-bottom:8px;}
|
|
141
|
+
.stages li{flex:1;display:flex;align-items:center;gap:7px;font-size:12.5px;font-weight:700;color:var(--ink-faint);}
|
|
142
|
+
.stages .sdot{width:11px;height:11px;border-radius:50%;background:var(--line);flex:0 0 auto;}
|
|
143
|
+
.stages li.done{color:var(--ink-soft);}
|
|
144
|
+
.stages li.done .sdot{background:var(--ok);}
|
|
145
|
+
.stages li.now{color:var(--accent-deep);}
|
|
146
|
+
.stages li.now .sdot{background:var(--accent);animation:pulseDot 1.4s ease-in-out infinite;}
|
|
147
|
+
@keyframes pulseDot{0%,100%{box-shadow:0 0 0 0 rgba(249,115,22,0.35)}50%{box-shadow:0 0 0 6px rgba(249,115,22,0.06)}}
|
|
148
|
+
@media (prefers-reduced-motion:reduce){.stages li.now .sdot{animation:none}}
|
|
149
|
+
.pnote{font-size:13px;font-weight:600;color:var(--ink-soft);line-height:1.5;}
|
|
150
|
+
|
|
151
|
+
/* ── page-level states ── */
|
|
152
|
+
.alert{
|
|
153
|
+
display:none;background:rgba(220,38,38,0.07);border:2px solid rgba(220,38,38,0.25);
|
|
154
|
+
color:var(--err);font-weight:600;font-size:14px;border-radius:14px;padding:12px 16px;margin-bottom:18px;
|
|
155
|
+
}
|
|
156
|
+
.alert.on{display:block;}
|
|
157
|
+
.loading{font-size:14.5px;font-weight:600;color:var(--ink-faint);padding:14px 0;}
|
|
158
|
+
.empty{padding:10px 0 4px;}
|
|
159
|
+
.empty p{font-size:15px;font-weight:600;color:var(--ink-soft);margin-bottom:16px;max-width:52ch;line-height:1.55;}
|
|
160
|
+
.finePrint{font-size:12.5px;color:var(--ink-faint);font-weight:600;margin-top:16px;line-height:1.5;}
|
|
161
|
+
|
|
162
|
+
.foot{
|
|
163
|
+
position:relative;z-index:1;text-align:center;font-size:13px;font-weight:600;color:var(--ink-faint);
|
|
164
|
+
padding:0 20px 34px;
|
|
165
|
+
}
|
|
166
|
+
@media (max-width:640px){
|
|
167
|
+
.projHead{gap:8px;}
|
|
168
|
+
.actions{flex-direction:column;align-items:stretch;}
|
|
169
|
+
}
|
|
170
|
+
</style>
|
|
171
|
+
</head>
|
|
172
|
+
<body>
|
|
173
|
+
<div class="sky" aria-hidden="true">
|
|
174
|
+
<div class="cloud a"></div><div class="cloud b"></div><div class="cloud c"></div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<header class="top">
|
|
178
|
+
<a class="mark" href="/" data-brand-product>cloud bongos</a>
|
|
179
|
+
<div class="top-right">
|
|
180
|
+
<a class="btn small ghost" href="/create">+ New project</a>
|
|
181
|
+
<div class="who" id="who"></div>
|
|
182
|
+
</div>
|
|
183
|
+
</header>
|
|
184
|
+
|
|
185
|
+
<div class="wrap">
|
|
186
|
+
<main class="card">
|
|
187
|
+
<!-- signed-out gate: identity-only sign-in (ADR 0143 least-privilege — unlike
|
|
188
|
+
/create, listing your own instances needs no repo scope) -->
|
|
189
|
+
<section id="gate" hidden>
|
|
190
|
+
<h1>Your projects</h1>
|
|
191
|
+
<p class="lede">Every project you own, in one place — its live status, its address, and the controls that matter. Sign in with GitHub to see yours.</p>
|
|
192
|
+
<a class="btn" id="signin" href="/api/bongos/auth/web/start?return=/projects">
|
|
193
|
+
<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>
|
|
194
|
+
Sign in with GitHub
|
|
195
|
+
</a>
|
|
196
|
+
<div class="alert" id="gateAlert" role="alert"></div>
|
|
197
|
+
</section>
|
|
198
|
+
|
|
199
|
+
<!-- signed-in: the list -->
|
|
200
|
+
<section id="mine" hidden>
|
|
201
|
+
<h1>Your projects</h1>
|
|
202
|
+
<p class="lede">Everything you own, watched live. Standups keep running even with this tab closed — come back any time.</p>
|
|
203
|
+
<div class="alert" id="pageAlert" role="alert"></div>
|
|
204
|
+
<div class="loading" id="loading">Reading your projects…</div>
|
|
205
|
+
<section class="empty" id="empty" hidden>
|
|
206
|
+
<p>No projects yet. Your first one is a few minutes of gentle drumming away — its own repository, its own home on the web, built by agents.</p>
|
|
207
|
+
<a class="btn" href="/create">Create your first project</a>
|
|
208
|
+
</section>
|
|
209
|
+
<ul class="projects" id="projectsList"></ul>
|
|
210
|
+
<p class="finePrint">Buttons here only <em>ask</em> — teardown and address changes are queued for the platform's own runner; nothing touches servers from this page.</p>
|
|
211
|
+
</section>
|
|
212
|
+
</main>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div class="foot">A non-profit, capture-resistant commons. Built by agents, on Cloud Bongos.</div>
|
|
216
|
+
|
|
217
|
+
<script>
|
|
218
|
+
/* Brand-pack fill (R57): the server injects window.__BRANDING__ ahead of this
|
|
219
|
+
script; baked markup above is the standalone fallback. */
|
|
220
|
+
(function () {
|
|
221
|
+
'use strict';
|
|
222
|
+
|
|
223
|
+
var API = '/api/bongos';
|
|
224
|
+
var POLL_MS = 5000; /* the create.html watch cadence */
|
|
225
|
+
var POLL_SOFT_MS = 10000; /* transient failure back-off */
|
|
226
|
+
|
|
227
|
+
var b = (typeof window !== 'undefined' && window.__BRANDING__) || {};
|
|
228
|
+
var productName = (b.identity && b.identity.productName) || 'Cloud Bongos';
|
|
229
|
+
document.querySelectorAll('[data-brand-product]').forEach(function (n) { n.textContent = productName.toLowerCase(); });
|
|
230
|
+
|
|
231
|
+
function $(id) { return document.getElementById(id); }
|
|
232
|
+
function esc(s) {
|
|
233
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
234
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
var me = null;
|
|
239
|
+
var byId = {}; /* id → last-seen instance (+open_intent) */
|
|
240
|
+
var timers = {}; /* id → watch timer */
|
|
241
|
+
var lastMsg = {}; /* id → {text, cls} — survives a card re-render */
|
|
242
|
+
|
|
243
|
+
/* ── API plumbing ─────────────────────────────────────────────────────── */
|
|
244
|
+
|
|
245
|
+
/* Parse the error envelope truthfully: { error: { code, message, details } }.
|
|
246
|
+
Every refusal the provisioning routes give is SAID to the owner — the
|
|
247
|
+
1002691/1002692 rule: never a silent swallow, never a false success. */
|
|
248
|
+
function apiError(data, status) {
|
|
249
|
+
var e = (data && data.error) || {};
|
|
250
|
+
return { code: e.code || ('http_' + status), message: e.message || '', details: e.details || e || {} };
|
|
251
|
+
}
|
|
252
|
+
function jsonOrEmpty(res) { return res.json().catch(function () { return {}; }); }
|
|
253
|
+
|
|
254
|
+
function signInAgain() {
|
|
255
|
+
return 'Your session expired — <a href="' + API + '/auth/web/start?return=/projects">sign in again</a>.';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* ── status vocabulary ───────────────────────────────────────────────── */
|
|
259
|
+
|
|
260
|
+
var CHIP = {
|
|
261
|
+
requested: { cls: 'busy', label: 'queued' },
|
|
262
|
+
provisioning: { cls: 'busy', label: 'standing up' },
|
|
263
|
+
active: { cls: 'live', label: 'live' },
|
|
264
|
+
error: { cls: 'bad', label: 'snagged' },
|
|
265
|
+
tearing_down: { cls: 'busy', label: 'tearing down' },
|
|
266
|
+
torn_down: { cls: '', label: 'torn down' }
|
|
267
|
+
};
|
|
268
|
+
/* client mirror of provisioning.TEARDOWN_FROM minus tearing_down (already
|
|
269
|
+
going); no button either while a teardown intent is already queued */
|
|
270
|
+
function canTeardown(inst) {
|
|
271
|
+
if (inst.open_intent && inst.open_intent.action === 'teardown') return false;
|
|
272
|
+
return inst.status === 'provisioning' || inst.status === 'active';
|
|
273
|
+
}
|
|
274
|
+
/* client mirror of provisioning.DOMAIN_ATTACH_FROM */
|
|
275
|
+
function canAttachDomain(inst) { return !inst.domain && (inst.status === 'requested' || inst.status === 'error' || inst.status === 'active'); }
|
|
276
|
+
/* keep watching while the platform is doing something to this instance */
|
|
277
|
+
function shouldWatch(inst) {
|
|
278
|
+
if (inst.status === 'requested' || inst.status === 'provisioning' || inst.status === 'tearing_down' || inst.status === 'error') return true;
|
|
279
|
+
return !!inst.open_intent; /* e.g. a queued teardown or domain-attach on an active instance */
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* ── the live standup watch (the create.html stage line, per card) ────── */
|
|
283
|
+
|
|
284
|
+
function progressMarkup(stageNow, stagesDone, note, cls) {
|
|
285
|
+
var names = ['Request filed', 'Standing it up', 'Live'];
|
|
286
|
+
return '<div class="prog ' + (cls || '') + '" role="status" aria-live="polite">' +
|
|
287
|
+
'<ul class="stages">' + names.map(function (n, i) {
|
|
288
|
+
var c = stagesDone > i ? 'done' : (stageNow === i ? 'now' : '');
|
|
289
|
+
return '<li class="' + c + '"><span class="sdot"></span><span class="slbl">' + n + '</span></li>';
|
|
290
|
+
}).join('') + '</ul>' +
|
|
291
|
+
'<p class="pnote">' + note + '</p></div>';
|
|
292
|
+
}
|
|
293
|
+
function progressFor(inst) {
|
|
294
|
+
var s = inst.status;
|
|
295
|
+
if (s === 'requested') return progressMarkup(1, 1, 'Waiting for a runner to pick it up — usually under a minute.');
|
|
296
|
+
if (s === 'provisioning') return progressMarkup(1, 1, 'Raising the server, database, and address. A few minutes, nothing needed from you.');
|
|
297
|
+
if (s === 'error') return progressMarkup(1, 1, 'The standup hit a snag' + (inst.error_note ? ': ' + esc(inst.error_note) : '') + '. The platform keeps retrying — this card updates as it recovers.', 'isErr');
|
|
298
|
+
if (s === 'tearing_down') return progressMarkup(1, 0, 'Tearing it down now — the card settles when it’s done.', 'isErr');
|
|
299
|
+
if (s === 'active' && inst.open_intent) {
|
|
300
|
+
var a = inst.open_intent.action;
|
|
301
|
+
var doing = a === 'teardown' ? 'A teardown is queued' : a === 'domain-attach' ? 'Its new address is being wired' : 'A ' + esc(a) + ' run is queued';
|
|
302
|
+
return progressMarkup(2, 2, doing + ' — this card updates when it lands.');
|
|
303
|
+
}
|
|
304
|
+
return '';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* ── rendering ───────────────────────────────────────────────────────── */
|
|
308
|
+
|
|
309
|
+
function chipFor(inst) {
|
|
310
|
+
var c = CHIP[inst.status] || { cls: '', label: inst.status };
|
|
311
|
+
return '<span class="chip ' + c.cls + '">' + esc(c.label) + '</span>';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function addressRow(inst) {
|
|
315
|
+
if (inst.domain) {
|
|
316
|
+
if (inst.status === 'active') {
|
|
317
|
+
return 'Address: <a href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">' + esc(inst.domain) + '</a>';
|
|
318
|
+
}
|
|
319
|
+
return 'Address: <code>' + esc(inst.domain) + '</code> — it answers there once the instance is live.';
|
|
320
|
+
}
|
|
321
|
+
/* a torn-down instance has no address to speak of — the card's own line says so */
|
|
322
|
+
if (inst.status === 'torn_down' || inst.status === 'tearing_down') return '';
|
|
323
|
+
/* the HONEST no-address state: sign-in on the instance needs a real domain */
|
|
324
|
+
return '<span class="none">No address yet — sign-in on your instance only works once it has a domain.</span>';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function renderCard(inst) {
|
|
328
|
+
/* ids are server-generated, but escape anyway — every interpolated field in
|
|
329
|
+
this file goes through esc(), no exceptions (the grader's rule is right) */
|
|
330
|
+
var iid = esc(String(inst.id));
|
|
331
|
+
var li = document.querySelector('[data-inst="' + iid + '"]');
|
|
332
|
+
if (!li) return;
|
|
333
|
+
var gone = inst.status === 'torn_down';
|
|
334
|
+
li.className = 'proj' + (gone ? ' gone' : '');
|
|
335
|
+
var meta = [];
|
|
336
|
+
if (inst.target_ref) meta.push('repo <code>' + esc(inst.target_ref) + '</code>');
|
|
337
|
+
if (inst.hosting_shape) meta.push(esc(inst.hosting_shape));
|
|
338
|
+
if (inst.onboard_mode === 'adopt') meta.push('adopted repo');
|
|
339
|
+
var html =
|
|
340
|
+
'<div class="projHead"><span class="projName">' + esc(inst.slug) + '</span>' + chipFor(inst) + '</div>' +
|
|
341
|
+
(meta.length ? '<div class="projMeta">' + meta.join('<span aria-hidden="true">·</span>') + '</div>' : '');
|
|
342
|
+
var addr = addressRow(inst);
|
|
343
|
+
if (addr) html += '<div class="addr">' + addr + '</div>';
|
|
344
|
+
if (canAttachDomain(inst)) {
|
|
345
|
+
html += '<div class="attach">' +
|
|
346
|
+
'<input type="text" id="dom-' + iid + '" placeholder="myproject.example.com" autocomplete="off" spellcheck="false" maxlength="253">' +
|
|
347
|
+
'<button type="button" class="btn small" data-attach="' + iid + '">Attach address</button></div>';
|
|
348
|
+
}
|
|
349
|
+
html += progressFor(inst);
|
|
350
|
+
if (gone) {
|
|
351
|
+
html += '<div class="projMsg">Torn down' + (inst.torn_down_at ? ' ' + esc(new Date(inst.torn_down_at).toLocaleDateString()) : '') + '. Its handle stays yours — <a href="/create">start another</a> whenever you like.</div>';
|
|
352
|
+
}
|
|
353
|
+
var lm = lastMsg[inst.id];
|
|
354
|
+
html += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '">' + (lm ? lm.text : '') + '</div>';
|
|
355
|
+
if (canTeardown(inst)) {
|
|
356
|
+
html += '<div class="actions" id="act-' + iid + '">' +
|
|
357
|
+
'<button type="button" class="btn small danger" data-teardown="' + iid + '">Tear it down</button></div>';
|
|
358
|
+
}
|
|
359
|
+
li.innerHTML = html;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function render(instances) {
|
|
363
|
+
$('loading').hidden = true;
|
|
364
|
+
var list = $('projectsList');
|
|
365
|
+
byId = {};
|
|
366
|
+
if (!instances.length) {
|
|
367
|
+
$('empty').hidden = false;
|
|
368
|
+
list.innerHTML = '';
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
$('empty').hidden = true;
|
|
372
|
+
list.innerHTML = instances.map(function (inst) {
|
|
373
|
+
return '<li class="proj" data-inst="' + esc(String(inst.id)) + '"></li>';
|
|
374
|
+
}).join('');
|
|
375
|
+
instances.forEach(function (inst) {
|
|
376
|
+
byId[inst.id] = inst;
|
|
377
|
+
renderCard(inst);
|
|
378
|
+
if (shouldWatch(inst)) watch(inst.id);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function msg(id, text, cls) {
|
|
383
|
+
lastMsg[id] = text ? { text: text, cls: cls || '' } : null;
|
|
384
|
+
var el = $('msg-' + id);
|
|
385
|
+
if (!el) return;
|
|
386
|
+
el.className = 'projMsg' + (cls ? ' ' + cls : '');
|
|
387
|
+
el.innerHTML = text;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* ── the watch: resume the standup poll for any in-flight instance ────── */
|
|
391
|
+
|
|
392
|
+
function stopWatch(id) { if (timers[id]) { clearTimeout(timers[id]); delete timers[id]; } }
|
|
393
|
+
/* What the card RENDERS from — re-render only when this changes, so a quiet
|
|
394
|
+
poll tick never clobbers in-flight UI (an open teardown confirm, a
|
|
395
|
+
half-typed domain, a fresh action message). */
|
|
396
|
+
function sig(inst) {
|
|
397
|
+
return JSON.stringify([inst.status, inst.domain, inst.error_note,
|
|
398
|
+
inst.open_intent && inst.open_intent.action, inst.open_intent && inst.open_intent.state]);
|
|
399
|
+
}
|
|
400
|
+
function takeUpdate(id, inst) {
|
|
401
|
+
var changed = !byId[id] || sig(byId[id]) !== sig(inst);
|
|
402
|
+
byId[id] = inst;
|
|
403
|
+
if (changed) renderCard(inst);
|
|
404
|
+
}
|
|
405
|
+
function watch(id) {
|
|
406
|
+
stopWatch(id);
|
|
407
|
+
function tick() {
|
|
408
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(id), { credentials: 'same-origin' })
|
|
409
|
+
.then(function (res) {
|
|
410
|
+
if (res.status === 401) { throw new Error('gone:' + signInAgain()); }
|
|
411
|
+
if (res.status === 404) { throw new Error('gone:Can’t see this project any more — it may have been torn down.'); }
|
|
412
|
+
if (!res.ok) throw new Error('soft');
|
|
413
|
+
return res.json();
|
|
414
|
+
})
|
|
415
|
+
.then(function (data) {
|
|
416
|
+
var inst = data.instance || {};
|
|
417
|
+
inst.open_intent = data.open_intent || null;
|
|
418
|
+
takeUpdate(id, inst);
|
|
419
|
+
if (shouldWatch(inst)) timers[id] = setTimeout(tick, POLL_MS);
|
|
420
|
+
else stopWatch(id);
|
|
421
|
+
})
|
|
422
|
+
.catch(function (err) {
|
|
423
|
+
var m = String(err && err.message || '');
|
|
424
|
+
if (m.indexOf('gone:') === 0) { msg(id, m.slice(5), 'err'); stopWatch(id); return; }
|
|
425
|
+
/* transient (network / 5xx): keep the last view, try again a bit later */
|
|
426
|
+
timers[id] = setTimeout(tick, POLL_SOFT_MS);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
timers[id] = setTimeout(tick, POLL_MS);
|
|
430
|
+
}
|
|
431
|
+
function watchOnce(id) {
|
|
432
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(id), { credentials: 'same-origin' })
|
|
433
|
+
.then(function (res) { if (!res.ok) throw new Error('soft'); return res.json(); })
|
|
434
|
+
.then(function (data) {
|
|
435
|
+
var inst = data.instance || {};
|
|
436
|
+
inst.open_intent = data.open_intent || null;
|
|
437
|
+
takeUpdate(id, inst);
|
|
438
|
+
if (shouldWatch(inst)) watch(id);
|
|
439
|
+
})
|
|
440
|
+
.catch(function () { if (byId[id] && shouldWatch(byId[id])) watch(id); });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* ── teardown: ask, confirm, then SAY exactly what the server said ────── */
|
|
444
|
+
|
|
445
|
+
function askTeardown(id) {
|
|
446
|
+
var act = $('act-' + id);
|
|
447
|
+
if (!act) return;
|
|
448
|
+
act.innerHTML = '<div class="confirmRow"><span>Really tear it down? The server and database go away; the repo stays yours.</span>' +
|
|
449
|
+
'<button type="button" class="btn small danger" data-teardown-yes="' + esc(id) + '">Yes, tear it down</button>' +
|
|
450
|
+
'<button type="button" class="linklike" data-teardown-no="' + esc(id) + '">Keep it</button></div>';
|
|
451
|
+
}
|
|
452
|
+
function unAskTeardown(id) { var inst = byId[id]; if (inst) renderCard(inst); }
|
|
453
|
+
function doTeardown(id) {
|
|
454
|
+
var act = $('act-' + id);
|
|
455
|
+
if (act) act.innerHTML = '<span class="projMsg">Asking…</span>';
|
|
456
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/teardown', {
|
|
457
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: '{}'
|
|
458
|
+
}).then(function (res) {
|
|
459
|
+
if (res.status === 401) { msg(id, signInAgain(), 'err'); return null; }
|
|
460
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
461
|
+
if (res.ok) {
|
|
462
|
+
/* truthful outcomes (task 1002691): queued:true = really queued;
|
|
463
|
+
queued:false = one was ALREADY queued; superseded = a stale
|
|
464
|
+
provision intent was cleared to make room */
|
|
465
|
+
var note = esc(data.message || 'Teardown requested.');
|
|
466
|
+
if (data.superseded) note += ' (A stale standup request was cleared to make room.)';
|
|
467
|
+
msg(id, note, 'ok');
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
var e = apiError(data, res.status);
|
|
471
|
+
if (e.code === 'intent_conflict') {
|
|
472
|
+
msg(id, esc(e.message || ('A ' + (e.details.action || 'previous') + ' run is still ' + (e.details.state || 'open') + ' — retry in a moment.')), 'err');
|
|
473
|
+
} else if (e.code === 'not_teardownable') {
|
|
474
|
+
msg(id, esc(e.message || ('Nothing to tear down right now — the instance is ' + (e.details.status || 'settled') + '.')), 'err');
|
|
475
|
+
} else {
|
|
476
|
+
msg(id, esc(e.message || 'The teardown could not be queued — try again in a moment.'), 'err');
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}).catch(function () {
|
|
480
|
+
msg(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
481
|
+
}).then(function () {
|
|
482
|
+
/* restore the actions row ("Asking…" replaced it) even when the state
|
|
483
|
+
didn't change, then pick up whatever DID change */
|
|
484
|
+
if (byId[id]) renderCard(byId[id]);
|
|
485
|
+
watchOnce(id);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* ── domain attach: same truth-telling for every refusal (task 1002692) ── */
|
|
490
|
+
|
|
491
|
+
function doAttach(id) {
|
|
492
|
+
var input = $('dom-' + id);
|
|
493
|
+
var domain = input ? String(input.value || '').trim() : '';
|
|
494
|
+
if (!domain) { msg(id, 'Type the domain you own first — e.g. <code>myproject.example.com</code>.', 'err'); return; }
|
|
495
|
+
msg(id, 'Recording the address…');
|
|
496
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/domain', {
|
|
497
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
498
|
+
body: JSON.stringify({ domain: domain })
|
|
499
|
+
}).then(function (res) {
|
|
500
|
+
if (res.status === 401) { msg(id, signInAgain(), 'err'); return null; }
|
|
501
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
502
|
+
if (res.ok) {
|
|
503
|
+
msg(id, esc(data.message || 'Address recorded.'), 'ok');
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
var e = apiError(data, res.status);
|
|
507
|
+
if (e.code === 'bad_domain') {
|
|
508
|
+
msg(id, 'That doesn’t look like a usable address — ' + esc(e.details.reason || 'use a real hostname like myproject.example.com') + '.', 'err');
|
|
509
|
+
} else if (e.code === 'domain_taken') {
|
|
510
|
+
msg(id, esc(e.message || 'That address is already in use by another project.'), 'err');
|
|
511
|
+
} else if (e.code === 'domain_already_set') {
|
|
512
|
+
msg(id, esc(e.message || 'This project already has an address — re-pointing isn’t supported here.'), 'err');
|
|
513
|
+
} else if (e.code === 'domain_not_attachable') {
|
|
514
|
+
msg(id, esc(e.message || 'An address can be attached once the instance settles.'), 'err');
|
|
515
|
+
} else if (e.code === 'intent_conflict') {
|
|
516
|
+
/* the address may still have been RECORDED (details.recorded) — say so */
|
|
517
|
+
var t = esc(e.message || 'Another run holds the queue right now — re-send once it settles.');
|
|
518
|
+
if (e.details.recorded) t += ' (The address itself is saved.)';
|
|
519
|
+
msg(id, t, 'err');
|
|
520
|
+
} else {
|
|
521
|
+
msg(id, esc(e.message || 'The address could not be recorded — try again in a moment.'), 'err');
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}).catch(function () {
|
|
525
|
+
msg(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
526
|
+
}).then(function () { watchOnce(id); });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/* one delegated listener for all card actions */
|
|
530
|
+
$('projectsList').addEventListener('click', function (ev) {
|
|
531
|
+
var t = ev.target.closest('button');
|
|
532
|
+
if (!t) return;
|
|
533
|
+
if (t.hasAttribute('data-teardown')) return askTeardown(t.getAttribute('data-teardown'));
|
|
534
|
+
if (t.hasAttribute('data-teardown-yes')) return doTeardown(t.getAttribute('data-teardown-yes'));
|
|
535
|
+
if (t.hasAttribute('data-teardown-no')) return unAskTeardown(t.getAttribute('data-teardown-no'));
|
|
536
|
+
if (t.hasAttribute('data-attach')) return doAttach(t.getAttribute('data-attach'));
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
/* ── boot: who am I → my instances ────────────────────────────────────── */
|
|
540
|
+
|
|
541
|
+
function renderWho() {
|
|
542
|
+
var w = $('who');
|
|
543
|
+
if (me) {
|
|
544
|
+
w.innerHTML = (me.avatar_url ? '<img src="' + esc(me.avatar_url) + '" alt="">' : '') +
|
|
545
|
+
'<span>' + esc(me.github_login || 'signed in') + '</span>';
|
|
546
|
+
} else {
|
|
547
|
+
w.innerHTML = '';
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function load() {
|
|
552
|
+
fetch(API + '/provisioning/instances', { credentials: 'same-origin' })
|
|
553
|
+
.then(function (res) {
|
|
554
|
+
if (res.status === 401) { $('mine').hidden = true; $('gate').hidden = false; throw new Error('gated'); }
|
|
555
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
556
|
+
return res.json();
|
|
557
|
+
})
|
|
558
|
+
.then(function (data) { render(data.instances || []); })
|
|
559
|
+
.catch(function (err) {
|
|
560
|
+
if (String(err && err.message) === 'gated') return;
|
|
561
|
+
$('loading').hidden = true;
|
|
562
|
+
var a = $('pageAlert');
|
|
563
|
+
a.textContent = 'Couldn’t read your projects right now — refresh in a moment.';
|
|
564
|
+
a.classList.add('on');
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
fetch(API + '/me', { credentials: 'same-origin' })
|
|
569
|
+
.then(function (res) { if (!res.ok) throw new Error(String(res.status)); return res.json(); })
|
|
570
|
+
.then(function (data) {
|
|
571
|
+
me = data.builder || data.me || data;
|
|
572
|
+
renderWho();
|
|
573
|
+
$('gate').hidden = true;
|
|
574
|
+
$('mine').hidden = false;
|
|
575
|
+
load();
|
|
576
|
+
})
|
|
577
|
+
.catch(function (err) {
|
|
578
|
+
renderWho();
|
|
579
|
+
$('gate').hidden = false;
|
|
580
|
+
if (String(err && err.message) !== '401') {
|
|
581
|
+
var g = $('gateAlert');
|
|
582
|
+
if (!navigator.onLine || String(err && err.message).indexOf('Failed') === 0) {
|
|
583
|
+
g.textContent = 'Can’t reach the platform right now — sign-in will work once you’re online with it.';
|
|
584
|
+
g.classList.add('on');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
})();
|
|
589
|
+
</script>
|
|
590
|
+
</body>
|
|
591
|
+
</html>
|
package/scripts/gds/onboard.js
CHANGED
|
@@ -199,6 +199,15 @@ async function run() {
|
|
|
199
199
|
|
|
200
200
|
// Step 10 — brand
|
|
201
201
|
log('\n⑨ Brand the instance look — fill theme/palette/fonts in config/branding.json (docs/branding-fill-prompt.md).');
|
|
202
|
+
|
|
203
|
+
// Step 11 — enter the builders hall (task 1002703): the point of all of the above.
|
|
204
|
+
// With a domain the hall lives on the instance; without one, `bongos dev` runs it locally.
|
|
205
|
+
if (a.domain) {
|
|
206
|
+
log(`\n⑩ Enter your builders hall — open https://${a.domain}/builders. That's your project's home.`);
|
|
207
|
+
} else {
|
|
208
|
+
log('\n⑩ Enter your builders hall — no domain yet, so run `bongos dev` in your repo checkout:');
|
|
209
|
+
log(' it stands the instance up locally and serves the hall on your machine.');
|
|
210
|
+
}
|
|
202
211
|
log('\n✓ onboard walkthrough complete. Your standalone instance is live and owned.\n');
|
|
203
212
|
} finally {
|
|
204
213
|
rl.close();
|
package/src/module-api.js
CHANGED
|
@@ -557,8 +557,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
557
557
|
// landed since 1.19.63 with no explicit bump. run 31560059785. (task 1002620)
|
|
558
558
|
// 1.19.65 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
559
559
|
// landed since 1.19.64 with no explicit bump. run 31562193292. (task 1002620)
|
|
560
|
+
// 1.19.66 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
561
|
+
// landed since 1.19.65 with no explicit bump. run 31599966075. (task 1002620)
|
|
562
|
+
// 1.19.67 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
563
|
+
// landed since 1.19.66 with no explicit bump. run 31605591253. (task 1002620)
|
|
560
564
|
// ---------------------------------------------------------------------------
|
|
561
|
-
const CORE_VERSION = '1.19.
|
|
565
|
+
const CORE_VERSION = '1.19.67'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
562
566
|
|
|
563
567
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
564
568
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/src/platform-server.js
CHANGED
|
@@ -143,6 +143,15 @@ function buildPlatformApp() {
|
|
|
143
143
|
if (!html) return next();
|
|
144
144
|
res.type('html').send(html);
|
|
145
145
|
});
|
|
146
|
+
// The owner's my-projects surface (goal 1000059, task 1002693) — the durable
|
|
147
|
+
// place to find, watch, and manage one's instances after the /create tab
|
|
148
|
+
// closes. A sibling landing page, branding-injected like /create.
|
|
149
|
+
app.get('/projects', (req, res, next) => {
|
|
150
|
+
if (!isApexHost(req)) return next();
|
|
151
|
+
const html = landingPage('projects.html');
|
|
152
|
+
if (!html) return next();
|
|
153
|
+
res.type('html').send(html);
|
|
154
|
+
});
|
|
146
155
|
const landingAssets = express.static(LANDING_DIR, { index: false, fallthrough: true });
|
|
147
156
|
app.use((req, res, next) => {
|
|
148
157
|
if (!isApexHost(req)) return next();
|
package/tests/onboard.mjs
CHANGED
|
@@ -7,14 +7,15 @@ import { createRequire } from 'node:module';
|
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
8
|
const ob = require('../scripts/gds/onboard.js');
|
|
9
9
|
|
|
10
|
-
test('onboardPlan is the canonical sequence: name+address first,
|
|
10
|
+
test('onboardPlan is the canonical sequence: name+address first, the builders hall last, four legs automated around the human steps', () => {
|
|
11
11
|
const plan = ob.onboardPlan();
|
|
12
12
|
const keys = plan.map((s) => s.key);
|
|
13
13
|
assert.equal(keys[0], 'name-address');
|
|
14
|
-
assert.equal(keys[keys.length - 1], '
|
|
14
|
+
assert.equal(keys[keys.length - 1], 'enter-hall');
|
|
15
|
+
assert.equal(keys[keys.length - 2], 'brand');
|
|
15
16
|
// the irreducibly-human steps (must be UI-first prompts)
|
|
16
17
|
const human = plan.filter((s) => s.kind === 'human').map((s) => s.key);
|
|
17
|
-
assert.deepEqual(human, ['github-repo', 'oauth-app', 'first-sign-in']);
|
|
18
|
+
assert.deepEqual(human, ['github-repo', 'oauth-app', 'first-sign-in', 'enter-hall']);
|
|
18
19
|
// the DNS preflight comes BEFORE provision (task 1980: before the address step)
|
|
19
20
|
assert.ok(keys.indexOf('dns-preflight') < keys.indexOf('provision'), 'dns preflight precedes provision');
|
|
20
21
|
// OAuth secret placement comes after the OAuth app + before first sign-in
|
|
@@ -52,6 +53,46 @@ test('onboardPlan lists seed-first-version as an explicit AUTO step right after
|
|
|
52
53
|
assert.ok(ob.onboardPlan('adopt').some((s) => s.key === 'seed-version'), 'adopt gets it too');
|
|
53
54
|
});
|
|
54
55
|
|
|
56
|
+
test('onboardPlan ends at the builders hall, terminal label branching on domain (task 1002703)', () => {
|
|
57
|
+
// The hall is the entire point of creating a project, yet the sequence used to stop
|
|
58
|
+
// at brand — owners only learned about `bongos dev` or their hall URL by being told
|
|
59
|
+
// (idea 1000693). One terminal step in the ONE source reaches every render surface.
|
|
60
|
+
for (const mode of ['greenfield', 'adopt']) {
|
|
61
|
+
const dflt = ob.onboardPlan(mode);
|
|
62
|
+
const last = dflt[dflt.length - 1];
|
|
63
|
+
assert.equal(last.key, 'enter-hall', `${mode}: the plan ends at the hall`);
|
|
64
|
+
assert.equal(last.kind, 'human', 'entering the hall is the owner\'s own action');
|
|
65
|
+
assert.match(last.label, /bongos dev/, 'no domain → the local-hall one-liner');
|
|
66
|
+
|
|
67
|
+
const withDomain = ob.onboardPlan(mode, { domain: 'mercury.example.com' });
|
|
68
|
+
const lastD = withDomain[withDomain.length - 1];
|
|
69
|
+
assert.equal(lastD.key, 'enter-hall');
|
|
70
|
+
assert.match(lastD.label, /https:\/\/mercury\.example\.com\/builders/, 'domain → the instance\'s hall address');
|
|
71
|
+
assert.doesNotMatch(lastD.label, /bongos dev/, 'with a domain the local fallback is noise');
|
|
72
|
+
// only the terminal label branches — the skeleton is identical
|
|
73
|
+
assert.deepEqual(withDomain.map((s) => s.key), dflt.map((s) => s.key));
|
|
74
|
+
}
|
|
75
|
+
// a blank / whitespace domain is "no domain"
|
|
76
|
+
const blank = ob.onboardPlan('greenfield', { domain: ' ' });
|
|
77
|
+
assert.match(blank[blank.length - 1].label, /bongos dev/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('every render surface carries the terminal hall step (task 1002703)', () => {
|
|
81
|
+
// The human-steps drift guard below pins the TAG; this pins the CONTENT — each
|
|
82
|
+
// surface must point at the hall (domain branch) and at `bongos dev` (local branch).
|
|
83
|
+
const { readFileSync } = require('node:fs');
|
|
84
|
+
const path = require('node:path');
|
|
85
|
+
const read = (rel) => readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');
|
|
86
|
+
const cli = read('scripts/gds/onboard.js');
|
|
87
|
+
assert.match(cli, /builders hall/i, 'the CLI walkthrough ends at the hall');
|
|
88
|
+
assert.match(cli, /bongos dev/, 'the CLI offers the local-hall one-liner when no domain');
|
|
89
|
+
const createHtml = read('public-landing/create.html');
|
|
90
|
+
const enterHall = createHtml.match(/data-onboard-step="enter-hall"[^]*?<\/li>/g) || [];
|
|
91
|
+
assert.ok(enterHall.length >= 1, '/create tags the enter-hall step');
|
|
92
|
+
assert.match(enterHall.join(''), /\/builders/, '/create points at the hall address');
|
|
93
|
+
assert.match(enterHall.join(''), /bongos dev/, '/create offers the local hall when no domain');
|
|
94
|
+
});
|
|
95
|
+
|
|
55
96
|
test('oauthAppInstructions pins the apex callback + homepage and points at ACCOUNT Developer settings', () => {
|
|
56
97
|
const oa = ob.oauthAppInstructions('mercury.example.com');
|
|
57
98
|
assert.equal(oa.homepageUrl, 'https://mercury.example.com');
|
|
@@ -162,6 +162,32 @@ await ta('POST /instances: an unknown onboard_mode is REFUSED (never silently co
|
|
|
162
162
|
assert.deepEqual(res.body.error.valid, ['greenfield', 'adopt']);
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
function get(path) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
http.get(`${base}${path}`, (res) => {
|
|
168
|
+
let d = '';
|
|
169
|
+
res.on('data', (c) => { d += c; });
|
|
170
|
+
res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* non-JSON */ } resolve({ status: res.statusCode, body: j, raw: d }); });
|
|
171
|
+
}).on('error', reject);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await ta('GET /onboard-plan: ?domain= relabels the terminal enter-hall step; garbage is ignored, never echoed (task 1002703)', async () => {
|
|
176
|
+
const bare = await get('/provisioning/onboard-plan');
|
|
177
|
+
assert.equal(bare.status, 200);
|
|
178
|
+
const bareLast = bare.body.steps[bare.body.steps.length - 1];
|
|
179
|
+
assert.equal(bareLast.key, 'enter-hall');
|
|
180
|
+
assert.match(bareLast.label, /bongos dev/, 'no domain → the local-hall one-liner');
|
|
181
|
+
const dom = await get('/provisioning/onboard-plan?domain=mercury.example.com');
|
|
182
|
+
const domLast = dom.body.steps[dom.body.steps.length - 1];
|
|
183
|
+
assert.match(domLast.label, /https:\/\/mercury\.example\.com\/builders/, 'domain → that instance\'s hall');
|
|
184
|
+
// clients render these labels — a non-hostname must be dropped, not reflected back
|
|
185
|
+
const bad = await get('/provisioning/onboard-plan?domain=' + encodeURIComponent('<script>alert(1)</script>'));
|
|
186
|
+
const badLast = bad.body.steps[bad.body.steps.length - 1];
|
|
187
|
+
assert.match(badLast.label, /bongos dev/);
|
|
188
|
+
assert.ok(!badLast.label.includes('<script>'), 'unvalidated input never reaches the label');
|
|
189
|
+
});
|
|
190
|
+
|
|
165
191
|
await ta('POST /instances: adopt WITHOUT target_ref is refused — there is no repo to layer onto', async () => {
|
|
166
192
|
// Without target_ref the runner would fall back to greenfield behavior on a row labelled
|
|
167
193
|
// adopt. Refuse at the door rather than store a lie.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// tests/public_landing_projects.mjs — the owner's my-projects surface (task
|
|
2
|
+
// 1002693, goal 1000059 / gap 6 of the creation-flow audit): after the /create
|
|
3
|
+
// tab closes an owner was blind to their own instances — only create.html read
|
|
4
|
+
// GET /provisioning/instances, and it was sessionStorage-bound. /projects is the
|
|
5
|
+
// durable place to find, watch, and manage them without the original tab.
|
|
6
|
+
//
|
|
7
|
+
// Covers (DB-free — static HTML over the platform app, runs in the unit gate):
|
|
8
|
+
// • GET /projects on the apex serves the branding-injected projects page
|
|
9
|
+
// • /projects is apex-only — the builders/status hosts never serve it (the
|
|
10
|
+
// /create precedent: landing pages belong to the apex host)
|
|
11
|
+
// • the page drives ONLY the own-scoped provisioning surface: list, per-id
|
|
12
|
+
// watch (the create.html poll pattern), teardown, domain-attach — and
|
|
13
|
+
// carries the truthful refusal copy for each (task 1002691/1002692 lessons:
|
|
14
|
+
// intent_conflict, not_teardownable, bad_domain, domain_taken,
|
|
15
|
+
// domain_already_set, domain_not_attachable are all SAID, never swallowed)
|
|
16
|
+
// • an instance with no domain gets an HONEST no-address state, not a blank
|
|
17
|
+
// • create.html's poll give-up now points at /projects — the false "the hall
|
|
18
|
+
// has it" stop (the hall my-projects card was removed in task 1002675) is gone
|
|
19
|
+
//
|
|
20
|
+
// Run: node --test tests/public_landing_projects.mjs
|
|
21
|
+
|
|
22
|
+
import assert from 'node:assert/strict';
|
|
23
|
+
import { test, before, after } from 'node:test';
|
|
24
|
+
import http from 'node:http';
|
|
25
|
+
import platformMod from '../src/platform-server.js';
|
|
26
|
+
|
|
27
|
+
const { buildPlatformApp } = platformMod;
|
|
28
|
+
|
|
29
|
+
let server;
|
|
30
|
+
let port;
|
|
31
|
+
|
|
32
|
+
before(async () => {
|
|
33
|
+
const app = buildPlatformApp();
|
|
34
|
+
server = http.createServer(app);
|
|
35
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
36
|
+
port = server.address().port;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
after(async () => {
|
|
40
|
+
await new Promise((resolve) => server.close(resolve));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Explicit Host header so the apex / builders.* / status.* routing is exercised
|
|
44
|
+
// (fetch treats `host` as forbidden — the platform_boot.mjs helper).
|
|
45
|
+
function request(pathname, host = '127.0.0.1') {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const req = http.request(
|
|
48
|
+
{ host: '127.0.0.1', port, path: pathname, method: 'GET', headers: { Host: host } },
|
|
49
|
+
(res) => {
|
|
50
|
+
let body = '';
|
|
51
|
+
res.on('data', (c) => (body += c));
|
|
52
|
+
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
req.on('error', reject);
|
|
56
|
+
req.end();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test('the apex host serves the branded /projects page', async () => {
|
|
61
|
+
const res = await request('/projects', 'game.test.local');
|
|
62
|
+
assert.equal(res.status, 200);
|
|
63
|
+
assert.match(res.headers['content-type'] || '', /text\/html/);
|
|
64
|
+
// branding-injected exactly like the landing + /create (same landingPage memo)
|
|
65
|
+
assert.match(res.body, /window\.__BRANDING__=/);
|
|
66
|
+
// the page's own shell, not the 404 tail or a stray document
|
|
67
|
+
assert.match(res.body, /id="projectsList"/);
|
|
68
|
+
assert.doesNotMatch(res.body, /404 — not found/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('/projects is apex-only — the builders and status hosts never serve it', async () => {
|
|
72
|
+
for (const host of ['builders.test.local', 'status.test.local']) {
|
|
73
|
+
const res = await request('/projects', host);
|
|
74
|
+
assert.doesNotMatch(res.body, /id="projectsList"/, `${host} must not serve the projects page`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('the page drives the own-scoped provisioning surface: list, watch, teardown, domain', async () => {
|
|
79
|
+
const { body } = await request('/projects', 'game.test.local');
|
|
80
|
+
// list + per-id watch (the create.html:1034 poll pattern — GET, own-scoped)
|
|
81
|
+
assert.match(body, /provisioning\/instances/);
|
|
82
|
+
// manage: request teardown + attach an address — both ENQUEUE-only routes
|
|
83
|
+
// (ADR 0111 §2: this page can only ask; the control-plane runner acts)
|
|
84
|
+
assert.match(body, /\/teardown/);
|
|
85
|
+
assert.match(body, /\/domain/);
|
|
86
|
+
// it never reaches for archon/fleet surfaces
|
|
87
|
+
assert.doesNotMatch(body, /provisioning\/fleet/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('every truthful refusal the API can give is mapped to human copy, never swallowed', async () => {
|
|
91
|
+
const { body } = await request('/projects', 'game.test.local');
|
|
92
|
+
for (const code of [
|
|
93
|
+
'intent_conflict', // an open run holds the slot (tasks 1002691/1002692)
|
|
94
|
+
'not_teardownable', // teardown refused for the real state
|
|
95
|
+
'bad_domain', // malformed hostname
|
|
96
|
+
'domain_taken', // another instance already answers there
|
|
97
|
+
'domain_already_set', // re-pointing is not this surface
|
|
98
|
+
'domain_not_attachable', // mid-flight/torn-down states refuse truthfully
|
|
99
|
+
]) {
|
|
100
|
+
assert.match(body, new RegExp(code), `the page must handle '${code}' explicitly`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('a domainless instance gets an honest no-address state', async () => {
|
|
105
|
+
const { body } = await request('/projects', 'game.test.local');
|
|
106
|
+
assert.match(body, /[Nn]o address yet/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('the empty state points a new owner at /create — never at a hall', async () => {
|
|
110
|
+
const { body } = await request('/projects', 'game.test.local');
|
|
111
|
+
assert.match(body, /href="\/create"/);
|
|
112
|
+
// the hall my-projects card was REMOVED (task 1002675; hall my-projects is
|
|
113
|
+
// federation-only) — this page must not send an owner there
|
|
114
|
+
assert.doesNotMatch(body, /Enter the Hall/i);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('create.html: the poll give-up points at /projects — "the hall has it" is dead', async () => {
|
|
118
|
+
const { body } = await request('/create', 'game.test.local');
|
|
119
|
+
assert.match(body, /\/projects/, 'the give-up path must hand the owner the projects page');
|
|
120
|
+
assert.doesNotMatch(body, /the hall has it/, 'the false poll-stop copy must be gone');
|
|
121
|
+
});
|