@bongos/core 1.19.570 → 1.19.572
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 +48 -28
- package/clients/bongos-client/index.d.ts +3 -3
- package/docs/adr/0257-auth-resolves-before-the-hall-mounts-anything.md +84 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +13 -4
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +3 -3
- package/docs/copy-registry.json +3 -3
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/builders.js +42 -13
- package/modules/hall-ui/public/goals.js +10 -1
- package/modules/lifecycle/db-goals.js +47 -2
- package/modules/lifecycle/db.js +4 -0
- package/modules/lifecycle/migrations/lifecycle_008_goal_version_admissions.sql +51 -0
- package/modules/lifecycle/routes/goal-route-authz.js +7 -1
- package/modules/lifecycle/routes/goals.js +37 -4
- package/modules/lifecycle/routes/versions.js +10 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/goal_admission_override.mjs +179 -0
- package/tests/goal_category.mjs +1 -1
- package/tests/goal_create_version_gate.mjs +7 -4
- package/tests/goal_tier.mjs +4 -2
- package/tests/hall_landing_boot.mjs +141 -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.572",
|
|
6
|
+
"core_contract": "1.19.572",
|
|
7
|
+
"source_commit": "31894c826b1cb16600f02046b2e17a09f78f3712",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-07T00:51:07.099Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 450,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2042,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2516,
|
|
20
|
+
"tree_sha256": "dc4746621684478bdfcc20dba9ccc094ed1722956378f38939e11ff3f83e35d5",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -392,7 +392,7 @@
|
|
|
392
392
|
{
|
|
393
393
|
"path": "clients/bongos-client/index.d.ts",
|
|
394
394
|
"mode": "0000644",
|
|
395
|
-
"sha256": "
|
|
395
|
+
"sha256": "adaa20c8ce6bee76dc289f10929014382aa89d7373f94ddafc84f4c87eeb0ed4"
|
|
396
396
|
},
|
|
397
397
|
{
|
|
398
398
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1799,20 +1799,25 @@
|
|
|
1799
1799
|
"mode": "0000644",
|
|
1800
1800
|
"sha256": "4b7d5b4ca5ad92a128235a6c3407617f2d8b34c26ed6e1c8b2a72e63eed7adac"
|
|
1801
1801
|
},
|
|
1802
|
+
{
|
|
1803
|
+
"path": "docs/adr/0257-auth-resolves-before-the-hall-mounts-anything.md",
|
|
1804
|
+
"mode": "0000644",
|
|
1805
|
+
"sha256": "5ebfaa564a125f2c5bb0b391b8ff01914f147c15c9ef3f4caa91bafa8bd2e8ed"
|
|
1806
|
+
},
|
|
1802
1807
|
{
|
|
1803
1808
|
"path": "docs/adr/README.md",
|
|
1804
1809
|
"mode": "0000644",
|
|
1805
|
-
"sha256": "
|
|
1810
|
+
"sha256": "e9b47055a52c284035c201fae3f172215c746caabf938922821f4efcda2e8980"
|
|
1806
1811
|
},
|
|
1807
1812
|
{
|
|
1808
1813
|
"path": "docs/api-reference.md",
|
|
1809
1814
|
"mode": "0000644",
|
|
1810
|
-
"sha256": "
|
|
1815
|
+
"sha256": "0a622847ab68980279beba2e1a54813b2393107144f88dd31228fe4dfd248b90"
|
|
1811
1816
|
},
|
|
1812
1817
|
{
|
|
1813
1818
|
"path": "docs/api/openapi.json",
|
|
1814
1819
|
"mode": "0000644",
|
|
1815
|
-
"sha256": "
|
|
1820
|
+
"sha256": "60ca0b131c661df16fb993182d2085d0b04ea15a4d91bec4318c3413fb8dc311"
|
|
1816
1821
|
},
|
|
1817
1822
|
{
|
|
1818
1823
|
"path": "docs/architecture.md",
|
|
@@ -1837,12 +1842,12 @@
|
|
|
1837
1842
|
{
|
|
1838
1843
|
"path": "docs/copy-inventory.md",
|
|
1839
1844
|
"mode": "0000644",
|
|
1840
|
-
"sha256": "
|
|
1845
|
+
"sha256": "8bdf39ea0b8ed9583f9ef68b0edae767eec9c2825f8ef355d678b3db3d08b5e1"
|
|
1841
1846
|
},
|
|
1842
1847
|
{
|
|
1843
1848
|
"path": "docs/copy-registry.json",
|
|
1844
1849
|
"mode": "0000644",
|
|
1845
|
-
"sha256": "
|
|
1850
|
+
"sha256": "cd51497ccca735bf1f8178291b427f4166aff79870a080a4a9d036238f0ff90c"
|
|
1846
1851
|
},
|
|
1847
1852
|
{
|
|
1848
1853
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2687,7 +2692,7 @@
|
|
|
2687
2692
|
{
|
|
2688
2693
|
"path": "docs/module-api-changelog.md",
|
|
2689
2694
|
"mode": "0000644",
|
|
2690
|
-
"sha256": "
|
|
2695
|
+
"sha256": "d7acc0617ab16007150c0f733248576e5c285286f22b32ec722fa061748994f9"
|
|
2691
2696
|
},
|
|
2692
2697
|
{
|
|
2693
2698
|
"path": "docs/modules-contract.md",
|
|
@@ -4642,7 +4647,7 @@
|
|
|
4642
4647
|
{
|
|
4643
4648
|
"path": "modules/hall-ui/public/builders.js",
|
|
4644
4649
|
"mode": "0000644",
|
|
4645
|
-
"sha256": "
|
|
4650
|
+
"sha256": "ac1252ef25e0fe8b5774ef3d6566970657437de1151cd3e9e194859067fee789"
|
|
4646
4651
|
},
|
|
4647
4652
|
{
|
|
4648
4653
|
"path": "modules/hall-ui/public/chime.wav",
|
|
@@ -4822,7 +4827,7 @@
|
|
|
4822
4827
|
{
|
|
4823
4828
|
"path": "modules/hall-ui/public/goals.js",
|
|
4824
4829
|
"mode": "0000644",
|
|
4825
|
-
"sha256": "
|
|
4830
|
+
"sha256": "7800d109e109d61772edcf9fdd1f01dcd079a0937358f2ed8da6cbed361c1e9c"
|
|
4826
4831
|
},
|
|
4827
4832
|
{
|
|
4828
4833
|
"path": "modules/hall-ui/public/goals.states.json",
|
|
@@ -5462,7 +5467,7 @@
|
|
|
5462
5467
|
{
|
|
5463
5468
|
"path": "modules/lifecycle/db-goals.js",
|
|
5464
5469
|
"mode": "0000644",
|
|
5465
|
-
"sha256": "
|
|
5470
|
+
"sha256": "31f39ea088b03feb217af4ce2fbf0b4dba0d8dad3e8eec625cf11e0dd4cb4228"
|
|
5466
5471
|
},
|
|
5467
5472
|
{
|
|
5468
5473
|
"path": "modules/lifecycle/db-grade.js",
|
|
@@ -5502,7 +5507,7 @@
|
|
|
5502
5507
|
{
|
|
5503
5508
|
"path": "modules/lifecycle/db.js",
|
|
5504
5509
|
"mode": "0000644",
|
|
5505
|
-
"sha256": "
|
|
5510
|
+
"sha256": "61ed58e2d0dbe9aa0b889d27b6a9c38df024a40bc797f22b0ff766fedf85a9de"
|
|
5506
5511
|
},
|
|
5507
5512
|
{
|
|
5508
5513
|
"path": "modules/lifecycle/dead-deps.js",
|
|
@@ -5619,6 +5624,11 @@
|
|
|
5619
5624
|
"mode": "0000644",
|
|
5620
5625
|
"sha256": "0d17971d88c2f75a61a5646325ebab121fd4406cdd39324a1128a5e610ce6d7d"
|
|
5621
5626
|
},
|
|
5627
|
+
{
|
|
5628
|
+
"path": "modules/lifecycle/migrations/lifecycle_008_goal_version_admissions.sql",
|
|
5629
|
+
"mode": "0000644",
|
|
5630
|
+
"sha256": "4e4099764712b6ac8ad322cb846eb1e4b1118e936d03628e01a6a04adb4d4ebf"
|
|
5631
|
+
},
|
|
5622
5632
|
{
|
|
5623
5633
|
"path": "modules/lifecycle/mingle.js",
|
|
5624
5634
|
"mode": "0000644",
|
|
@@ -5677,12 +5687,12 @@
|
|
|
5677
5687
|
{
|
|
5678
5688
|
"path": "modules/lifecycle/routes/goal-route-authz.js",
|
|
5679
5689
|
"mode": "0000644",
|
|
5680
|
-
"sha256": "
|
|
5690
|
+
"sha256": "5022ec9cdd2ba5087bc0b4607691c21d361f4a4edbf029ff3dea37ade373e890"
|
|
5681
5691
|
},
|
|
5682
5692
|
{
|
|
5683
5693
|
"path": "modules/lifecycle/routes/goals.js",
|
|
5684
5694
|
"mode": "0000644",
|
|
5685
|
-
"sha256": "
|
|
5695
|
+
"sha256": "d408d89cf736a7d4cdf45ccff44c2038bbc64b13be42dd1b460198dfc8112535"
|
|
5686
5696
|
},
|
|
5687
5697
|
{
|
|
5688
5698
|
"path": "modules/lifecycle/routes/help-requests.js",
|
|
@@ -5742,7 +5752,7 @@
|
|
|
5742
5752
|
{
|
|
5743
5753
|
"path": "modules/lifecycle/routes/versions.js",
|
|
5744
5754
|
"mode": "0000644",
|
|
5745
|
-
"sha256": "
|
|
5755
|
+
"sha256": "5efe819220f9128844793a0c068babcd402860b434aab322fd9a99f8d2d26167"
|
|
5746
5756
|
},
|
|
5747
5757
|
{
|
|
5748
5758
|
"path": "modules/lifecycle/routes/visuals.js",
|
|
@@ -7537,12 +7547,12 @@
|
|
|
7537
7547
|
{
|
|
7538
7548
|
"path": "package-lock.json",
|
|
7539
7549
|
"mode": "0000644",
|
|
7540
|
-
"sha256": "
|
|
7550
|
+
"sha256": "439507cc695f5ef93f4fe1aa9a48556b4b9e11a023fa4e60912de1ed8befc5ee"
|
|
7541
7551
|
},
|
|
7542
7552
|
{
|
|
7543
7553
|
"path": "package.json",
|
|
7544
7554
|
"mode": "0000644",
|
|
7545
|
-
"sha256": "
|
|
7555
|
+
"sha256": "bf5fe42d980095754ea4b13501ede3a559376dae4127a99915767ba4be0c27d2"
|
|
7546
7556
|
},
|
|
7547
7557
|
{
|
|
7548
7558
|
"path": "public-docs/index.html",
|
|
@@ -9242,7 +9252,7 @@
|
|
|
9242
9252
|
{
|
|
9243
9253
|
"path": "src/module-api.js",
|
|
9244
9254
|
"mode": "0000644",
|
|
9245
|
-
"sha256": "
|
|
9255
|
+
"sha256": "be71a580784409ac5e4507201fbbf9915e16f0682098e3b9e98ec49a0af98216"
|
|
9246
9256
|
},
|
|
9247
9257
|
{
|
|
9248
9258
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10324,6 +10334,11 @@
|
|
|
10324
10334
|
"mode": "0000644",
|
|
10325
10335
|
"sha256": "3e157d5bf44370e4935b70203fe59e40cf9120d12c472f24ed7bd85cb33cbe91"
|
|
10326
10336
|
},
|
|
10337
|
+
{
|
|
10338
|
+
"path": "tests/goal_admission_override.mjs",
|
|
10339
|
+
"mode": "0000644",
|
|
10340
|
+
"sha256": "94b378fb96bbbb6285b61c4d00827f57e0b29d64d9036576670ffaf8ca233239"
|
|
10341
|
+
},
|
|
10327
10342
|
{
|
|
10328
10343
|
"path": "tests/goal_advisory.mjs",
|
|
10329
10344
|
"mode": "0000644",
|
|
@@ -10337,7 +10352,7 @@
|
|
|
10337
10352
|
{
|
|
10338
10353
|
"path": "tests/goal_category.mjs",
|
|
10339
10354
|
"mode": "0000644",
|
|
10340
|
-
"sha256": "
|
|
10355
|
+
"sha256": "dbdfd351d5edc3ac6adcb1a34e072fab0a8db9669030d7476bbf18aa1e81540d"
|
|
10341
10356
|
},
|
|
10342
10357
|
{
|
|
10343
10358
|
"path": "tests/goal_category_ui.mjs",
|
|
@@ -10362,7 +10377,7 @@
|
|
|
10362
10377
|
{
|
|
10363
10378
|
"path": "tests/goal_create_version_gate.mjs",
|
|
10364
10379
|
"mode": "0000644",
|
|
10365
|
-
"sha256": "
|
|
10380
|
+
"sha256": "28da055ccad13b327135dce6292df9c6bc53e2589648e0a4361ada0ddd21b863"
|
|
10366
10381
|
},
|
|
10367
10382
|
{
|
|
10368
10383
|
"path": "tests/goal_criterion_confirm_ui.mjs",
|
|
@@ -10417,7 +10432,7 @@
|
|
|
10417
10432
|
{
|
|
10418
10433
|
"path": "tests/goal_tier.mjs",
|
|
10419
10434
|
"mode": "0000644",
|
|
10420
|
-
"sha256": "
|
|
10435
|
+
"sha256": "3c420d28341a6e5aaeb5d4af978e72acefec3c9bd7d97f66fd11a72517118606"
|
|
10421
10436
|
},
|
|
10422
10437
|
{
|
|
10423
10438
|
"path": "tests/goal_wall_proof.mjs",
|
|
@@ -10714,6 +10729,11 @@
|
|
|
10714
10729
|
"mode": "0000644",
|
|
10715
10730
|
"sha256": "5654f4366ddcfcb61848a119a2dba3827f3fe19c0151005558a803c2e09db573"
|
|
10716
10731
|
},
|
|
10732
|
+
{
|
|
10733
|
+
"path": "tests/hall_landing_boot.mjs",
|
|
10734
|
+
"mode": "0000644",
|
|
10735
|
+
"sha256": "36711b8d885203ecb2f70ecd6aaf18359bc6796d50300bc8b062485a24ba49a4"
|
|
10736
|
+
},
|
|
10717
10737
|
{
|
|
10718
10738
|
"path": "tests/hall_nav.mjs",
|
|
10719
10739
|
"mode": "0000644",
|
|
@@ -49,7 +49,7 @@ export interface GetGithubRepoResponse { ok: boolean; repo: unknown; private_dep
|
|
|
49
49
|
export interface GetGithubReposResponse { repos: unknown }
|
|
50
50
|
export interface GetGoalsIdConflictsResponse { ok: boolean; goal_id: unknown; conflicts: unknown; cross_goals_considered: unknown }
|
|
51
51
|
export interface GetGoalsIdRequestsResponse { requests: unknown }
|
|
52
|
-
export interface GetGoalsIdResponse { goal: unknown; members: unknown; criteria: unknown; dependencies: unknown; criterionless_task_count: unknown }
|
|
52
|
+
export interface GetGoalsIdResponse { goal: unknown; members: unknown; criteria: unknown; dependencies: unknown; criterionless_task_count: unknown; admitted: unknown }
|
|
53
53
|
export interface GetGoalsResponse { goals: unknown }
|
|
54
54
|
export interface GetGoalsRollupsResponse { rollups: unknown }
|
|
55
55
|
export interface GetGovernmentAssignmentsResponse { assignments: unknown }
|
|
@@ -278,8 +278,8 @@ export interface PostGoalsIdTransferRequest { builder_id?: StringifiableId }
|
|
|
278
278
|
export interface PostGoalsIdTransferResponse { ok: boolean; goal: unknown; changed: boolean }
|
|
279
279
|
export interface PostGoalsReorderRequest { version_id: string; ordered_ids: number[] }
|
|
280
280
|
export interface PostGoalsReorderResponse { ok: boolean; goals: unknown }
|
|
281
|
-
export interface PostGoalsRequest { version_id: string; title: string; subtitle?: string; description?: string; scope_modules?: string[]; succeeds_goal_id?: number; category_id?: number }
|
|
282
|
-
export interface PostGoalsResponse { ok: boolean; goal: unknown; succeeds_goal_id: unknown; category_advisory: unknown }
|
|
281
|
+
export interface PostGoalsRequest { version_id: string; title: string; subtitle?: string; description?: string; scope_modules?: string[]; succeeds_goal_id?: number; category_id?: number; admit_reason?: string }
|
|
282
|
+
export interface PostGoalsResponse { ok: boolean; goal: unknown; succeeds_goal_id: unknown; category_advisory: unknown; admitted: unknown }
|
|
283
283
|
export interface PostGovernmentAssignmentsRequest { builder_id: number; rank_key: string }
|
|
284
284
|
export interface PostGovernmentBoardItemsItemIdVotesRequest { direction: string; reason_md?: string; section_key?: string }
|
|
285
285
|
export interface PostGovernmentBoardItemsRequest { subject_type: string; proposal: Record<string, unknown>; rationale_md?: string; acknowledge_self_removal?: boolean }
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# ADR 0257 — Auth resolves before the hall mounts anything, and a widget's boot read may never navigate
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-09-06
|
|
5
|
+
- **Task:** [task 1003673](https://cloudbongos.com/builders#/task/1003673) (BONGOS-V1, goal 1000063 — *The front door*)
|
|
6
|
+
- **Deciders:** Claude, under the Archon's standing scope
|
|
7
|
+
- **Related:** [ADR 0050](<redacted>.md) (admission is invite-gated by default — the posture this defect made unusable) · [ADR 0194](<redacted>.md) + [ADR 0247](<redacted>.md) (the door the landing form renders) · [ADR 0062 §4](<redacted>.md) (the hall-widget registry)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
An owner stood up an instance, invited a colleague, and watched them sign in with
|
|
14
|
+
GitHub. They got *"Sign-in failed — this account is not approved yet. New here?
|
|
15
|
+
Request access first"*, whose one button goes to `/builders`. The owner then went
|
|
16
|
+
looking for the approval queue and found nothing to approve: `GET /access-requests`
|
|
17
|
+
answered `{"requests": []}`, and the hall hides the Access-requests panel when the
|
|
18
|
+
queue is empty. Both halves are working as designed — **signing in does not file a
|
|
19
|
+
request**; only the landing page's *Request access* form does.
|
|
20
|
+
|
|
21
|
+
The landing page could not be reached. A signed-out visitor to `/builders` was
|
|
22
|
+
bounced to GitHub, refused as a first-timer, and pointed back at `/builders` to be
|
|
23
|
+
bounced again. A closed loop, and the form sat inside it. Every invite-only
|
|
24
|
+
instance — the default posture since ADR 0050 — therefore had **no way to admit its
|
|
25
|
+
first builder**, and the symptom pointed at the approval UI rather than at the door.
|
|
26
|
+
|
|
27
|
+
Reproduced against `main` (not only against an old pin) by serving
|
|
28
|
+
`modules/hall-ui/public` statically with every authenticated route answering 401:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
GET /api/bongos/me 401 (soft — builders.js would renderLanding())
|
|
32
|
+
GET /api/bongos/goals 401 -> window.location.assign(/auth/web/start)
|
|
33
|
+
GET /api/bongos/auth/web/start the dead end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`builders.js` was already careful: `/me` is read with `softAuth`, and a 401 there
|
|
37
|
+
renders the landing rather than bouncing. But its `DOMContentLoaded` handler called
|
|
38
|
+
`mountHallWidgets()` **first**, synchronously. `goals.js`'s `mount()` reads `/goals`
|
|
39
|
+
through a `getJSON` with no `softAuth`, and that helper answers 401 by *navigating*.
|
|
40
|
+
The widget's read therefore raced `renderLanding()` — and won.
|
|
41
|
+
|
|
42
|
+
## Decision
|
|
43
|
+
|
|
44
|
+
**1. The front door settles which page this is before anything else runs.**
|
|
45
|
+
`builders.js` boots through `boot()`: read `/me` (soft), and only then choose.
|
|
46
|
+
Signed out → `renderLanding()` and stop; nothing else mounts, nothing else fetches.
|
|
47
|
+
Signed in → mount widgets, wire events, render the hall. The resolved `/me` payload
|
|
48
|
+
is handed to `loadAll()` rather than re-fetched, so the page still asks once.
|
|
49
|
+
|
|
50
|
+
Widgets are member surfaces — `renderLanding()` already hid every one of them after
|
|
51
|
+
the fact — so not mounting them for a signed-out visitor removes a whole class of
|
|
52
|
+
races rather than the one instance of it that was found.
|
|
53
|
+
|
|
54
|
+
**2. A widget's own boot read may never navigate.** `goals.js`'s mount-time read is
|
|
55
|
+
`softAuth`, and answers "no session" by rendering nothing. A 401 during page boot is
|
|
56
|
+
not an error and not an instruction to go and sign in; it is the answer *this visitor
|
|
57
|
+
has no session*, which the landing already handles. Only a user action may send
|
|
58
|
+
someone to the sign-in flow.
|
|
59
|
+
|
|
60
|
+
## Consequences
|
|
61
|
+
|
|
62
|
+
- An invite-only instance can admit its first builder: the landing renders, the
|
|
63
|
+
request form is reachable, the queue fills, and the owner has something to approve.
|
|
64
|
+
- The "Sign-in failed" page's CTA now lands somewhere useful — the loop is closed at
|
|
65
|
+
both ends.
|
|
66
|
+
- `tests/hall_landing_boot.mjs` executes the real `builders.js` in a DOM stub and
|
|
67
|
+
asserts the invariant three ways: a signed-out boot never navigates, renders the
|
|
68
|
+
landing, and mounts no widgets; it asks for `/me` and nothing else; and a signed-in
|
|
69
|
+
boot still mounts the hall on a single `/me`. The guard was confirmed to FAIL
|
|
70
|
+
against the pre-fix file — a regression test that passes both ways guards nothing.
|
|
71
|
+
- A widget added later can still reintroduce the class if it hard-redirects at mount
|
|
72
|
+
AND something mounts it while signed out. The boot order is what makes that
|
|
73
|
+
unreachable; the `softAuth` rule is the belt beside it.
|
|
74
|
+
|
|
75
|
+
## Rejected
|
|
76
|
+
|
|
77
|
+
- **Patch `goals.js` alone.** It was the widget that happened to fire first. Any
|
|
78
|
+
mount-time authenticated read can steal the page, so the fix belongs at the boot
|
|
79
|
+
order, not at whichever widget was caught doing it.
|
|
80
|
+
- **Make the 401 → redirect helper soft everywhere.** A signed-in builder whose
|
|
81
|
+
session expires mid-visit *should* be sent to sign in. The distinction that matters
|
|
82
|
+
is boot versus user action, not the status code.
|
|
83
|
+
- **Open enrollment** (`OPEN_XENOS_ENROLLMENT`) as the cure. That removes the gate
|
|
84
|
+
instead of repairing the door, and ADR 0050 chose the gate deliberately.
|
package/docs/adr/README.md
CHANGED
|
@@ -348,3 +348,4 @@ This keeps the decision history honest and traceable.
|
|
|
348
348
|
| 0246 | [**Catalog membership is a projection of the provisioning row, not a side effect of federation** ([task 1003576](https://cloudbongos.com/builders#/task/1003576) · goal 1000046, criterion `proj-detail-interview` / C3). The BV1.R18 proof ([task 1002337](https://cloudbongos.com/builders#/task/1002337)) demonstrated C3 against live and found its last clause — “then it publishes” — structurally false: the owner’s verdict reads `provisioning_instances` while the public map reads `platform_identity_projects`, and the ONLY bridge was `federateInstance → registerClient → upsertProject`, which `shouldFederate` refuses for anything but `co-tenant` (the wizard hard-codes `standalone`) and which runs ONCE, at provision time, through a COALESCE. So a wizard-made project was never catalogued at all and a co-tenant one that answered later never updated — `publishable: true`, `{"projects": []}`, forever. The same missing row is [task 1003580](https://cloudbongos.com/builders#/task/1003580)’s GAP 1: `POST /projects/invite` resolves against `platform_identity_sso_clients`, whose only writer is that same `registerClient`. Underneath is one conflation — federation mints CREDENTIALS and was incidentally the only thing that minted MEMBERSHIP, while [ADR 0182](<redacted>.md) D4 already fixed the map’s gate at IDENTITY, which has nothing to do with hosting topology. **Decision: the catalog row is a PROJECTION of the provisioning row, re-derived at every write that can change its inputs** — two halves, neither sufficient alone. (1) THE ROW SHAPE: every control-plane project gets both rows whatever its hosting shape, and the client row carries NO credential (`secret_hash = ''`, `redirect_uris = '{}'`), so **the shape is the enforcement** rather than a flag a caller could forget — `redirectUriAllowed` refuses `/sso/authorize`, `clientSecretValid` refuses `/sso/token`, `/sso/membership/check-in` and `/sso/activity/rollup`, and the one leg that checked NEITHER (`/sso/device/start`, which resolves a client from a user-typed origin) grew an explicit `clientIsCredentialed` guard. (2) THE TIMING: re-taken on create, on `PATCH …/detail`, on `PATCH …/settings` when the door changes, and on `POST …/domain` (which re-keys the row, retiring the abandoned origin — [ADR 0181](0181-teardown-state-gate-and-address-retention.md) §2’s leak, in the catalog instead of DNS). **NO MIGRATION** — `secret_hash` is `NOT NULL` and `''` satisfies it while hashing to nothing. `description` and `visibility` are ASSIGNED, not COALESCEd (clearing a description must UN-publish); `featured`/`tagline`/`art_url`/`template` are curation and stay untouched, so [ADR 0182](<redacted>.md) D3 holds. It grants nothing — the row is attribution per [ADR 0238](<redacted>.md), ownership stays `provisioning_instances.owner_builder_id` — and because it writes rows keyed on owner-supplied values (a slug, a BYO domain) each write carries its **claim rule in the ON CONFLICT action’s own WHERE**: the client is refreshed only `WHERE secret_hash = ''` (a real client’s origin, which binds the CLI mint audience, can never be rewritten) and the project only `WHERE owner_github_id = EXCLUDED.owner_github_id` (`NULL = anything` is not true, so the hand-seeded rows cannot be claimed). Writing `visibility` does not contradict [ADR 0182](<redacted>.md) D5: the hub authorizes nothing — the authority already ran on the provisioning plane ([ADR 0238](<redacted>.md)’s own-scoped, audited PATCH), and a project the control plane does NOT own has no provisioning row, so D5’s instance report (R08) stays the only channel for those. `tearing_down`/`torn_down` project `hidden`; a `no_address` instance is not catalogued at all. Travels on the kernel port `project.catalog`, the `project.publishGate` seam in the other direction ([ADR 0083](<redacted>.md) forbids the import). Rejected: extending `shouldFederate` to every shape (hands a real secret to instances whose web.env the platform does not write, and keeps the once-at-standup bug); bridging only at the publish moment (leaves the invite with nothing to resolve); registering only at standup (the bug itself); a `catalog_only` boolean column (needs a migration and is weaker than the shape); letting the bridge write `featured`; and backfilling the three existing catalog rows from code.](<redacted>.md) | provisioning / project creation |
|
|
349
349
|
|
|
350
350
|
> ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
|
|
351
|
+
| 0257 | [**Auth resolves before the hall mounts anything, and a widget's boot read may never navigate** ([task 1003673](https://cloudbongos.com/builders#/task/1003673) · goal 1000063 — *The front door*). An invite-only instance could not admit its FIRST builder, and the symptom lied about where the fault was: an owner saw an empty Access-requests queue and no approve button, because **signing in does not file a request** — only the landing's *Request access* form does, and that form was unreachable. A signed-out visitor to `/builders` was bounced to GitHub, refused as a first-timer ("request access first"), and pointed back at `/builders` to be bounced again. Reproduced against `main`, not just an old pin. `builders.js` was already careful — `/me` is read `softAuth` and a 401 there renders the landing — but `DOMContentLoaded` called `mountHallWidgets()` FIRST, synchronously, and `goals.js`'s mount-time read of `/goals` goes through a `getJSON` with no `softAuth`, which answers 401 by NAVIGATING. The widget's read raced `renderLanding()` and won. **Decision: the front door settles which page this is before anything else runs** — `boot()` reads `/me` (soft), then either renders the landing and stops (nothing mounts, nothing else fetches) or mounts the hall, handing the resolved payload to `loadAll()` so the page still asks once. Widgets are member surfaces (`renderLanding` hid them all after the fact anyway), so not mounting them while signed out removes the CLASS rather than the one instance of it that was found; **a widget's own boot read may never navigate** is the belt beside it (`goals.js` reads soft and renders nothing without a session). A 401 during boot is not an instruction to go and sign in — only a user action is. `tests/hall_landing_boot.mjs` executes the real `builders.js` in a DOM stub and was confirmed to FAIL against the pre-fix file. Rejected: patching `goals.js` alone (it was merely first); making the 401 redirect soft everywhere (an expired session mid-visit SHOULD be sent to sign in — the line is boot vs user action); open enrollment as the cure (removes the gate [ADR 0050](<redacted>.md) chose deliberately instead of repairing the door).](<redacted>.md) | hall-ui / admission / front door |
|
package/docs/api/openapi.json
CHANGED
|
@@ -16955,14 +16955,16 @@
|
|
|
16955
16955
|
"members": {},
|
|
16956
16956
|
"criteria": {},
|
|
16957
16957
|
"dependencies": {},
|
|
16958
|
-
"criterionless_task_count": {}
|
|
16958
|
+
"criterionless_task_count": {},
|
|
16959
|
+
"admitted": {}
|
|
16959
16960
|
},
|
|
16960
16961
|
"required": [
|
|
16961
16962
|
"goal",
|
|
16962
16963
|
"members",
|
|
16963
16964
|
"criteria",
|
|
16964
16965
|
"dependencies",
|
|
16965
|
-
"criterionless_task_count"
|
|
16966
|
+
"criterionless_task_count",
|
|
16967
|
+
"admitted"
|
|
16966
16968
|
]
|
|
16967
16969
|
},
|
|
16968
16970
|
"GetGoalsResponse": {
|
|
@@ -20325,6 +20327,11 @@
|
|
|
20325
20327
|
"category_id": {
|
|
20326
20328
|
"type": "integer",
|
|
20327
20329
|
"minimum": 1
|
|
20330
|
+
},
|
|
20331
|
+
"admit_reason": {
|
|
20332
|
+
"type": "string",
|
|
20333
|
+
"minLength": 8,
|
|
20334
|
+
"maxLength": 500
|
|
20328
20335
|
}
|
|
20329
20336
|
},
|
|
20330
20337
|
"required": [
|
|
@@ -20341,13 +20348,15 @@
|
|
|
20341
20348
|
},
|
|
20342
20349
|
"goal": {},
|
|
20343
20350
|
"succeeds_goal_id": {},
|
|
20344
|
-
"category_advisory": {}
|
|
20351
|
+
"category_advisory": {},
|
|
20352
|
+
"admitted": {}
|
|
20345
20353
|
},
|
|
20346
20354
|
"required": [
|
|
20347
20355
|
"ok",
|
|
20348
20356
|
"goal",
|
|
20349
20357
|
"succeeds_goal_id",
|
|
20350
|
-
"category_advisory"
|
|
20358
|
+
"category_advisory",
|
|
20359
|
+
"admitted"
|
|
20351
20360
|
]
|
|
20352
20361
|
},
|
|
20353
20362
|
"PostGovernmentAssignmentsRequest": {
|
package/docs/api-reference.md
CHANGED
|
@@ -226,7 +226,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
226
226
|
| Method | Path | Rank | Body | Description |
|
|
227
227
|
|---|---|---|---|---|
|
|
228
228
|
| GET | `/api/bongos/goals` | `any-builder` | — | GET /goals?version=&status= — list goals (read). |
|
|
229
|
-
| POST | `/api/bongos/goals` | `metic+archon` | `version_id`, `title`, `subtitle`, `description`, `scope_modules`, `succeeds_goal_id`, `category_id` | POST /goals — create a goal. |
|
|
229
|
+
| POST | `/api/bongos/goals` | `metic+archon` | `version_id`, `title`, `subtitle`, `description`, `scope_modules`, `succeeds_goal_id`, `category_id`, `admit_reason` | POST /goals — create a goal. |
|
|
230
230
|
| GET | `/api/bongos/goals/:id` | `any-builder` | — | GET /goals/:id — one goal with its members + linked criteria (read). |
|
|
231
231
|
| PATCH | `/api/bongos/goals/:id` | `any-builder` | — | PATCH /goals/:id — correct a goal's own title/subtitle/description (tasks 1003102, 1003224). |
|
|
232
232
|
| PATCH | `/api/bongos/goals/:id/accepting-requests` | `any-builder` | — | PATCH /goals/:id/accepting-requests { accepting_requests } — a private goal's lead closes or re-opens it to join-requests (task 1002971, … |
|
package/docs/copy-inventory.md
CHANGED
|
@@ -135,7 +135,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
135
135
|
| `1b326b0be1a4` | certain | A {…} — you are a {…}. Mingle pairs across crafts. | `modules/hall-ui/public/collab.js:196` |
|
|
136
136
|
| `996edffc1966` | certain | Access requests | `modules/hall-ui/public/watch.html:46` |
|
|
137
137
|
| `996edffc1966` | certain | Access requests | `modules/hall-ui/public/watch.html:61` |
|
|
138
|
-
| `303f8d90266f` | certain | Achievements could not be loaded. | `modules/hall-ui/public/builders.js:
|
|
138
|
+
| `303f8d90266f` | certain | Achievements could not be loaded. | `modules/hall-ui/public/builders.js:1158` |
|
|
139
139
|
| `6ca31dd5eb91` | certain | Acknowledge the primer | `modules/hall-ui/public/primer.html:99` |
|
|
140
140
|
| `4461b13f95c2` | certain | Actions apply at once. Closing stops and deletes the box; you can start a fresh one anytime. | `modules/hall-ui/public/settings.html:209` |
|
|
141
141
|
| `524188fe0078` | certain | Active sessions | `modules/hall-ui/public/settings.html:162` |
|
|
@@ -445,7 +445,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
445
445
|
| `529460e82058` | certain | Goal invitations to you | `modules/hall-ui/public/goal-inbox.js:113` |
|
|
446
446
|
| `e2a9e33abe60` | certain | Goal tasks | `modules/hall-ui/public/goals.html:136` |
|
|
447
447
|
| `e0bc28d10785` | certain | Goals could not be loaded. Refresh to try again. | `modules/hall-ui/public/goals-page.js:890` |
|
|
448
|
-
| `4517f2253d0f` | certain | Goals couldn’t be loaded just now. Refresh to try again. | `modules/hall-ui/public/goals.js:
|
|
448
|
+
| `4517f2253d0f` | certain | Goals couldn’t be loaded just now. Refresh to try again. | `modules/hall-ui/public/goals.js:121` |
|
|
449
449
|
| `6a85997bda21` | certain | Goals created | `modules/hall-ui/public/profile.html:145` |
|
|
450
450
|
| `2ed2cb8fbffd` | certain | Goals page | `modules/hall-ui/public/goals.js:94` |
|
|
451
451
|
| `320a0516a33e` | certain | Goals seeded — awaiting kickoff | `modules/hall-ui/public/roadmap.js:205` |
|
|
@@ -907,7 +907,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
907
907
|
| `381f29be7497` | certain | The most recently shipped tasks. | `modules/hall-ui/public/work.html:108` |
|
|
908
908
|
| `82479c539fca` | certain | The name shown beside your work — in the roster, the leaderboard, and the work feed. Your sign-in and the cred | `modules/hall-ui/public/settings.html:75` |
|
|
909
909
|
| `b88dd8b96b72` | certain | The next person to notice this sentence reads your note instead of re-litigating it. | `modules/hall-ui/public/copy-desk.html:135` |
|
|
910
|
-
| `aa716f3dd551` | certain | The page could not be loaded. Try again. | `modules/hall-ui/public/builders.js:
|
|
910
|
+
| `aa716f3dd551` | certain | The page could not be loaded. Try again. | `modules/hall-ui/public/builders.js:1425` |
|
|
911
911
|
| `74bc22f38140` | certain | The platform confirmed this request came from the account it names. | `modules/hall-ui/public/watch.js:349` |
|
|
912
912
|
| `6c3d88de42de` | certain | The primer | `modules/hall-ui/public/primer.html:41` |
|
|
913
913
|
| `dea378cd4837` | certain | The primer did not load | `modules/hall-ui/public/primer.html:88` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -2211,7 +2211,7 @@
|
|
|
2211
2211
|
"surface": "builders-hall",
|
|
2212
2212
|
"text": "Achievements could not be loaded.",
|
|
2213
2213
|
"file": "modules/hall-ui/public/builders.js",
|
|
2214
|
-
"line":
|
|
2214
|
+
"line": 1158,
|
|
2215
2215
|
"origin": "js-markup",
|
|
2216
2216
|
"confidence": "certain"
|
|
2217
2217
|
},
|
|
@@ -5001,7 +5001,7 @@
|
|
|
5001
5001
|
"surface": "builders-hall",
|
|
5002
5002
|
"text": "Goals couldn’t be loaded just now. Refresh to try again.",
|
|
5003
5003
|
"file": "modules/hall-ui/public/goals.js",
|
|
5004
|
-
"line":
|
|
5004
|
+
"line": 121,
|
|
5005
5005
|
"origin": "js-markup",
|
|
5006
5006
|
"confidence": "certain"
|
|
5007
5007
|
},
|
|
@@ -9159,7 +9159,7 @@
|
|
|
9159
9159
|
"surface": "builders-hall",
|
|
9160
9160
|
"text": "The page could not be loaded. Try again.",
|
|
9161
9161
|
"file": "modules/hall-ui/public/builders.js",
|
|
9162
|
-
"line":
|
|
9162
|
+
"line": 1425,
|
|
9163
9163
|
"origin": "js-notice",
|
|
9164
9164
|
"confidence": "certain"
|
|
9165
9165
|
},
|
|
@@ -1589,5 +1589,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1589
1589
|
landed since 1.19.568 with no explicit bump. run 34058387853. (task 1002620)
|
|
1590
1590
|
1.19.570 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1591
1591
|
landed since 1.19.569 with no explicit bump. run 34060442118. (task 1002620)
|
|
1592
|
+
1.19.571 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1593
|
+
landed since 1.19.570 with no explicit bump. run 34065943703. (task 1002620)
|
|
1594
|
+
1.19.572 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1595
|
+
landed since 1.19.571 with no explicit bump. run 34071078624. (task 1002620)
|
|
1592
1596
|
---------------------------------------------------------------------------
|
|
1593
1597
|
```
|
|
@@ -1044,16 +1044,20 @@
|
|
|
1044
1044
|
|
|
1045
1045
|
// ---- bootstrap -----------------------------------------------------------
|
|
1046
1046
|
|
|
1047
|
-
|
|
1047
|
+
// `prefetchedMe` is boot()'s already-resolved /me — the SAME payload, handed in
|
|
1048
|
+
// so the page does not ask twice. Called with nothing (a re-render), it fetches.
|
|
1049
|
+
async function loadAll(prefetchedMe) {
|
|
1048
1050
|
// Profile first. If /me returns 401, render an unauth landing card with a
|
|
1049
1051
|
// Sign-in button instead of bouncing straight to GitHub — gives the visitor
|
|
1050
1052
|
// a beat of orientation about what they're walking into.
|
|
1051
|
-
let me;
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1053
|
+
let me = prefetchedMe || null;
|
|
1054
|
+
if (!me) {
|
|
1055
|
+
try {
|
|
1056
|
+
me = await getJSON(`${API}/me`, { softAuth: true });
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
if (err && err.code === AUTH_NEEDED) { renderLanding(); return; }
|
|
1059
|
+
throw err;
|
|
1060
|
+
}
|
|
1057
1061
|
}
|
|
1058
1062
|
// Signed in: drop the landing card if a previous unauth render left it up.
|
|
1059
1063
|
const landingRoot = $('#landing-root');
|
|
@@ -1374,7 +1378,31 @@
|
|
|
1374
1378
|
get newcomerGated() { return newcomerGated; },
|
|
1375
1379
|
});
|
|
1376
1380
|
|
|
1377
|
-
|
|
1381
|
+
// boot — AUTH FIRST, then the page (task 1003673).
|
|
1382
|
+
//
|
|
1383
|
+
// The front door has exactly two shapes, and which one this is must be settled
|
|
1384
|
+
// BEFORE anything else runs. A signed-out visitor gets the landing (the door:
|
|
1385
|
+
// sign in, or request access) and nothing else; a builder gets the hall.
|
|
1386
|
+
//
|
|
1387
|
+
// Why the order is load-bearing: mountHallWidgets() mounts every registered
|
|
1388
|
+
// widget, and a widget's mount() reads its own data. Any such read that treats
|
|
1389
|
+
// a 401 as "go and sign in" NAVIGATES — and mounting before /me resolved gave
|
|
1390
|
+
// those reads a head start on renderLanding(), so the signed-out visitor was
|
|
1391
|
+
// bounced to GitHub, refused as a first-timer ("request access first"), and
|
|
1392
|
+
// sent back here to be bounced again. A closed loop: the request form was
|
|
1393
|
+
// unreachable, so an invite-only instance could never admit its first builder.
|
|
1394
|
+
// Widgets are for members, so a signed-out visitor simply never mounts them
|
|
1395
|
+
// (renderLanding hides those sections anyway) and nothing can steal the page.
|
|
1396
|
+
async function boot() {
|
|
1397
|
+
let me = null;
|
|
1398
|
+
try {
|
|
1399
|
+
me = await getJSON(`${API}/me`, { softAuth: true });
|
|
1400
|
+
} catch (err) {
|
|
1401
|
+
if (!(err && err.code === AUTH_NEEDED)) throw err;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
if (!me) { renderLanding(); return; }
|
|
1405
|
+
|
|
1378
1406
|
// BV1.R75: stamp the registry's <section>s into <main> BEFORE wiring events —
|
|
1379
1407
|
// wireEvents() + the data-render code address the widgets by their static ids,
|
|
1380
1408
|
// which only exist once the registry has mounted them.
|
|
@@ -1386,11 +1414,12 @@
|
|
|
1386
1414
|
// #731: the Discord connect/link card moved to /builders/settings, and the
|
|
1387
1415
|
// OAuth callback now lands there — so the ?discord=joined|linked toast is
|
|
1388
1416
|
// handled by settings.js, not here.
|
|
1389
|
-
loadAll()
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1417
|
+
await loadAll(me);
|
|
1418
|
+
if (window.__buildersSelfId) startLiveUpdates();
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1422
|
+
boot().catch((err) => {
|
|
1394
1423
|
if (err && err.message === 'redirecting to auth') return;
|
|
1395
1424
|
console.error('[builders] load failed', err);
|
|
1396
1425
|
toast('The page could not be loaded. Try again.');
|
|
@@ -101,7 +101,13 @@
|
|
|
101
101
|
const me = await getJSON(`${API}/me`, { softAuth: true }).catch(() => null);
|
|
102
102
|
myId = me && me.builder ? me.builder.id : null;
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
// softAuth (task 1003673): a widget's own boot read may NEVER navigate.
|
|
105
|
+
// This ran at mount time and its 401 hard-redirected to GitHub, which is
|
|
106
|
+
// what stole the page from a signed-out visitor before the landing could
|
|
107
|
+
// paint. boot() no longer mounts widgets for a signed-out visitor; this
|
|
108
|
+
// keeps the widget itself harmless if it is ever mounted without a session
|
|
109
|
+
// (an expired cookie mid-visit), where the honest answer is to say nothing.
|
|
110
|
+
const gres = await getJSON(`${API}/goals`, { softAuth: true });
|
|
105
111
|
const candidates = (gres.goals || []).filter((g) => g.status !== 'archived');
|
|
106
112
|
const details = (await Promise.all(candidates.map((g) =>
|
|
107
113
|
getJSON(`${API}/goals/${encodeURIComponent(g.id)}`).catch(() => null)))).filter(Boolean);
|
|
@@ -109,6 +115,9 @@
|
|
|
109
115
|
render(joined);
|
|
110
116
|
} catch (err) {
|
|
111
117
|
const body = host.querySelector('#goals-body');
|
|
118
|
+
// Signed out is not a failure — it is simply nothing to show. Only a real
|
|
119
|
+
// error earns the retry line (and the console noise).
|
|
120
|
+
if (err && err.code === 'auth-needed') { if (body) body.innerHTML = ''; return; }
|
|
112
121
|
if (body) body.innerHTML = '<p class="goal-empty">Goals couldn’t be loaded just now. Refresh to try again.</p>';
|
|
113
122
|
console.error('[hall] goals load failed', err);
|
|
114
123
|
}
|