@bongos/core 1.19.505 → 1.19.506
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 +21 -16
- package/docs/copy-inventory.md +11 -14
- package/docs/copy-registry.json +32 -49
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/sessions.html +10 -6
- package/modules/hall-ui/public/sessions.js +101 -40
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hall_audit.mjs +38 -0
- package/tests/sessions_kit_pager.mjs +312 -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.506",
|
|
6
|
+
"core_contract": "1.19.506",
|
|
7
|
+
"source_commit": "0c5102c78cf4acf0c5ab19a0f34ab43caa0a53d0",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-03T17:
|
|
9
|
+
"built_at": "2026-09-03T17:56:16.813Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 427,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1945,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2396,
|
|
20
|
+
"tree_sha256": "d0ec8df4b7c843085a5692c55b908977f2bbc25023e21dc62e246e62cfdfbd0d",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1727,12 +1727,12 @@
|
|
|
1727
1727
|
{
|
|
1728
1728
|
"path": "docs/copy-inventory.md",
|
|
1729
1729
|
"mode": "0000644",
|
|
1730
|
-
"sha256": "
|
|
1730
|
+
"sha256": "6f3476adddccabea94bb3bb8af804ec1e6dfc1710a9e3f76fb4ba062b44e3234"
|
|
1731
1731
|
},
|
|
1732
1732
|
{
|
|
1733
1733
|
"path": "docs/copy-registry.json",
|
|
1734
1734
|
"mode": "0000644",
|
|
1735
|
-
"sha256": "
|
|
1735
|
+
"sha256": "d6f13f1ab6d579e195b3240511d68181aabd64591933f1911fc0009fbdf875d8"
|
|
1736
1736
|
},
|
|
1737
1737
|
{
|
|
1738
1738
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2577,7 +2577,7 @@
|
|
|
2577
2577
|
{
|
|
2578
2578
|
"path": "docs/module-api-changelog.md",
|
|
2579
2579
|
"mode": "0000644",
|
|
2580
|
-
"sha256": "
|
|
2580
|
+
"sha256": "130aad5d413e749405b9f5104259cdfb9dfde539ae35326649c64e79c5ff6604"
|
|
2581
2581
|
},
|
|
2582
2582
|
{
|
|
2583
2583
|
"path": "docs/modules-contract.md",
|
|
@@ -4997,12 +4997,12 @@
|
|
|
4997
4997
|
{
|
|
4998
4998
|
"path": "modules/hall-ui/public/sessions.html",
|
|
4999
4999
|
"mode": "0000644",
|
|
5000
|
-
"sha256": "
|
|
5000
|
+
"sha256": "160985637f34bbf424e9333af158b260aad268e4a1dd9f22cf4da789283746b2"
|
|
5001
5001
|
},
|
|
5002
5002
|
{
|
|
5003
5003
|
"path": "modules/hall-ui/public/sessions.js",
|
|
5004
5004
|
"mode": "0000644",
|
|
5005
|
-
"sha256": "
|
|
5005
|
+
"sha256": "9470e5816bcb5aa0a77856413df9e20e5d5aa1a1948a01d41810037e8355e7f9"
|
|
5006
5006
|
},
|
|
5007
5007
|
{
|
|
5008
5008
|
"path": "modules/hall-ui/public/sessions.states.json",
|
|
@@ -7227,12 +7227,12 @@
|
|
|
7227
7227
|
{
|
|
7228
7228
|
"path": "package-lock.json",
|
|
7229
7229
|
"mode": "0000644",
|
|
7230
|
-
"sha256": "
|
|
7230
|
+
"sha256": "7a6dd18246bdbc4fbf57c52d54246a17ce2f5fb144bb256a6a163804175a8be3"
|
|
7231
7231
|
},
|
|
7232
7232
|
{
|
|
7233
7233
|
"path": "package.json",
|
|
7234
7234
|
"mode": "0000644",
|
|
7235
|
-
"sha256": "
|
|
7235
|
+
"sha256": "26b904a0eb436c1b3654cac79c075a39ccd61bd9db7d3eab8c96f43507fd2686"
|
|
7236
7236
|
},
|
|
7237
7237
|
{
|
|
7238
7238
|
"path": "public-docs/index.html",
|
|
@@ -8902,7 +8902,7 @@
|
|
|
8902
8902
|
{
|
|
8903
8903
|
"path": "src/module-api.js",
|
|
8904
8904
|
"mode": "0000644",
|
|
8905
|
-
"sha256": "
|
|
8905
|
+
"sha256": "818b1e08d902b65dedaf1d670b920a12f8eedbc3191051c4977675ec34a4e8d6"
|
|
8906
8906
|
},
|
|
8907
8907
|
{
|
|
8908
8908
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10282,7 +10282,7 @@
|
|
|
10282
10282
|
{
|
|
10283
10283
|
"path": "tests/hall_audit.mjs",
|
|
10284
10284
|
"mode": "0000644",
|
|
10285
|
-
"sha256": "
|
|
10285
|
+
"sha256": "7df5602bb92b44186613870501ecb4e959c9288f8d103cfc8075a3a24d51d840"
|
|
10286
10286
|
},
|
|
10287
10287
|
{
|
|
10288
10288
|
"path": "tests/hall_board_world.mjs",
|
|
@@ -11524,6 +11524,11 @@
|
|
|
11524
11524
|
"mode": "0000644",
|
|
11525
11525
|
"sha256": "a630ac72981f230e8aa6609152765c13e8928ac329cb0ff3fe615371f31208d2"
|
|
11526
11526
|
},
|
|
11527
|
+
{
|
|
11528
|
+
"path": "tests/sessions_kit_pager.mjs",
|
|
11529
|
+
"mode": "0000644",
|
|
11530
|
+
"sha256": "ce4a6f81b17821ed34710f2e3f616eeb51655973b3c9b2537b74f7c371ef0719"
|
|
11531
|
+
},
|
|
11527
11532
|
{
|
|
11528
11533
|
"path": "tests/setup_device_flow_fallback.mjs",
|
|
11529
11534
|
"mode": "0000644",
|
package/docs/copy-inventory.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
> Deterministic on a given tree — regenerate and commit rather than hand-editing.
|
|
5
5
|
> Machine-readable twin: [`copy-registry.json`](copy-registry.json).
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**1623 strings** across 3 surfaces · 1412 distinct pieces of copy · 1623 certain, 0 heuristic · 151 shown in more than one place.
|
|
8
8
|
|
|
9
9
|
## Surfaces
|
|
10
10
|
|
|
11
11
|
| surface | lives at | files | strings |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| `builders-hall` | `modules/hall-ui/public` | 73 |
|
|
13
|
+
| `builders-hall` | `modules/hall-ui/public` | 73 | 1180 |
|
|
14
14
|
| `status-dashboard` | `modules/status-ui/public` | 4 | 98 |
|
|
15
15
|
| `landing` | `modules/public-landing/public` | 5 | 345 |
|
|
16
16
|
|
|
@@ -76,9 +76,9 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
76
76
|
| `f6bb75b08e98` | 2x | Back to {…} | modules/hall-ui/public/task.js:188, modules/hall-ui/public/task.js:197 |
|
|
77
77
|
| `60b248791dc3` | 2x | Builders — {{worldName}} | modules/hall-ui/public/roster.html:6, modules/hall-ui/public/roster.html:6 |
|
|
78
78
|
| `563d29ed86d8` | 2x | Bulk actions | modules/hall-ui/public/goals.html:172, modules/hall-ui/public/work.html:129 |
|
|
79
|
-
| … | |
|
|
79
|
+
| … | | _111 more in the JSON registry_ | |
|
|
80
80
|
|
|
81
|
-
## `builders-hall` —
|
|
81
|
+
## `builders-hall` — 1180 strings
|
|
82
82
|
|
|
83
83
|
| id | conf | text | where |
|
|
84
84
|
|---|---|---|---|
|
|
@@ -284,8 +284,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
284
284
|
| `589a4cfc5442` | certain | Could not load idea {…}. | `modules/hall-ui/public/ideas.js:629` |
|
|
285
285
|
| `192c893fc64e` | certain | Could not load modules. | `modules/hall-ui/public/modules.js:318` |
|
|
286
286
|
| `8a4b590fa1a0` | certain | Could not load requests. | `modules/hall-ui/public/collab.js:173` |
|
|
287
|
-
| `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:
|
|
288
|
-
| `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:
|
|
287
|
+
| `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:243` |
|
|
288
|
+
| `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:281` |
|
|
289
289
|
| `b2147cc90e36` | certain | Could not load the idea template. Reload, or file from the terminal with capture.js. | `modules/hall-ui/public/ideas.js:611` |
|
|
290
290
|
| `7604af13c40e` | certain | Could not load the profile. | `modules/hall-ui/public/profile.js:759` |
|
|
291
291
|
| `8b69e445592a` | certain | Could not load the question labels; showing the answers as stored. | `modules/hall-ui/public/idea.js:244` |
|
|
@@ -330,7 +330,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
330
330
|
| `b1602d01f4bb` | certain | Could not unblock: {…} | `modules/hall-ui/public/harbor.js:187` |
|
|
331
331
|
| `3db890b074a7` | certain | Could not update visibility just now. | `modules/hall-ui/public/goals-render.js:1043` |
|
|
332
332
|
| `112d12b24879` | certain | Could not verify your access. Go back and try again. | `modules/hall-ui/public/watch.js:1010` |
|
|
333
|
-
| `6bd74fd20311` | certain | Could not verify your access. Try again. | `modules/hall-ui/public/sessions.js:
|
|
333
|
+
| `6bd74fd20311` | certain | Could not verify your access. Try again. | `modules/hall-ui/public/sessions.js:280` |
|
|
334
334
|
| `278e8281c82f` | certain | Could not vote: {…} | `modules/hall-ui/public/government.js:990` |
|
|
335
335
|
| `a48e9f098392` | certain | Could not {…} just now. | `modules/hall-ui/public/goals-render.js:1029` |
|
|
336
336
|
| `cb09ee26014f` | certain | Could not {…}: {…} | `modules/hall-ui/public/government.js:321` |
|
|
@@ -559,7 +559,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
559
559
|
| `9af4b7a3a36f` | certain | Module truth | `modules/hall-ui/public/atlas.html:48` |
|
|
560
560
|
| `319e9db996ea` | certain | Modules — {{worldName}} | `modules/hall-ui/public/modules.html:6` |
|
|
561
561
|
| `319e9db996ea` | certain | Modules — {{worldName}} | `modules/hall-ui/public/modules.html:6` |
|
|
562
|
-
| `1ae18e58fead` | certain | Most recent first. Each load of this page is itself recorded in the audit log. | `modules/hall-ui/public/sessions.js:
|
|
562
|
+
| `1ae18e58fead` | certain | Most recent first. Each load of this page is itself recorded in the audit log. | `modules/hall-ui/public/sessions.js:196` |
|
|
563
563
|
| `f14a91b3f625` | certain | Most recent first. Each row is one recorded work session; every id on it opens its record. | `modules/hall-ui/public/sessions.html:56` |
|
|
564
564
|
| `e52ae74f0052` | certain | Move to goal | `modules/hall-ui/public/task.js:510` |
|
|
565
565
|
| `ae84ebe1d735` | certain | Name cannot be blank. | `modules/hall-ui/public/settings.js:831` |
|
|
@@ -618,7 +618,6 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
618
618
|
| `ad9a17a8a5eb` | certain | Nothing shipped yet. | `modules/hall-ui/public/work.js:1016` |
|
|
619
619
|
| `f326632f2013` | certain | Nothing to submit — every module here ships with the core. | `modules/hall-ui/public/modules.js:258` |
|
|
620
620
|
| `9d25ba1d2f17` | certain | Off voice — says it in the wrong register | `modules/hall-ui/public/copy-desk.html:89` |
|
|
621
|
-
| `55bc72820efc` | certain | Older → | `modules/hall-ui/public/sessions.html:70` |
|
|
622
621
|
| `b89ad49770e7` | certain | On Windows, run | `modules/hall-ui/public/hall-render.js:811` |
|
|
623
622
|
| `11a1905c76b2` | certain | On the board | `modules/hall-ui/public/hall-render.js:556` |
|
|
624
623
|
| `ea1f179dd878` | certain | On this page | `modules/hall-ui/public/drachmae.html:51` |
|
|
@@ -650,7 +649,6 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
650
649
|
| `695ffff13730` | certain | Over time | `modules/hall-ui/public/profile.html:72` |
|
|
651
650
|
| `5ded44da0cc5` | certain | Override requests | `modules/hall-ui/public/watch.html:139` |
|
|
652
651
|
| `15a49bf15007` | certain | Page {…} of {…} | `modules/hall-ui/public/hall-kit.js:109` |
|
|
653
|
-
| `d1cd74b39c2e` | certain | Page {…} of {…} · {…}–{…} of {…} | `modules/hall-ui/public/sessions.js:143` |
|
|
654
652
|
| `8595b0aa76a0` | certain | Pair Claude Desktop | `modules/hall-ui/public/settings.html:242` |
|
|
655
653
|
| `cf68a394cfa5` | certain | Paste command copied. | `modules/hall-ui/public/settings.js:996` |
|
|
656
654
|
| `a495649d1f82` | certain | Paste your key first. | `modules/hall-ui/public/settings.js:1117` |
|
|
@@ -774,7 +772,6 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
774
772
|
| `ae43a132699b` | certain | Selectable today | `modules/hall-ui/public/government.js:721` |
|
|
775
773
|
| `d14e8e9acaff` | certain | Session ended. | `modules/hall-ui/public/settings-sessions.js:81` |
|
|
776
774
|
| `55e0a34bafed` | certain | Session log{…} | `modules/hall-ui/public/task.js:447` |
|
|
777
|
-
| `3f8f6c62adb8` | certain | Session pages | `modules/hall-ui/public/sessions.html:67` |
|
|
778
775
|
| `cccb362bd6da` | certain | Sessions — {{worldName}} | `modules/hall-ui/public/sessions.html:6` |
|
|
779
776
|
| `cccb362bd6da` | certain | Sessions — {{worldName}} | `modules/hall-ui/public/sessions.html:6` |
|
|
780
777
|
| `b823d9a37a35` | certain | Set up key storage | `modules/hall-ui/public/settings.js:1069` |
|
|
@@ -878,7 +875,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
878
875
|
| `38ce5d68c182` | certain | The roster | `modules/hall-ui/public/roster.html:36` |
|
|
879
876
|
| `dd6a926b8137` | certain | The same sentence, in several places. Sometimes that is right and the answer is to dismiss it; sometimes it is | `modules/hall-ui/public/copy-desk.html:57` |
|
|
880
877
|
| `cd4ceb6e7e48` | certain | The seeded plan | `modules/hall-ui/public/roadmap.js:194` |
|
|
881
|
-
| `d01d36a596b0` | certain | The session log could not be loaded. Try again. | `modules/hall-ui/public/sessions.js:
|
|
878
|
+
| `d01d36a596b0` | certain | The session log could not be loaded. Try again. | `modules/hall-ui/public/sessions.js:242` |
|
|
879
|
+
| `d01d36a596b0` | certain | The session log could not be loaded. Try again. | `modules/hall-ui/public/sessions.js:294` |
|
|
882
880
|
| `e48a9b77d91b` | certain | The session log opens from a higher rank. | `modules/hall-ui/public/sessions.html:41` |
|
|
883
881
|
| `af664a644b13` | certain | The seven government structures, for getting a team up to speed before deciding when to flip a switch. | `modules/hall-ui/public/government.js:721` |
|
|
884
882
|
| `6360a5988f26` | certain | The shape and health of the codebase. | `modules/hall-ui/public/watch.html:112` |
|
|
@@ -1205,7 +1203,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1205
1203
|
| `275f3565c78b` | certain | {…} reset to its default {…} permissions. | `modules/hall-ui/public/government.js:304` |
|
|
1206
1204
|
| `33afba17ee0f` | certain | {…} seeded | `modules/hall-ui/public/roadmap.js:194` |
|
|
1207
1205
|
| `732b4c935e36` | certain | {…} sessions | `modules/hall-ui/public/task.js:446` |
|
|
1208
|
-
| `3d76dce19c4a` | certain | {…} session{…} across {…} builder{…}. | `modules/hall-ui/public/sessions.js:
|
|
1206
|
+
| `3d76dce19c4a` | certain | {…} session{…} across {…} builder{…}. | `modules/hall-ui/public/sessions.js:194` |
|
|
1209
1207
|
| `e08fa22622f0` | certain | {…} shipped, {…} confirmed, {…} completed, {…} remaining of {…} | `modules/hall-ui/public/roadmap.js:74` |
|
|
1210
1208
|
| `e08fa22622f0` | certain | {…} shipped, {…} confirmed, {…} completed, {…} remaining of {…} | `modules/hall-ui/public/roadmap.js:273` |
|
|
1211
1209
|
| `ad27ae9f5433` | certain | {…} tasks · {…} | `modules/hall-ui/public/roadmap.js:390` |
|
|
@@ -1253,7 +1251,6 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1253
1251
|
| `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:184` |
|
|
1254
1252
|
| `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:195` |
|
|
1255
1253
|
| `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:207` |
|
|
1256
|
-
| `1e7cf7b1e64e` | certain | ← Newer | `modules/hall-ui/public/sessions.html:68` |
|
|
1257
1254
|
| `2180e0ecf138` | certain | → no idea # | `modules/hall-ui/public/task-detail.js:484` |
|
|
1258
1255
|
| `d5d76f1c0dfe` | certain | ○ reserved | `modules/hall-ui/public/diagrams.html:44` |
|
|
1259
1256
|
| `97b0773e72a5` | certain | ● LIVE | `modules/hall-ui/public/diagrams.html:42` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
"generator": "scripts/gds/copy-inventory.js",
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
|
-
"strings":
|
|
7
|
-
"unique_ids":
|
|
8
|
-
"certain":
|
|
6
|
+
"strings": 1623,
|
|
7
|
+
"unique_ids": 1412,
|
|
8
|
+
"certain": 1623,
|
|
9
9
|
"heuristic": 0,
|
|
10
|
-
"duplicate_ids":
|
|
10
|
+
"duplicate_ids": 151
|
|
11
11
|
},
|
|
12
12
|
"surfaces": {
|
|
13
13
|
"builders-hall": {
|
|
14
14
|
"at": "modules/hall-ui/public",
|
|
15
15
|
"files": 73,
|
|
16
|
-
"strings":
|
|
16
|
+
"strings": 1180
|
|
17
17
|
},
|
|
18
18
|
"status-dashboard": {
|
|
19
19
|
"at": "modules/status-ui/public",
|
|
@@ -604,8 +604,8 @@
|
|
|
604
604
|
"text": "Could not load sessions.",
|
|
605
605
|
"occurrences": 2,
|
|
606
606
|
"at": [
|
|
607
|
-
"modules/hall-ui/public/sessions.js:
|
|
608
|
-
"modules/hall-ui/public/sessions.js:
|
|
607
|
+
"modules/hall-ui/public/sessions.js:243",
|
|
608
|
+
"modules/hall-ui/public/sessions.js:281"
|
|
609
609
|
]
|
|
610
610
|
},
|
|
611
611
|
{
|
|
@@ -1268,6 +1268,16 @@
|
|
|
1268
1268
|
"modules/hall-ui/public/primer.html:89"
|
|
1269
1269
|
]
|
|
1270
1270
|
},
|
|
1271
|
+
{
|
|
1272
|
+
"id": "d01d36a596b0",
|
|
1273
|
+
"surface": "builders-hall",
|
|
1274
|
+
"text": "The session log could not be loaded. Try again.",
|
|
1275
|
+
"occurrences": 2,
|
|
1276
|
+
"at": [
|
|
1277
|
+
"modules/hall-ui/public/sessions.js:242",
|
|
1278
|
+
"modules/hall-ui/public/sessions.js:294"
|
|
1279
|
+
]
|
|
1280
|
+
},
|
|
1271
1281
|
{
|
|
1272
1282
|
"id": "e9a366866022",
|
|
1273
1283
|
"surface": "builders-hall",
|
|
@@ -3423,7 +3433,7 @@
|
|
|
3423
3433
|
"surface": "builders-hall",
|
|
3424
3434
|
"text": "Could not load sessions.",
|
|
3425
3435
|
"file": "modules/hall-ui/public/sessions.js",
|
|
3426
|
-
"line":
|
|
3436
|
+
"line": 243,
|
|
3427
3437
|
"origin": "js-notice",
|
|
3428
3438
|
"confidence": "certain"
|
|
3429
3439
|
},
|
|
@@ -3432,7 +3442,7 @@
|
|
|
3432
3442
|
"surface": "builders-hall",
|
|
3433
3443
|
"text": "Could not load sessions.",
|
|
3434
3444
|
"file": "modules/hall-ui/public/sessions.js",
|
|
3435
|
-
"line":
|
|
3445
|
+
"line": 281,
|
|
3436
3446
|
"origin": "js-notice",
|
|
3437
3447
|
"confidence": "certain"
|
|
3438
3448
|
},
|
|
@@ -3837,7 +3847,7 @@
|
|
|
3837
3847
|
"surface": "builders-hall",
|
|
3838
3848
|
"text": "Could not verify your access. Try again.",
|
|
3839
3849
|
"file": "modules/hall-ui/public/sessions.js",
|
|
3840
|
-
"line":
|
|
3850
|
+
"line": 280,
|
|
3841
3851
|
"origin": "js-text-assign",
|
|
3842
3852
|
"confidence": "certain"
|
|
3843
3853
|
},
|
|
@@ -5898,7 +5908,7 @@
|
|
|
5898
5908
|
"surface": "builders-hall",
|
|
5899
5909
|
"text": "Most recent first. Each load of this page is itself recorded in the audit log.",
|
|
5900
5910
|
"file": "modules/hall-ui/public/sessions.js",
|
|
5901
|
-
"line":
|
|
5911
|
+
"line": 196,
|
|
5902
5912
|
"origin": "js-text-assign",
|
|
5903
5913
|
"confidence": "certain"
|
|
5904
5914
|
},
|
|
@@ -6424,15 +6434,6 @@
|
|
|
6424
6434
|
"origin": "html-text",
|
|
6425
6435
|
"confidence": "certain"
|
|
6426
6436
|
},
|
|
6427
|
-
{
|
|
6428
|
-
"id": "55bc72820efc",
|
|
6429
|
-
"surface": "builders-hall",
|
|
6430
|
-
"text": "Older →",
|
|
6431
|
-
"file": "modules/hall-ui/public/sessions.html",
|
|
6432
|
-
"line": 70,
|
|
6433
|
-
"origin": "html-text",
|
|
6434
|
-
"confidence": "certain"
|
|
6435
|
-
},
|
|
6436
6437
|
{
|
|
6437
6438
|
"id": "b89ad49770e7",
|
|
6438
6439
|
"surface": "builders-hall",
|
|
@@ -6712,15 +6713,6 @@
|
|
|
6712
6713
|
"origin": "js-text-assign",
|
|
6713
6714
|
"confidence": "certain"
|
|
6714
6715
|
},
|
|
6715
|
-
{
|
|
6716
|
-
"id": "d1cd74b39c2e",
|
|
6717
|
-
"surface": "builders-hall",
|
|
6718
|
-
"text": "Page {…} of {…} · {…}–{…} of {…}",
|
|
6719
|
-
"file": "modules/hall-ui/public/sessions.js",
|
|
6720
|
-
"line": 143,
|
|
6721
|
-
"origin": "js-text-assign",
|
|
6722
|
-
"confidence": "certain"
|
|
6723
|
-
},
|
|
6724
6716
|
{
|
|
6725
6717
|
"id": "8595b0aa76a0",
|
|
6726
6718
|
"surface": "builders-hall",
|
|
@@ -7828,15 +7820,6 @@
|
|
|
7828
7820
|
"origin": "js-markup",
|
|
7829
7821
|
"confidence": "certain"
|
|
7830
7822
|
},
|
|
7831
|
-
{
|
|
7832
|
-
"id": "3f8f6c62adb8",
|
|
7833
|
-
"surface": "builders-hall",
|
|
7834
|
-
"text": "Session pages",
|
|
7835
|
-
"file": "modules/hall-ui/public/sessions.html",
|
|
7836
|
-
"line": 67,
|
|
7837
|
-
"origin": "html-attr:aria-label",
|
|
7838
|
-
"confidence": "certain"
|
|
7839
|
-
},
|
|
7840
7823
|
{
|
|
7841
7824
|
"id": "cccb362bd6da",
|
|
7842
7825
|
"surface": "builders-hall",
|
|
@@ -8769,7 +8752,16 @@
|
|
|
8769
8752
|
"surface": "builders-hall",
|
|
8770
8753
|
"text": "The session log could not be loaded. Try again.",
|
|
8771
8754
|
"file": "modules/hall-ui/public/sessions.js",
|
|
8772
|
-
"line":
|
|
8755
|
+
"line": 242,
|
|
8756
|
+
"origin": "js-text-assign",
|
|
8757
|
+
"confidence": "certain"
|
|
8758
|
+
},
|
|
8759
|
+
{
|
|
8760
|
+
"id": "d01d36a596b0",
|
|
8761
|
+
"surface": "builders-hall",
|
|
8762
|
+
"text": "The session log could not be loaded. Try again.",
|
|
8763
|
+
"file": "modules/hall-ui/public/sessions.js",
|
|
8764
|
+
"line": 294,
|
|
8773
8765
|
"origin": "js-text-assign",
|
|
8774
8766
|
"confidence": "certain"
|
|
8775
8767
|
},
|
|
@@ -11712,7 +11704,7 @@
|
|
|
11712
11704
|
"surface": "builders-hall",
|
|
11713
11705
|
"text": "{…} session{…} across {…} builder{…}.",
|
|
11714
11706
|
"file": "modules/hall-ui/public/sessions.js",
|
|
11715
|
-
"line":
|
|
11707
|
+
"line": 194,
|
|
11716
11708
|
"origin": "js-text-assign",
|
|
11717
11709
|
"confidence": "certain"
|
|
11718
11710
|
},
|
|
@@ -12139,15 +12131,6 @@
|
|
|
12139
12131
|
"origin": "html-text",
|
|
12140
12132
|
"confidence": "certain"
|
|
12141
12133
|
},
|
|
12142
|
-
{
|
|
12143
|
-
"id": "1e7cf7b1e64e",
|
|
12144
|
-
"surface": "builders-hall",
|
|
12145
|
-
"text": "← Newer",
|
|
12146
|
-
"file": "modules/hall-ui/public/sessions.html",
|
|
12147
|
-
"line": 68,
|
|
12148
|
-
"origin": "html-text",
|
|
12149
|
-
"confidence": "certain"
|
|
12150
|
-
},
|
|
12151
12134
|
{
|
|
12152
12135
|
"id": "2180e0ecf138",
|
|
12153
12136
|
"surface": "builders-hall",
|
|
@@ -1447,5 +1447,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1447
1447
|
landed since 1.19.503 with no explicit bump. run 33784387586. (task 1002620)
|
|
1448
1448
|
1.19.505 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1449
1449
|
landed since 1.19.504 with no explicit bump. run 33785121177. (task 1002620)
|
|
1450
|
+
1.19.506 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1451
|
+
landed since 1.19.505 with no explicit bump. run 33787453336. (task 1002620)
|
|
1450
1452
|
---------------------------------------------------------------------------
|
|
1451
1453
|
```
|
|
@@ -64,11 +64,11 @@
|
|
|
64
64
|
</thead>
|
|
65
65
|
<tbody id="sessions-body"></tbody>
|
|
66
66
|
</table>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
<!-- The kit stamps the pager bar here (sessions.js, OTBKit.pagerBarHtml).
|
|
68
|
+
makeLedger is deliberately NOT used: it stamps table + pager into one
|
|
69
|
+
mount, and this page keeps them separate so the bar sits below the
|
|
70
|
+
ledger rather than inside whatever scrollport the table is in. -->
|
|
71
|
+
<div id="sessions-pager-mount"></div>
|
|
72
72
|
<p class="ledger__note" id="ledger-foot"></p>
|
|
73
73
|
</section>
|
|
74
74
|
</main>
|
|
@@ -88,7 +88,11 @@
|
|
|
88
88
|
<!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
|
|
89
89
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
90
90
|
<script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
|
|
91
|
+
<!-- The shared component kit — the pager contract this page now uses instead of
|
|
92
|
+
its own (task 1003279-sibling follow-up of 1003281). Must load BEFORE
|
|
93
|
+
sessions.js, which reads window.OTBKit at boot. -->
|
|
94
|
+
<script src="/builders/hall-kit.js"></script>
|
|
91
95
|
<script src="/builders/brand-apply.js?v=2026-06-19-r56"></script>
|
|
92
|
-
<script src="/builders/sessions.js?v=2026-
|
|
96
|
+
<script src="/builders/sessions.js?v=2026-08-28-kitpager"></script>
|
|
93
97
|
</body>
|
|
94
98
|
</html>
|
|
@@ -121,91 +121,146 @@
|
|
|
121
121
|
$('#sessions-scroll').hidden = true;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
// ---- paging
|
|
124
|
+
// ---- paging --------------------------------------------------------------
|
|
125
|
+
//
|
|
126
|
+
// This page used to hand-roll its own server-offset pager, and hall-ui/CLAUDE.md
|
|
127
|
+
// carried it as the ONE sanctioned exception to "no bespoke pager" (audited task
|
|
128
|
+
// 2179) — because the kit could only slice an array the browser already held, and
|
|
129
|
+
// this corpus cannot be held. Task 1003281 added the server-offset mode to the kit
|
|
130
|
+
// and left the migration as its own task; this is it. The exception is gone, so
|
|
131
|
+
// there is one pager contract in the hall again rather than one-plus-a-footnote.
|
|
132
|
+
//
|
|
133
|
+
// What the reader gains: a per-page control that remembers their choice, and a
|
|
134
|
+
// clamp — if the corpus shrinks under an open page, renderServerPage() says so and
|
|
135
|
+
// we re-ask, instead of showing an empty page under a range that claims rows.
|
|
136
|
+
//
|
|
137
|
+
// The one thing NOT taken from the kit is makeLedger: it stamps table+pager into a
|
|
138
|
+
// single mount, and the CARD is this page's scroll container (hall-ui/CLAUDE.md, the
|
|
139
|
+
// sideways-scroll rule), so a pager stamped into the table's mount would slide with
|
|
140
|
+
// the columns. Keeping it a separate sibling below the ledger is what holds it still.
|
|
141
|
+
// (This page used to carry a .ledger-table-wrap scrollport; task 1003470 removed it
|
|
142
|
+
// because the wrapper broke the shell's sticky th. The separate mount outlived it.)
|
|
143
|
+
// Table markup stays in sessions.html; the PAGER is the kit's.
|
|
125
144
|
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
// Guarded the way every other kit consumer guards it (profile.js): the kit is a
|
|
146
|
+
// separate <script>, so a cache miss or a load-order slip must degrade to a named
|
|
147
|
+
// failure rather than an uncaught throw at parse time that also kills load().
|
|
148
|
+
const kit = window.OTBKit;
|
|
149
|
+
const PAGER_PREFIX = 'sessions';
|
|
150
|
+
// 50 was this page's fixed page size before the per-page control existed; keeping it
|
|
151
|
+
// as the default means a returning reader's first page looks exactly as it did.
|
|
152
|
+
const pager = kit ? kit.makePager(PAGER_PREFIX, 'otb.sessions.perPage', { defaultPerPage: 50 }) : null;
|
|
153
|
+
// `page`/`perPage` here are the last values that actually PAINTED. The kit moves the
|
|
154
|
+
// pager first and calls back second, so a failed fetch has to put it back to these —
|
|
155
|
+
// otherwise the bar goes on claiming a page whose rows never arrived. Keeping both
|
|
156
|
+
// (not just the page) means a failed per-page change rolls back cleanly too.
|
|
157
|
+
const state = { total: 0, shown: 0, page: 1, perPage: pager?.perPage ?? 0 };
|
|
128
158
|
let loading = false;
|
|
159
|
+
// Two clicks can be in flight at once; only the newest may paint (the profile
|
|
160
|
+
// activity-log idiom, task 1003281).
|
|
161
|
+
let inFlight = 0;
|
|
129
162
|
|
|
163
|
+
// Paint the kit's bar, then apply the one thing the kit deliberately does not model:
|
|
164
|
+
// a fetch is in flight, so nothing may be pressed again until it lands. Only the
|
|
165
|
+
// DISABLING is ours — re-enabling the arrows is left to the kit, which knows whether
|
|
166
|
+
// there is a page on either side; re-enabling them here would light up "Prev" on
|
|
167
|
+
// page 1. Returns the kit's clamped flag: true = the page outran a shrunken corpus.
|
|
130
168
|
function renderPager() {
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
169
|
+
const clamped = kit.renderServerPage(pager, state.total, state.shown);
|
|
170
|
+
const perPage = $(`#${PAGER_PREFIX}-per-page`);
|
|
171
|
+
if (perPage) perPage.disabled = loading;
|
|
172
|
+
if (loading) {
|
|
173
|
+
for (const id of ['prev', 'next']) {
|
|
174
|
+
const el = $(`#${PAGER_PREFIX}-${id}`);
|
|
175
|
+
if (el) el.disabled = true;
|
|
176
|
+
}
|
|
137
177
|
}
|
|
138
|
-
|
|
139
|
-
const page = Math.floor(state.offset / PAGE_SIZE) + 1;
|
|
140
|
-
const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
141
|
-
const from = state.offset + 1;
|
|
142
|
-
const to = Math.min(state.offset + PAGE_SIZE, total);
|
|
143
|
-
$('#pager-pos').textContent = `Page ${fmtInt(page)} of ${fmtInt(pages)} · ${fmtInt(from)}–${fmtInt(to)} of ${fmtInt(total)}`;
|
|
144
|
-
$('#pager-prev').disabled = loading || state.offset <= 0;
|
|
145
|
-
$('#pager-next').disabled = loading || state.offset + PAGE_SIZE >= total;
|
|
178
|
+
return clamped;
|
|
146
179
|
}
|
|
147
180
|
|
|
181
|
+
// Paint one page. Returns true when the pager clamped — the corpus shrank under an
|
|
182
|
+
// open page, so the caller must re-ask rather than leave a range that lies.
|
|
148
183
|
function showData(data) {
|
|
149
184
|
const sessions = Array.isArray(data.sessions) ? data.sessions : [];
|
|
150
185
|
const totals = data.totals || { total: data.total || sessions.length };
|
|
151
186
|
state.total = num(totals.total);
|
|
187
|
+
state.shown = sessions.length;
|
|
152
188
|
$('#gate-scroll').hidden = true;
|
|
153
189
|
$('#stats-scroll').hidden = false;
|
|
154
190
|
$('#sessions-scroll').hidden = false;
|
|
155
191
|
renderStats(totals);
|
|
156
192
|
renderTable(sessions);
|
|
157
|
-
renderPager();
|
|
193
|
+
const clamped = renderPager();
|
|
158
194
|
$('#ledger-sub').textContent =
|
|
159
195
|
`${fmtInt(totals.total)} session${num(totals.total) === 1 ? '' : 's'} across ${fmtInt(totals.builders)} builder${num(totals.builders) === 1 ? '' : 's'}.`;
|
|
160
196
|
$('#ledger-foot').textContent =
|
|
161
197
|
'Most recent first. Each load of this page is itself recorded in the audit log.';
|
|
198
|
+
return clamped;
|
|
162
199
|
}
|
|
163
200
|
|
|
164
201
|
// ---- load ----------------------------------------------------------------
|
|
165
202
|
|
|
166
|
-
// Fetch
|
|
167
|
-
|
|
203
|
+
// Fetch the page the pager currently points at. Returns true on success.
|
|
204
|
+
//
|
|
205
|
+
// `pager.page` has ALREADY moved by the time this runs — wirePager advances it and
|
|
206
|
+
// then calls back — so a failure restores `state.page`/`state.perPage`, the last
|
|
207
|
+
// values that painted. That roll-back is what the old hand-roll did with
|
|
208
|
+
// `state.offset`, and it is the one piece of it worth carrying over.
|
|
209
|
+
async function fetchPage({ reclamps = 0 } = {}) {
|
|
210
|
+
const seq = ++inFlight;
|
|
168
211
|
loading = true;
|
|
169
212
|
renderPager();
|
|
170
213
|
try {
|
|
171
214
|
const res = await api.request(
|
|
172
215
|
'GET',
|
|
173
|
-
`${API}/sessions/search?order=recent&limit=${
|
|
216
|
+
`${API}/sessions/search?order=recent&limit=${pager.perPage}&offset=${kit.pageOffset(pager)}`
|
|
174
217
|
);
|
|
218
|
+
// A newer click already went out; painting this answer would show a page nobody
|
|
219
|
+
// is on any more.
|
|
220
|
+
if (seq !== inFlight) return true;
|
|
175
221
|
if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return false; }
|
|
176
222
|
if (res.status === 403) {
|
|
177
223
|
showSealed('You do not have access to the session log.');
|
|
178
224
|
return false;
|
|
179
225
|
}
|
|
180
226
|
if (!res.ok) throw new Error(`search → ${res.status}`);
|
|
181
|
-
const
|
|
182
|
-
|
|
227
|
+
const clamped = showData(res.data);
|
|
228
|
+
// The corpus shrank under this page (sessions pruned between loads):
|
|
229
|
+
// renderServerPage pulled the page back into range, so go get the rows that page
|
|
230
|
+
// actually holds rather than leaving a range that disagrees with the table.
|
|
231
|
+
// One re-ask, never a loop: a corpus shrinking on every round-trip would
|
|
232
|
+
// otherwise spin here, and a stale range for one paint is the smaller harm.
|
|
233
|
+
if (clamped && reclamps < 1) { loading = false; return fetchPage({ reclamps: reclamps + 1 }); }
|
|
234
|
+
state.page = pager.page;
|
|
235
|
+
state.perPage = pager.perPage;
|
|
183
236
|
return true;
|
|
184
237
|
} catch (err) {
|
|
238
|
+
if (seq !== inFlight) return true;
|
|
185
239
|
console.error('[sessions] failed to load corpus', err);
|
|
240
|
+
pager.page = state.page;
|
|
241
|
+
pager.perPage = state.perPage;
|
|
186
242
|
$('#ledger-sub').textContent = 'The session log could not be loaded. Try again.';
|
|
187
243
|
toast('Could not load sessions.');
|
|
188
244
|
return false;
|
|
189
245
|
} finally {
|
|
190
|
-
|
|
191
|
-
|
|
246
|
+
if (seq === inFlight) {
|
|
247
|
+
loading = false;
|
|
248
|
+
renderPager();
|
|
249
|
+
}
|
|
192
250
|
}
|
|
193
251
|
}
|
|
194
252
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
function wirePager() {
|
|
207
|
-
$('#pager-prev').addEventListener('click', () => goTo(state.offset - PAGE_SIZE));
|
|
208
|
-
$('#pager-next').addEventListener('click', () => goTo(state.offset + PAGE_SIZE));
|
|
253
|
+
function mountPager() {
|
|
254
|
+
const mount = $('#sessions-pager-mount');
|
|
255
|
+
if (!mount) return;
|
|
256
|
+
mount.innerHTML = kit.pagerBarHtml(PAGER_PREFIX, { label: 'sessions' });
|
|
257
|
+
// The kit has already updated `pager` by the time this fires; go get the rows that
|
|
258
|
+
// match it. Scrolling up only on success keeps a failed page from yanking the
|
|
259
|
+
// reader away from the table they were still looking at.
|
|
260
|
+
kit.wirePager(pager, async () => {
|
|
261
|
+
const ok = await fetchPage();
|
|
262
|
+
if (ok) window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
263
|
+
});
|
|
209
264
|
}
|
|
210
265
|
|
|
211
266
|
async function load() {
|
|
@@ -234,8 +289,14 @@
|
|
|
234
289
|
return;
|
|
235
290
|
}
|
|
236
291
|
|
|
292
|
+
if (!kit) {
|
|
293
|
+
console.error('[sessions] OTBKit did not load — the pager cannot be built');
|
|
294
|
+
$('#ledger-sub').textContent = 'The session log could not be loaded. Try again.';
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
237
298
|
// 2. Fetch the first page (Archon-gated + audited server-side).
|
|
238
|
-
|
|
299
|
+
mountPager();
|
|
239
300
|
await fetchPage();
|
|
240
301
|
}
|
|
241
302
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.506",
|
|
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.506",
|
|
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.506",
|
|
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",
|
package/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.506'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/hall_audit.mjs
CHANGED
|
@@ -198,6 +198,44 @@ test('the sidebar scrolls its nav list, not the whole column', () => {
|
|
|
198
198
|
|
|
199
199
|
// ---- 6. every hall list is paged through the kit (task 1002446) -----------
|
|
200
200
|
|
|
201
|
+
test('the kit is the ONLY thing in the hall that emits pager markup', () => {
|
|
202
|
+
// hall-ui/CLAUDE.md's "no bespoke pager" rule had one sanctioned exception for two
|
|
203
|
+
// releases — sessions.js, which paged server-side by offset, a thing the kit could
|
|
204
|
+
// not express (audited task 2179). Task 1003281 added `makeLedger({ serverPager })`
|
|
205
|
+
// and left the migration as its own task; it has now landed, so the hall is back to
|
|
206
|
+
// ONE pager contract with no footnote.
|
|
207
|
+
//
|
|
208
|
+
// A rule that only lives in prose is a rule that comes back as an eleventh pager.
|
|
209
|
+
// This is the machine-checkable half: the kit STAMPS every bar at runtime, so any
|
|
210
|
+
// .pager markup written by hand — in a page's HTML or in another script's template —
|
|
211
|
+
// is by construction a second implementation. Adding an exception here is where the
|
|
212
|
+
// conversation happens, exactly like the budgets above.
|
|
213
|
+
const OWNER = 'hall-kit.js';
|
|
214
|
+
const offenders = [];
|
|
215
|
+
for (const f of fs.readdirSync(HALL)) {
|
|
216
|
+
if (!/\.(html|js)$/.test(f) || f === OWNER) continue;
|
|
217
|
+
const src = read(f);
|
|
218
|
+
// `class="pager…"` in a template string or in static markup. The kit's own callers
|
|
219
|
+
// pass a PREFIX and get the markup back; they never write the class themselves.
|
|
220
|
+
if (/class="pager[\s"_-]/.test(src)) offenders.push(f);
|
|
221
|
+
}
|
|
222
|
+
assert.deepEqual(offenders, [],
|
|
223
|
+
`${offenders.join(', ')} writes pager markup by hand — call OTBKit.pagerBarHtml(prefix) `
|
|
224
|
+
+ '(or makeLedger, which stamps it for you) instead of a second copy.');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('the session log pages on the kit — the last hand-roll is gone', () => {
|
|
228
|
+
// The specific exception this rule carried, pinned by name so its removal cannot be
|
|
229
|
+
// quietly undone. Behaviour is exercised for real in tests/sessions_kit_pager.mjs;
|
|
230
|
+
// this is the structural half.
|
|
231
|
+
const js = read('sessions.js');
|
|
232
|
+
for (const fn of ['makePager', 'pagerBarHtml', 'wirePager', 'pageOffset', 'renderServerPage']) {
|
|
233
|
+
assert.match(js, new RegExp(`kit\\.${fn}\\(`), `sessions.js must page through the kit's ${fn}`);
|
|
234
|
+
}
|
|
235
|
+
assert.doesNotMatch(js, /const PAGE_SIZE\b/, 'its fixed page size is gone — per-page is the kit\'s now');
|
|
236
|
+
assert.match(read('sessions.html'), /hall-kit\.js/, 'and the page has to load the kit');
|
|
237
|
+
});
|
|
238
|
+
|
|
201
239
|
test('the peer-recognition feed is paged through OTBKit, not by hand', () => {
|
|
202
240
|
assert.match(read('hall-widgets.js'), /pagerBarHtml\('acclaim'/, 'the acclaim card stamps the kit pager bar');
|
|
203
241
|
const js = read('builders.js');
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// tests/sessions_kit_pager.mjs — the session log pages on the KIT's contract, not
|
|
2
|
+
// its own (goal 1000072, the migration task 1003281 left behind).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS FILE EXISTS. `sessions.js` was the hall's one sanctioned hand-rolled
|
|
5
|
+
// pager — audited as an exception at task 2179 because the kit could only slice an
|
|
6
|
+
// array the browser already held, and the session corpus cannot be held. Task 1003281
|
|
7
|
+
// added the server-offset mode to the kit and explicitly deferred the migration:
|
|
8
|
+
// "sessions.js stays as it is; migrating it is its own task." This is that task, and
|
|
9
|
+
// an exception removed without a test is an exception waiting to be re-added.
|
|
10
|
+
//
|
|
11
|
+
// It runs the REAL hall-kit.js and the REAL sessions.js in one vm against a stub DOM
|
|
12
|
+
// and a fake API (the profile_activity.mjs idiom), so what is asserted is the request
|
|
13
|
+
// the page actually issues and the bar a reader actually sees — not a stub of the kit
|
|
14
|
+
// agreeing with a stub of the page.
|
|
15
|
+
//
|
|
16
|
+
// The five behaviours the migration had to keep or gain:
|
|
17
|
+
// 1. it asks the SERVER for one page (the corpus is never pulled into the browser)
|
|
18
|
+
// 2. a page click moves the offset by the reader's per-page, not a fixed 50
|
|
19
|
+
// 3. per-page is a real control now, and it resets to page 1 and persists
|
|
20
|
+
// 4. a failed fetch ROLLS BACK the pager — the bar may never claim a page whose
|
|
21
|
+
// rows never arrived (the one piece of the hand-roll worth keeping)
|
|
22
|
+
// 5. nothing is pressable while a fetch is in flight
|
|
23
|
+
//
|
|
24
|
+
// Run: node tests/sessions_kit_pager.mjs
|
|
25
|
+
import { strict as assert } from 'node:assert';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import vm from 'node:vm';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { makeRunner } from './helpers.mjs';
|
|
31
|
+
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
34
|
+
const KIT_JS = read('modules/hall-ui/public/hall-kit.js');
|
|
35
|
+
const SESSIONS_JS = read('modules/hall-ui/public/sessions.js');
|
|
36
|
+
const SESSIONS_HTML = read('modules/hall-ui/public/sessions.html');
|
|
37
|
+
|
|
38
|
+
const { test, summary } = makeRunner();
|
|
39
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// the harness: a stub DOM just rich enough for the kit's ids + the page's cells
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
function makeEl(id) {
|
|
46
|
+
const listeners = new Map();
|
|
47
|
+
return {
|
|
48
|
+
id,
|
|
49
|
+
innerHTML: '', textContent: '', value: '',
|
|
50
|
+
hidden: false, disabled: false,
|
|
51
|
+
options: [], style: {}, dataset: {},
|
|
52
|
+
classList: { add() {}, remove() {}, toggle() {} },
|
|
53
|
+
addEventListener(ev, fn) {
|
|
54
|
+
if (!listeners.has(ev)) listeners.set(ev, []);
|
|
55
|
+
listeners.get(ev).push(fn);
|
|
56
|
+
},
|
|
57
|
+
fire(ev, arg) { for (const fn of listeners.get(ev) || []) fn(arg); },
|
|
58
|
+
listenerCount(ev) { return (listeners.get(ev) || []).length; },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// `rank: 'metic'` is the gate sessions.js applies before it fetches anything.
|
|
63
|
+
function harness({ total = 320, failAfter = null, viewerRank = 'metic', perPagePref = null, dropKit = false } = {}) {
|
|
64
|
+
const els = new Map();
|
|
65
|
+
const getEl = (id) => {
|
|
66
|
+
if (!els.has(id)) els.set(id, makeEl(id));
|
|
67
|
+
return els.get(id);
|
|
68
|
+
};
|
|
69
|
+
const documentObj = {
|
|
70
|
+
documentElement: { dataset: {}, style: {}, classList: { add() {}, remove() {} } },
|
|
71
|
+
getElementById: getEl,
|
|
72
|
+
querySelector(sel) { return getEl(String(sel).replace(/^#/, '')); },
|
|
73
|
+
querySelectorAll() { return []; },
|
|
74
|
+
createElement() { return makeEl(); },
|
|
75
|
+
addEventListener() {},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const requests = [];
|
|
79
|
+
let calls = 0;
|
|
80
|
+
const rowsFor = (limit, offset) => Array.from(
|
|
81
|
+
{ length: Math.max(0, Math.min(limit, total - offset)) },
|
|
82
|
+
(_, i) => ({ session_id: `sid-${offset + i}`, builder_id: 1, github_login: 'jaxri', rank: 'metic', task_ids: [], assistant_turns: 3, total_tokens: 100, tool_errors: 0, duration_seconds: 60, uploaded_at: '2026-08-01T00:00:00Z' })
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const client = {
|
|
86
|
+
request: async (method, url) => {
|
|
87
|
+
requests.push(url);
|
|
88
|
+
if (url.endsWith('/me')) {
|
|
89
|
+
return { status: 200, ok: true, data: { builder: { id: 1, rank: viewerRank } } };
|
|
90
|
+
}
|
|
91
|
+
if (url.includes('/sessions/search')) {
|
|
92
|
+
calls += 1;
|
|
93
|
+
if (failAfter != null && calls > failAfter) return { status: 500, ok: false, data: null };
|
|
94
|
+
const q = new URLSearchParams(url.split('?')[1] || '');
|
|
95
|
+
const limit = Number(q.get('limit'));
|
|
96
|
+
const offset = Number(q.get('offset'));
|
|
97
|
+
return {
|
|
98
|
+
status: 200,
|
|
99
|
+
ok: true,
|
|
100
|
+
data: { sessions: rowsFor(limit, offset), totals: { total, builders: 3 } },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { status: 404, ok: false, data: null };
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const store = new Map();
|
|
108
|
+
// Seeded BEFORE the scripts run — makePager reads the preference at boot, so this is
|
|
109
|
+
// the only way to model a returning reader.
|
|
110
|
+
if (perPagePref != null) store.set('otb.sessions.perPage', String(perPagePref));
|
|
111
|
+
const windowObj = {
|
|
112
|
+
BongosClient: { createClient: () => client },
|
|
113
|
+
location: { assign() {} },
|
|
114
|
+
localStorage: {
|
|
115
|
+
getItem: (k) => (store.has(k) ? store.get(k) : null),
|
|
116
|
+
setItem: (k, v) => store.set(k, String(v)),
|
|
117
|
+
},
|
|
118
|
+
scrollTo() {},
|
|
119
|
+
addEventListener() {},
|
|
120
|
+
OTB: {
|
|
121
|
+
escapeHtml: (s) => String(s == null ? '' : s),
|
|
122
|
+
$: (sel) => documentObj.querySelector(sel),
|
|
123
|
+
num: (n) => Number(n) || 0,
|
|
124
|
+
fmtInt: (n) => String(Number(n) || 0),
|
|
125
|
+
fmtCompact: (n) => String(Number(n) || 0),
|
|
126
|
+
fmtDate: (iso) => String(iso ?? '').slice(0, 10),
|
|
127
|
+
rankLabel: (r) => String(r ?? ''),
|
|
128
|
+
toast() {},
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const sandbox = {
|
|
133
|
+
window: windowObj, document: documentObj, location: windowObj.location,
|
|
134
|
+
console: { error() {}, warn() {}, log() {} },
|
|
135
|
+
setTimeout, clearTimeout, URLSearchParams,
|
|
136
|
+
};
|
|
137
|
+
sandbox.globalThis = sandbox;
|
|
138
|
+
vm.createContext(sandbox);
|
|
139
|
+
// The real kit first — that is the point: the page is held to the shipped contract.
|
|
140
|
+
vm.runInContext(KIT_JS, sandbox, { filename: 'hall-kit.js' });
|
|
141
|
+
if (dropKit) delete sandbox.window.OTBKit;
|
|
142
|
+
vm.runInContext(SESSIONS_JS, sandbox, { filename: 'sessions.js' });
|
|
143
|
+
|
|
144
|
+
const searches = () => requests.filter((u) => u.includes('/sessions/search'));
|
|
145
|
+
const q = (n) => {
|
|
146
|
+
const list = searches();
|
|
147
|
+
const url = list[n < 0 ? list.length + n : n];
|
|
148
|
+
return Object.fromEntries(new URLSearchParams(url.split('?')[1]));
|
|
149
|
+
};
|
|
150
|
+
return { els, getEl, searches, q, store, kit: sandbox.window.OTBKit };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// 1. the page markup no longer carries a pager of its own
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
await test('sessions.html carries a mount, not a hand-rolled pager', () => {
|
|
158
|
+
assert.ok(!/id="ledger-pager"/.test(SESSIONS_HTML), 'the bespoke <nav class="pager"> is gone');
|
|
159
|
+
assert.ok(!/id="pager-(prev|next|pos)"/.test(SESSIONS_HTML), 'its bespoke button ids are gone');
|
|
160
|
+
assert.match(SESSIONS_HTML, /id="sessions-pager-mount"/);
|
|
161
|
+
assert.match(SESSIONS_HTML, /hall-kit\.js/, 'the kit must load on this page now');
|
|
162
|
+
// The kit must be loaded BEFORE the page script that reads window.OTBKit at boot.
|
|
163
|
+
assert.ok(SESSIONS_HTML.indexOf('hall-kit.js') < SESSIONS_HTML.indexOf('sessions.js?'),
|
|
164
|
+
'hall-kit.js must come before sessions.js');
|
|
165
|
+
// The pager mount stays OUTSIDE the horizontal scrollport, or it slides away with
|
|
166
|
+
// the columns on a narrow screen.
|
|
167
|
+
const wrapEnd = SESSIONS_HTML.indexOf('</div>', SESSIONS_HTML.indexOf('ledger-table-wrap'));
|
|
168
|
+
assert.ok(SESSIONS_HTML.indexOf('sessions-pager-mount') > wrapEnd,
|
|
169
|
+
'the pager mount must not be inside .ledger-table-wrap');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await test('sessions.js hand-rolls no pager state any more', () => {
|
|
173
|
+
assert.ok(!/const PAGE_SIZE\b/.test(SESSIONS_JS), 'the fixed page size is gone');
|
|
174
|
+
assert.ok(!/state\.offset/.test(SESSIONS_JS.replace(/^\s*\/\/.*$/gm, '')),
|
|
175
|
+
'the hand-rolled offset cursor is gone (comments may still explain it)');
|
|
176
|
+
for (const fn of ['makePager', 'pagerBarHtml', 'wirePager', 'pageOffset', 'renderServerPage']) {
|
|
177
|
+
assert.ok(SESSIONS_JS.includes(`kit.${fn}`), `sessions.js must use the kit's ${fn}`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// 2. what the page actually does, executed
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
await test('the first load asks the SERVER for one page — the corpus is never held', async () => {
|
|
186
|
+
const h = harness({ total: 320 });
|
|
187
|
+
await tick(); await tick();
|
|
188
|
+
assert.equal(h.searches().length, 1);
|
|
189
|
+
const p = h.q(0);
|
|
190
|
+
assert.equal(p.offset, '0');
|
|
191
|
+
assert.equal(p.limit, '50', 'the page keeps its historical 50 as the default per-page');
|
|
192
|
+
assert.equal(p.order, 'recent');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await test('Next moves the offset by the reader\'s per-page, and the bar says where they are', async () => {
|
|
196
|
+
const h = harness({ total: 320 });
|
|
197
|
+
await tick(); await tick();
|
|
198
|
+
h.getEl('sessions-next').fire('click');
|
|
199
|
+
await tick(); await tick();
|
|
200
|
+
assert.equal(h.searches().length, 2);
|
|
201
|
+
assert.equal(h.q(-1).offset, '50');
|
|
202
|
+
assert.equal(h.getEl('sessions-page').textContent, 'Page 2 of 7');
|
|
203
|
+
assert.equal(h.getEl('sessions-range').textContent, 'Showing 51–100 of 320');
|
|
204
|
+
assert.equal(h.getEl('sessions-pager').hidden, false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await test('per-page is a real control now: it re-asks, resets to page 1, and persists', async () => {
|
|
208
|
+
const h = harness({ total: 320 });
|
|
209
|
+
await tick(); await tick();
|
|
210
|
+
h.getEl('sessions-next').fire('click');
|
|
211
|
+
await tick(); await tick();
|
|
212
|
+
assert.equal(h.q(-1).offset, '50', 'sanity: on page 2');
|
|
213
|
+
|
|
214
|
+
const sel = h.getEl('sessions-per-page');
|
|
215
|
+
sel.fire('change', { target: { value: '100' } });
|
|
216
|
+
await tick(); await tick();
|
|
217
|
+
const p = h.q(-1);
|
|
218
|
+
assert.equal(p.limit, '100', 'the new page size reaches the server');
|
|
219
|
+
assert.equal(p.offset, '0', 'a per-page change returns to page 1');
|
|
220
|
+
assert.equal(h.store.get('otb.sessions.perPage'), '100', 'the choice is remembered');
|
|
221
|
+
assert.equal(h.getEl('sessions-page').textContent, 'Page 1 of 4');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await test('a remembered per-page is used on the very first request of the next visit', async () => {
|
|
225
|
+
// The point of persisting it: a returning reader does not have to set it again, and
|
|
226
|
+
// does not first get a page at the default size and then a second one at theirs.
|
|
227
|
+
const h = harness({ total: 320, perPagePref: 25 });
|
|
228
|
+
await tick(); await tick();
|
|
229
|
+
assert.equal(h.searches().length, 1, 'one request, not a default page then a corrected one');
|
|
230
|
+
assert.equal(h.q(0).limit, '25');
|
|
231
|
+
assert.equal(h.getEl('sessions-page').textContent, 'Page 1 of 13');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await test('a junk stored preference falls back to the default rather than to nothing', async () => {
|
|
235
|
+
// localStorage is reader-writable and survives across releases; a value the option
|
|
236
|
+
// list no longer offers must not become the page size.
|
|
237
|
+
const h = harness({ total: 320, perPagePref: 'lots' });
|
|
238
|
+
await tick(); await tick();
|
|
239
|
+
assert.equal(h.q(0).limit, '50');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await test('a failed page ROLLS BACK — the bar never claims a page whose rows never came', async () => {
|
|
243
|
+
// failAfter: 1 → the first load succeeds, every page click after it 500s.
|
|
244
|
+
const h = harness({ total: 320, failAfter: 1 });
|
|
245
|
+
await tick(); await tick();
|
|
246
|
+
assert.equal(h.getEl('sessions-page').textContent, 'Page 1 of 7');
|
|
247
|
+
|
|
248
|
+
h.getEl('sessions-next').fire('click');
|
|
249
|
+
await tick(); await tick();
|
|
250
|
+
assert.equal(h.searches().length, 2, 'it did try');
|
|
251
|
+
assert.equal(h.getEl('sessions-page').textContent, 'Page 1 of 7',
|
|
252
|
+
'the failed page 2 must not be left showing in the bar');
|
|
253
|
+
assert.equal(h.getEl('sessions-range').textContent, 'Showing 1–50 of 320',
|
|
254
|
+
'and the range must still describe the rows that are actually in the table');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await test('a failed per-page change rolls the SIZE back too', async () => {
|
|
258
|
+
const h = harness({ total: 320, failAfter: 1 });
|
|
259
|
+
await tick(); await tick();
|
|
260
|
+
h.getEl('sessions-per-page').fire('change', { target: { value: '10' } });
|
|
261
|
+
await tick(); await tick();
|
|
262
|
+
h.getEl('sessions-next').fire('click');
|
|
263
|
+
await tick(); await tick();
|
|
264
|
+
// Back on the 50-row page 1, the only page whose rows ever arrived, so Next asks
|
|
265
|
+
// for offset 50 and not offset 10.
|
|
266
|
+
assert.equal(h.q(-1).limit, '50', 'the rolled-back size is what the next request uses');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await test('nothing is pressable while a fetch is in flight', async () => {
|
|
270
|
+
const h = harness({ total: 320 });
|
|
271
|
+
await tick(); await tick();
|
|
272
|
+
assert.equal(h.getEl('sessions-per-page').disabled, false, 'idle: the control is live');
|
|
273
|
+
assert.equal(h.getEl('sessions-next').disabled, false);
|
|
274
|
+
h.getEl('sessions-next').fire('click');
|
|
275
|
+
// deliberately not awaited — this is mid-flight
|
|
276
|
+
assert.equal(h.getEl('sessions-next').disabled, true, 'in flight: Next is dead');
|
|
277
|
+
assert.equal(h.getEl('sessions-prev').disabled, true, 'in flight: Prev is dead');
|
|
278
|
+
assert.equal(h.getEl('sessions-per-page').disabled, true, 'in flight: per-page is dead');
|
|
279
|
+
await tick(); await tick();
|
|
280
|
+
assert.equal(h.getEl('sessions-per-page').disabled, false, 'landed: live again');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await test('Prev stays dead on page 1 — the kit decides that, not the page', async () => {
|
|
284
|
+
const h = harness({ total: 320 });
|
|
285
|
+
await tick(); await tick();
|
|
286
|
+
assert.equal(h.getEl('sessions-prev').disabled, true, 'page 1 has nothing before it');
|
|
287
|
+
assert.equal(h.getEl('sessions-next').disabled, false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await test('a corpus that fits one page shows no bar at all', async () => {
|
|
291
|
+
const h = harness({ total: 8 });
|
|
292
|
+
await tick(); await tick();
|
|
293
|
+
assert.equal(h.getEl('sessions-pager').hidden, true);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await test('a missing kit degrades to a named failure, not a blank page', async () => {
|
|
297
|
+
// The kit is a separate <script>; a cache miss must not throw at parse time and take
|
|
298
|
+
// the whole page — including the rank gate — down with it.
|
|
299
|
+
const h = harness({ total: 320, dropKit: true });
|
|
300
|
+
await tick(); await tick();
|
|
301
|
+
assert.equal(h.searches().length, 0, 'no corpus request without a pager to place it');
|
|
302
|
+
assert.match(h.getEl('ledger-sub').textContent, /could not be loaded/);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await test('the rank gate still runs before any corpus request', async () => {
|
|
306
|
+
const h = harness({ total: 320, viewerRank: 'xenos' });
|
|
307
|
+
await tick(); await tick();
|
|
308
|
+
assert.equal(h.searches().length, 0, 'a xenos must not even ask');
|
|
309
|
+
assert.equal(h.getEl('gate-scroll').hidden, false, 'they get the sealed notice');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
summary();
|