@bongos/core 1.17.3 → 1.18.3

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.
Files changed (163) hide show
  1. package/.bongos-core.json +212 -332
  2. package/.devcontainer/healthcheck.sh +0 -0
  3. package/.devcontainer/init-firewall.sh +0 -0
  4. package/.devcontainer/post-create.sh +0 -0
  5. package/GOVERNANCE.md +1 -1
  6. package/README.md +26 -22
  7. package/bin/bongos.js +0 -0
  8. package/config/branding.neutral.json +5 -1
  9. package/docker-entrypoint.sh +0 -0
  10. package/docs/adr/0142-project-context-is-prose-only.md +41 -0
  11. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  12. package/docs/adr/README.md +6 -0
  13. package/docs/api/openapi.json +1630 -632
  14. package/docs/api-reference.md +50 -15
  15. package/docs/file-map.md +0 -18
  16. package/docs/onboarding/diagrams/05-versioning.mmd +4 -6
  17. package/docs/onboarding/diagrams/assertions.json +5 -9
  18. package/docs/recipes/packaging-the-core.md +13 -0
  19. package/docs/recipes/upgrading-the-core.md +5 -2
  20. package/migrations/039_newcomer_friendly_column.sql +5 -0
  21. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  22. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  23. package/modules/hall-ui/public/builders.js +37 -0
  24. package/modules/hall-ui/public/goals-page.js +98 -176
  25. package/modules/hall-ui/public/goals.css +183 -0
  26. package/modules/hall-ui/public/goals.html +35 -261
  27. package/modules/hall-ui/public/goals.js +7 -2
  28. package/modules/hall-ui/public/hall-widgets.js +12 -4
  29. package/modules/hall-ui/public/my-projects.html +78 -0
  30. package/modules/hall-ui/public/my-projects.js +142 -0
  31. package/modules/hall-ui/public/scouting.html +45 -0
  32. package/modules/hall-ui/public/scouting.js +73 -0
  33. package/modules/hall-ui/public/settings.css +355 -0
  34. package/modules/hall-ui/public/settings.html +86 -414
  35. package/modules/hall-ui/public/settings.js +96 -35
  36. package/modules/hall-ui/public/style.css +31 -0
  37. package/modules/hall-ui/public/watch.css +184 -0
  38. package/modules/hall-ui/public/watch.html +111 -285
  39. package/modules/hall-ui/public/watch.js +59 -21
  40. package/modules/status-ui/public/explore.html +54 -0
  41. package/modules/status-ui/public/explore.js +67 -0
  42. package/package-lock.json +2 -2
  43. package/package.json +1 -1
  44. package/public-landing/create.html +292 -19
  45. package/public-landing/index.html +11 -76
  46. package/scripts/cron/style_guide_audit.sh +0 -0
  47. package/scripts/gds/api.js +0 -0
  48. package/scripts/gds/architect-audit.js +0 -0
  49. package/scripts/gds/audit-authorship.sh +0 -0
  50. package/scripts/gds/audit-replay.sh +0 -0
  51. package/scripts/gds/box.js +0 -0
  52. package/scripts/gds/capture.js +0 -0
  53. package/scripts/gds/cli-lib.js +11 -5
  54. package/scripts/gds/conductor-dispatch.js +0 -0
  55. package/scripts/gds/coreB-carve-proof.sh +0 -0
  56. package/scripts/gds/db-backup-nightly.sh +0 -0
  57. package/scripts/gds/db-backup-offsite.sh +0 -0
  58. package/scripts/gds/doctor.js +0 -0
  59. package/scripts/gds/grade-smoke.js +0 -0
  60. package/scripts/gds/hooks/pre-push.js +0 -0
  61. package/scripts/gds/install-git-hooks.js +0 -0
  62. package/scripts/gds/lint-migrations.js +102 -0
  63. package/scripts/gds/login.js +42 -10
  64. package/scripts/gds/main-audit.js +0 -0
  65. package/scripts/gds/mirror-redact.js +5 -0
  66. package/scripts/gds/oauth-secret.js +0 -0
  67. package/scripts/gds/onboarding-analyze.js +0 -0
  68. package/scripts/gds/onboarding-ship.js +0 -0
  69. package/scripts/gds/package-core.js +263 -4
  70. package/scripts/gds/paste-token.js +0 -0
  71. package/scripts/gds/portability-smoke-art.sh +0 -0
  72. package/scripts/gds/portability-smoke-memory.sh +0 -0
  73. package/scripts/gds/provision.js +270 -11
  74. package/scripts/gds/publish-manifest.js +15 -0
  75. package/scripts/gds/push-main.js +0 -0
  76. package/scripts/gds/render-ideas.js +0 -0
  77. package/scripts/gds/run-unit-tests.js +38 -14
  78. package/scripts/gds/security-baseline-check.sh +0 -0
  79. package/scripts/gds/session.js +0 -0
  80. package/scripts/gds/setup.js +41 -0
  81. package/scripts/gds/ship.js +126 -32
  82. package/scripts/gds/smoke-batch-claim.sh +0 -0
  83. package/scripts/gds/smoke-batch-release.sh +0 -0
  84. package/scripts/gds/smoke-box.sh +0 -0
  85. package/scripts/gds/smoke-claim-consistency.sh +0 -0
  86. package/scripts/gds/smoke-dependencies.sh +0 -0
  87. package/scripts/gds/smoke-write.sh +0 -0
  88. package/scripts/gds/upgrade.js +147 -5
  89. package/scripts/hall-preview/server.js +18 -1
  90. package/scripts/migrate.sh +10 -0
  91. package/scripts/migrate_cost_ledger.sh +0 -0
  92. package/scripts/render-diagrams.sh +0 -0
  93. package/scripts/security/dep-audit.js +0 -0
  94. package/scripts/setup-builder.js +0 -0
  95. package/scripts/setup-builder.sh +0 -0
  96. package/scripts/snapshots/maybe-render-on-deploy.sh +0 -0
  97. package/src/bongos/auth.js +362 -5
  98. package/src/bongos/db.js +66 -0
  99. package/src/bongos/github-repos.js +93 -0
  100. package/src/bongos/routes/auth.js +93 -11
  101. package/src/bongos/routes/github.js +90 -0
  102. package/src/bongos/routes/instance.js +15 -3
  103. package/src/bongos/routes.js +2 -0
  104. package/src/branding.js +17 -2
  105. package/src/instance-config.js +16 -0
  106. package/src/module-api.js +48 -1
  107. package/tests/branding.mjs +29 -23
  108. package/tests/github_repos_helpers.mjs +91 -0
  109. package/tests/github_token_store.mjs +121 -0
  110. package/tests/idp_device_exchange.mjs +105 -0
  111. package/tests/idp_federation.mjs +100 -0
  112. package/tests/migrations_forward_safe.mjs +86 -0
  113. package/tests/module_api.mjs +1 -1
  114. package/tests/package_core.mjs +246 -1
  115. package/tests/provision.mjs +226 -0
  116. package/tests/pruned_lockfile_integrity.mjs +79 -0
  117. package/tests/ship_ci_deploy.mjs +60 -0
  118. package/tests/upgrade.mjs +254 -5
  119. package/.gitignore +0 -91
  120. package/scripts/hall-preview/fixtures/access-requests.json +0 -1
  121. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  122. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  123. package/scripts/hall-preview/fixtures/audit-log.json +0 -1
  124. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -1
  125. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  126. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  127. package/scripts/hall-preview/fixtures/builders__roster.json +0 -1
  128. package/scripts/hall-preview/fixtures/goals.json +0 -502
  129. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  130. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  131. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  132. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  133. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  134. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  135. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  136. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  137. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -1
  138. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  139. package/scripts/hall-preview/fixtures/instance.json +0 -124
  140. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  141. package/scripts/hall-preview/fixtures/me.json +0 -476
  142. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  143. package/scripts/hall-preview/fixtures/override-requests.json +0 -1
  144. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  145. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  146. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  147. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  148. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  149. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  150. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  151. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  152. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  153. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  154. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  155. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  156. package/scripts/hall-preview/fixtures/security__docs.json +0 -1
  157. package/scripts/hall-preview/fixtures/security__reports.json +0 -1
  158. package/scripts/hall-preview/fixtures/sessions__search.json +0 -1
  159. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  160. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  161. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  162. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  163. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -10,199 +10,10 @@
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
12
12
  <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
13
- <style>
14
- /* Page-local styles for the watch — built on the shared design system's
15
- derived tokens; class names are load-bearing (watch.js writes them). */
16
- .watch-stats {
17
- display: grid;
18
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
19
- gap: 12px;
20
- margin-top: 6px;
21
- }
22
- .watch-stat {
23
- background: var(--surface);
24
- border: 1px solid var(--border);
25
- border-radius: var(--radius-sm);
26
- padding: 14px 16px;
27
- text-align: center;
28
- }
29
- .watch-stat__num {
30
- font-family: var(--font-display);
31
- font-size: 1.6rem;
32
- font-weight: 650;
33
- color: var(--ink);
34
- line-height: 1.2;
35
- font-variant-numeric: tabular-nums;
36
- }
37
- .watch-stat__label {
38
- font-size: 11px;
39
- font-weight: 600;
40
- color: var(--ink-faint);
41
- text-transform: uppercase;
42
- letter-spacing: 0.06em;
43
- margin-top: 4px;
44
- }
45
-
46
- .watch-table-wrap { overflow-x: auto; margin-top: 6px; }
47
- table.watch {
48
- width: 100%;
49
- border-collapse: collapse;
50
- font-size: 13.5px;
51
- color: var(--ink);
52
- }
53
- table.watch th, table.watch td {
54
- padding: 8px 10px;
55
- text-align: left;
56
- border-bottom: 1px solid var(--border);
57
- vertical-align: top;
58
- }
59
- table.watch th {
60
- font-size: 11px;
61
- font-weight: 600;
62
- text-transform: uppercase;
63
- letter-spacing: 0.06em;
64
- color: var(--ink-faint);
65
- border-bottom: 1px solid var(--border-strong);
66
- white-space: nowrap;
67
- }
68
- table.watch td.num { text-align: right; font-variant-numeric: tabular-nums; }
69
- table.watch td.warn { color: var(--danger); font-weight: 600; }
70
- table.watch tr:hover td { background: var(--surface-2); }
71
- .watch-mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 0.84rem; color: var(--info); }
72
- .watch-empty { color: var(--ink-faint); font-size: 13px; padding: 10px 0; }
73
- .watch-note { color: var(--ink-faint); font-size: 12.5px; margin-top: 10px; }
74
- .watch-rank { font-size: 12px; color: var(--ink-faint); }
75
- .watch-doc {
76
- white-space: pre-wrap;
77
- word-break: break-word;
78
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
79
- font-size: 0.8rem;
80
- line-height: 1.5;
81
- max-height: 60vh;
82
- overflow-y: auto;
83
- padding: 12px;
84
- margin-top: 8px;
85
- background: var(--surface-2);
86
- border: 1px solid var(--border);
87
- border-radius: var(--radius-sm);
88
- }
89
- .watch-sub-h, .watch-subh {
90
- font-family: var(--font-display);
91
- font-size: 14px;
92
- font-weight: 600;
93
- color: var(--ink);
94
- margin: 18px 0 4px;
95
- }
96
- .watch-sub-h:first-of-type { margin-top: 8px; }
97
-
98
- /* Severity / status badges. */
99
- .badge {
100
- display: inline-block;
101
- font-size: 11px;
102
- font-weight: 600;
103
- letter-spacing: 0.04em;
104
- text-transform: uppercase;
105
- padding: 2px 8px;
106
- border-radius: 999px;
107
- border: 1px solid var(--border-strong);
108
- color: var(--ink-soft);
109
- white-space: nowrap;
110
- }
111
- .badge--sev-critical { background: var(--danger-weak); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, transparent); }
112
- .badge--sev-high { background: var(--warn-weak); color: var(--warn); border-color: color-mix(in srgb, var(--warn) 35%, transparent); }
113
- .badge--sev-medium { background: var(--info-weak); color: var(--info); border-color: color-mix(in srgb, var(--info) 30%, transparent); }
114
- .badge--sev-low { background: var(--surface-2); color: var(--ink-soft); }
115
- .badge--status { background: var(--surface-2); color: var(--ink-soft); }
116
-
117
- /* Row action buttons + selects. */
118
- .watch-actions { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
119
- .watch-btn {
120
- font-family: var(--font-body);
121
- font-size: 12.5px;
122
- font-weight: 500;
123
- padding: 5px 12px;
124
- border: 1px solid var(--border-strong);
125
- background: var(--surface);
126
- color: var(--ink-soft);
127
- border-radius: var(--radius-sm);
128
- cursor: pointer;
129
- }
130
- .watch-btn:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }
131
- .watch-btn:disabled { opacity: 0.45; cursor: default; }
132
- .watch-btn--danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, var(--border-strong)); }
133
- .watch-select {
134
- font-family: var(--font-body);
135
- font-size: 13px;
136
- padding: 4px 6px;
137
- border: 1px solid var(--border-strong);
138
- background: var(--surface);
139
- color: var(--ink);
140
- border-radius: var(--radius-sm);
141
- }
142
-
143
- .watch-req {
144
- border-bottom: 1px solid var(--border);
145
- padding: 12px 0;
146
- display: flex; gap: 14px; flex-wrap: wrap;
147
- align-items: flex-start; justify-content: space-between;
148
- }
149
- .watch-req__who { font-weight: 600; color: var(--ink); }
150
- .watch-req__meta { color: var(--ink-faint); font-size: 12.5px; margin-top: 2px; }
151
- .watch-req__note { color: var(--ink); font-size: 13.5px; margin-top: 6px; max-width: 56ch; }
152
-
153
- .watch-unavailable { color: var(--ink-faint); font-size: 13px; padding: 8px 0; }
154
-
155
- .watch-trend { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; }
156
- .watch-trend__row {
157
- display: grid; grid-template-columns: 84px 1fr 64px;
158
- gap: 10px; align-items: center;
159
- font-size: 13px; color: var(--ink);
160
- }
161
- .watch-trend__bar-track {
162
- background: var(--surface-2); border-radius: 3px; height: 12px; overflow: hidden;
163
- border: 1px solid var(--border);
164
- }
165
- .watch-trend__bar-fill { background: var(--accent); height: 100%; display: block; }
166
- .watch-trend__val { text-align: right; font-variant-numeric: tabular-nums; color: var(--ink-soft); }
167
-
168
- .watch-list { list-style: none; padding: 0; margin: 8px 0 0; }
169
- .watch-list li {
170
- display: flex; justify-content: space-between; gap: 12px;
171
- padding: 5px 0; border-bottom: 1px solid var(--border);
172
- font-size: 13.5px;
173
- }
174
- .watch-list li .k { color: var(--ink); }
175
- .watch-list li .v { color: var(--ink-soft); font-variant-numeric: tabular-nums; }
176
-
177
- .watch-foot-link {
178
- display: inline-block; margin-top: 12px;
179
- font-size: 13px; font-weight: 500;
180
- color: var(--accent-text);
181
- }
182
-
183
- /* The in-page section rail: horizontal chip strip, sticky under the top bar
184
- at every width (overrides .section-rail's desktop column layout). */
185
- #watch-nav {
186
- position: sticky;
187
- top: var(--topbar-h);
188
- z-index: 5;
189
- flex-direction: row;
190
- overflow-x: auto;
191
- gap: 6px;
192
- padding: 8px 0;
193
- margin: 0 0 12px;
194
- background: color-mix(in srgb, var(--bg) 92%, transparent);
195
- backdrop-filter: blur(6px);
196
- }
197
- #watch-nav .rail-link {
198
- white-space: nowrap;
199
- border: 1px solid var(--border);
200
- background: var(--surface);
201
- border-radius: 999px;
202
- padding: 4px 12px;
203
- font-size: 12.5px;
204
- }
205
- </style>
13
+ <!-- Page-local styles (task 2173) — extracted from the old inline <style>
14
+ block per the hall placement rules; the attention tiles + tabs come from
15
+ style.css's shared .stat-row/.stat-tile + hall-kit rules. -->
16
+ <link rel="stylesheet" href="/builders/watch.css">
206
17
  </head>
207
18
  <body data-page="watch" data-page-title="Watch">
208
19
 
@@ -214,18 +25,6 @@
214
25
  <div class="app-content">
215
26
  <div class="page-head"><p class="page-head__sub" id="watch-sub">Loading…</p></div>
216
27
 
217
- <nav class="section-rail" id="watch-nav" hidden aria-label="Watch sections">
218
- <a class="rail-link" href="#gate-scroll">Access requests</a>
219
- <a class="rail-link" href="#watch-scroll">Security</a>
220
- <a class="rail-link" href="#roster-scroll">Roster</a>
221
- <a class="rail-link" href="#box-costs-scroll">Box costs</a>
222
- <a class="rail-link" href="#marks-scroll">Builder reviews</a>
223
- <a class="rail-link" href="#mason-marks-scroll">Reviews</a>
224
- <a class="rail-link" href="#surveyor-scroll">Estimates</a>
225
- <a class="rail-link" href="#ledger-scroll">Repo health</a>
226
- <a class="rail-link" href="#sessions-scroll">Sessions</a>
227
- </nav>
228
-
229
28
  <main>
230
29
  <!-- Restricted notice — shown by watch.js when the viewer is not an Archon. -->
231
30
  <section class="scroll" id="sealed-scroll" aria-labelledby="sealed-h" hidden>
@@ -233,85 +32,110 @@
233
32
  <p class="scroll__lede" id="sealed-body">This page is available to Archons only.</p>
234
33
  </section>
235
34
 
236
- <!-- 1. ACCESS REQUESTS -->
237
- <section class="scroll" id="gate-scroll" aria-labelledby="gate-h" hidden>
238
- <h2 id="gate-h" class="scroll__h">Access requests</h2>
239
- <p class="scroll__lede">People asking to join. Approve or dismiss each request.</p>
240
- <div id="gate-body"><p class="watch-empty">Loading access requests…</p></div>
241
- </section>
242
-
243
- <!-- 2. SECURITY -->
244
- <section class="scroll" id="watch-scroll" aria-labelledby="watch-h" hidden>
245
- <h2 id="watch-h" class="scroll__h">Security</h2>
246
- <p class="scroll__lede">Vulnerability reports, engagement reports, override requests, and the audit log.</p>
247
-
248
- <h3 class="watch-sub-h">Vulnerability reports</h3>
249
- <div id="reports-body"><p class="watch-empty">Loading reports…</p></div>
250
-
251
- <h3 class="watch-sub-h">Engagement reports</h3>
252
- <p class="watch-note">Dated exploit-detail engagement reports — kept out of the repo (threat-model §6.3) and served only to Archons. Upload via POST /api/bongos/security/docs.</p>
253
- <div id="engagement-docs-body"><p class="watch-empty">Loading engagement reports…</p></div>
254
-
255
- <h3 class="watch-sub-h">Override requests</h3>
256
- <div id="overrides-body"><p class="watch-empty">Loading the override queue…</p></div>
257
-
258
- <h3 class="watch-sub-h">Audit log</h3>
259
- <div id="audit-body"><p class="watch-empty">Loading the audit log…</p></div>
260
- </section>
261
-
262
- <!-- 3. ROSTER -->
263
- <section class="scroll" id="roster-scroll" aria-labelledby="roster-h" hidden>
264
- <h2 id="roster-h" class="scroll__h">Roster</h2>
265
- <p class="scroll__lede">Every builder in {{worldName}} — rank, {{currency}}, karma, and active claims.</p>
266
- <div id="roster-body"><p class="watch-empty">Loading the roster…</p></div>
267
- </section>
268
-
269
- <!-- 3b. BOX COSTS (#602) — per-builder dev-box cost ledger + totals. -->
270
- <section class="scroll" id="box-costs-scroll" aria-labelledby="box-costs-h" hidden>
271
- <h2 id="box-costs-h" class="scroll__h">Dev-box costs</h2>
272
- <p class="scroll__lede">What each builder's dev box has cost — spin-up, compute, and storage. Tracked for oversight; not charged in the early stage.</p>
273
- <div id="box-costs-body"><p class="watch-empty">Loading the cost ledger…</p></div>
274
- </section>
275
-
276
- <!-- 4. REVIEWS BY BUILDER -->
277
- <section class="scroll" id="marks-scroll" aria-labelledby="marks-h" hidden>
278
- <h2 id="marks-h" class="scroll__h">Reviews by builder</h2>
279
- <p class="scroll__lede">How each builder's shipped work has been graded over the past thirty days.</p>
280
- <div id="marks-body"><p class="watch-empty">Loading reviews…</p></div>
281
- </section>
282
-
283
- <!-- 5. REVIEW QUALITY -->
284
- <section class="scroll" id="mason-marks-scroll" aria-labelledby="mason-marks-h" hidden>
285
- <h2 id="mason-marks-h" class="scroll__h">Review quality</h2>
286
- <p class="scroll__lede">Grades across all recent shipped work — a rolling aggregate.</p>
287
- <div id="mason-marks-stats" class="watch-stats"></div>
288
- <div id="mason-marks-trend"></div>
289
- </section>
290
-
291
- <!-- 6. ESTIMATION ACCURACY -->
292
- <section class="scroll" id="surveyor-scroll" aria-labelledby="surveyor-h" hidden>
293
- <h2 id="surveyor-h" class="scroll__h">Estimation accuracy</h2>
294
- <p class="scroll__lede">How task estimates compare with the time work actually took.</p>
295
- <div id="surveyor-stats" class="watch-stats"></div>
296
- <div id="surveyor-body"></div>
297
- </section>
298
-
299
- <!-- 7. REPO HEALTH -->
300
- <section class="scroll" id="ledger-scroll" aria-labelledby="ledger-h" hidden>
301
- <h2 id="ledger-h" class="scroll__h">Repo health</h2>
302
- <p class="scroll__lede">The shape and health of the codebase.</p>
303
- <div id="ledger-stats" class="watch-stats"></div>
304
- <div id="ledger-loc"></div>
305
- <div id="ledger-files"></div>
306
- </section>
307
-
308
- <!-- 8. SESSIONS -->
309
- <section class="scroll" id="sessions-scroll" aria-labelledby="sessions-h" hidden>
310
- <h2 id="sessions-h" class="scroll__h">Sessions</h2>
311
- <p class="scroll__lede">A tally of recorded work sessions. The full record lives on the Sessions page.</p>
312
- <div id="sessions-stats" class="watch-stats"></div>
313
- <a class="watch-foot-link" href="/sessions">View all sessions →</a>
314
- </section>
35
+ <!-- task 2173: "what needs me today" first — the attention row's tiles
36
+ carry live counts and each links to the tab that holds the detail. -->
37
+ <div class="stat-row watch-attention" id="watch-attention" hidden>
38
+ <a class="stat-tile" href="#people"><span class="stat-tile__label">Access requests</span><span class="stat-tile__num" id="att-access">—</span><span class="stat-tile__sub">pending — People tab</span></a>
39
+ <a class="stat-tile" href="#security"><span class="stat-tile__label">Security reports</span><span class="stat-tile__num" id="att-security">—</span><span class="stat-tile__sub">open — Security tab</span></a>
40
+ <a class="stat-tile" href="#people"><span class="stat-tile__label">Stale claims</span><span class="stat-tile__num" id="att-stale">—</span><span class="stat-tile__sub">builders — People tab</span></a>
41
+ <a class="stat-tile" href="#operations"><span class="stat-tile__label">Sessions</span><span class="stat-tile__num" id="att-sessions">—</span><span class="stat-tile__sub">recorded — Operations tab</span></a>
42
+ </div>
43
+
44
+ <!-- task 2173: the nine surfaces sit behind three OTBKit tabs, hash-synced
45
+ (/watch#security). Evidence behind tabs; the attention row stays above. -->
46
+ <nav id="watch-tabs" hidden aria-label="Watch sections"></nav>
47
+
48
+ <!-- ============ tab: People ============ -->
49
+ <div id="pane-people" role="tabpanel" aria-labelledby="tab-people">
50
+
51
+ <!-- 1. ACCESS REQUESTS -->
52
+ <section class="scroll" id="gate-scroll" aria-labelledby="gate-h" hidden>
53
+ <h2 id="gate-h" class="scroll__h">Access requests</h2>
54
+ <p class="scroll__lede">People asking to join. Approve or dismiss each request.</p>
55
+ <div id="gate-body"><span class="kit-skel"></span></div>
56
+ </section>
57
+
58
+ <!-- 2. ROSTER -->
59
+ <section class="scroll" id="roster-scroll" aria-labelledby="roster-h" hidden>
60
+ <h2 id="roster-h" class="scroll__h">Roster</h2>
61
+ <p class="scroll__lede">Every builder in {{worldName}} — rank, {{currency}}, karma, and active claims.</p>
62
+ <div id="roster-body"><span class="kit-skel"></span></div>
63
+ </section>
64
+
65
+ <!-- 3. REVIEWS BY BUILDER -->
66
+ <section class="scroll" id="marks-scroll" aria-labelledby="marks-h" hidden>
67
+ <h2 id="marks-h" class="scroll__h">Reviews by builder</h2>
68
+ <p class="scroll__lede">How each builder's shipped work has been graded over the past thirty days.</p>
69
+ <div id="marks-body"><span class="kit-skel"></span></div>
70
+ </section>
71
+
72
+ <!-- 4. REVIEW QUALITY -->
73
+ <section class="scroll" id="mason-marks-scroll" aria-labelledby="mason-marks-h" hidden>
74
+ <h2 id="mason-marks-h" class="scroll__h">Review quality</h2>
75
+ <p class="scroll__lede">Grades across all recent shipped work — a rolling aggregate.</p>
76
+ <div id="mason-marks-stats" class="watch-stats"></div>
77
+ <div id="mason-marks-trend"></div>
78
+ </section>
79
+ </div>
80
+
81
+ <!-- ============ tab: Operations ============ -->
82
+ <div id="pane-operations" role="tabpanel" aria-labelledby="tab-operations" hidden>
83
+
84
+ <!-- 5. BOX COSTS (#602) — per-builder dev-box cost ledger + totals. -->
85
+ <section class="scroll" id="box-costs-scroll" aria-labelledby="box-costs-h" hidden>
86
+ <h2 id="box-costs-h" class="scroll__h">Dev-box costs</h2>
87
+ <p class="scroll__lede">What each builder's dev box has cost — spin-up, compute, and storage. Tracked for oversight; not charged in the early stage.</p>
88
+ <div id="box-costs-body"><span class="kit-skel"></span></div>
89
+ </section>
90
+
91
+ <!-- 6. SESSIONS -->
92
+ <section class="scroll" id="sessions-scroll" aria-labelledby="sessions-h" hidden>
93
+ <h2 id="sessions-h" class="scroll__h">Sessions</h2>
94
+ <p class="scroll__lede">A tally of recorded work sessions. The full record lives on the Sessions page.</p>
95
+ <div id="sessions-stats" class="watch-stats"></div>
96
+ <a class="watch-foot-link" href="/sessions">View all sessions →</a>
97
+ </section>
98
+
99
+ <!-- 7. REPO HEALTH -->
100
+ <section class="scroll" id="ledger-scroll" aria-labelledby="ledger-h" hidden>
101
+ <h2 id="ledger-h" class="scroll__h">Repo health</h2>
102
+ <p class="scroll__lede">The shape and health of the codebase.</p>
103
+ <div id="ledger-stats" class="watch-stats"></div>
104
+ <div id="ledger-loc"></div>
105
+ <div id="ledger-files"></div>
106
+ </section>
107
+ </div>
108
+
109
+ <!-- ============ tab: Security ============ -->
110
+ <div id="pane-security" role="tabpanel" aria-labelledby="tab-security" hidden>
111
+
112
+ <!-- 8. SECURITY -->
113
+ <section class="scroll" id="watch-scroll" aria-labelledby="watch-h" hidden>
114
+ <h2 id="watch-h" class="scroll__h">Security</h2>
115
+ <p class="scroll__lede">Vulnerability reports, engagement reports, override requests, and the audit log.</p>
116
+
117
+ <h3 class="watch-sub-h">Vulnerability reports</h3>
118
+ <div id="reports-body"><span class="kit-skel"></span></div>
119
+
120
+ <h3 class="watch-sub-h">Engagement reports</h3>
121
+ <p class="watch-note">Dated exploit-detail engagement reports — kept out of the repo (threat-model §6.3) and served only to Archons. Upload via POST /api/bongos/security/docs.</p>
122
+ <div id="engagement-docs-body"><span class="kit-skel"></span></div>
123
+
124
+ <h3 class="watch-sub-h">Override requests</h3>
125
+ <div id="overrides-body"><span class="kit-skel"></span></div>
126
+
127
+ <h3 class="watch-sub-h">Audit log</h3>
128
+ <div id="audit-body"><span class="kit-skel"></span></div>
129
+ </section>
130
+
131
+ <!-- 9. ESTIMATION ACCURACY -->
132
+ <section class="scroll" id="surveyor-scroll" aria-labelledby="surveyor-h" hidden>
133
+ <h2 id="surveyor-h" class="scroll__h">Estimation accuracy</h2>
134
+ <p class="scroll__lede">How task estimates compare with the time work actually took.</p>
135
+ <div id="surveyor-stats" class="watch-stats"></div>
136
+ <div id="surveyor-body"></div>
137
+ </section>
138
+ </div>
315
139
  </main>
316
140
 
317
141
  <footer class="app-foot" id="app-foot"></footer>
@@ -326,8 +150,10 @@
326
150
  <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
327
151
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
328
152
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
153
+ <!-- shared component kit (window.OTBKit) — tabs, ledger, pager (task 2166). -->
154
+ <script src="/builders/hall-kit.js"></script>
329
155
  <script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
330
156
  <script src="/builders/brand-apply.js?v=2026-06-19-r56"></script>
331
- <script src="/builders/watch.js?v=2026-06-19-r56"></script>
157
+ <script src="/builders/watch.js?v=2026-07-10-tabs"></script>
332
158
  </body>
333
159
  </html>
@@ -29,6 +29,9 @@
29
29
  // wrappers keep this file's exact variants (date+time fmtDate; '' rankLabel
30
30
  // empty; the 3200ms non-clearing toast). fmtDay / fmtDuration / fmtPct stay local.
31
31
  const { escapeHtml, $, num, fmtInt, fmtCompact } = window.OTB;
32
+ // Shared component kit (hall-kit.js, task 2166): tabs, ledger, pager, empty
33
+ // states — adopted here by task 2173.
34
+ const { makeTabs, makeLedger, makePager, emptyStateHtml } = window.OTBKit;
32
35
  const fmtDate = (iso) => window.OTB.fmtDate(iso, { time: true });
33
36
  const rankLabel = (rank) => window.OTB.rankLabel(rank, { empty: '' });
34
37
  const toast = (msg) => window.OTB.toast(msg);
@@ -90,6 +93,13 @@
90
93
  target.innerHTML = `<p class="watch-unavailable">${escapeHtml(message)}</p>`;
91
94
  }
92
95
 
96
+ // task 2173 — the attention row: each tile carries a live count fed by the
97
+ // section loader that owns the data, and links to the tab holding the detail.
98
+ function setAttention(id, n) {
99
+ const el = document.getElementById(id);
100
+ if (el) el.textContent = n == null ? '—' : fmtInt(n);
101
+ }
102
+
93
103
  // ---- shared render skeletons (C10) ----------------------------------------
94
104
 
95
105
  // section — the load→fetch→render→degrade skeleton every panel repeats. fetcher()
@@ -170,8 +180,9 @@
170
180
 
171
181
  function renderGate(data, target) {
172
182
  const requests = Array.isArray(data.requests) ? data.requests : [];
183
+ setAttention('att-access', requests.length); // tile count = table content
173
184
  if (!requests.length) {
174
- target.innerHTML = '<p class="watch-empty">No pending access requests.</p>';
185
+ target.innerHTML = emptyStateHtml('No pending access requests.');
175
186
  return;
176
187
  }
177
188
  target.innerHTML = requests
@@ -220,11 +231,15 @@
220
231
  // =========================================================================
221
232
 
222
233
  // 2a. Red-team / vulnerability reports.
234
+ // A report still needing Archon attention: not yet resolved (fixed/disputed).
235
+ const OPEN_REPORT = (r) => !['fixed', 'disputed', 'dismissed'].includes(String(r.status || '').toLowerCase());
236
+
223
237
  function renderReports(data, target) {
224
238
  const reports = Array.isArray(data.reports) ? data.reports : [];
225
239
  const statuses = Array.isArray(data.statuses) ? data.statuses : [];
240
+ setAttention('att-security', reports.filter(OPEN_REPORT).length);
226
241
  if (!reports.length) {
227
- target.innerHTML = '<p class="watch-empty">No vulnerability reports filed.</p>';
242
+ target.innerHTML = emptyStateHtml('No vulnerability reports filed.');
228
243
  return;
229
244
  }
230
245
  const rows = reports
@@ -282,7 +297,7 @@
282
297
  function renderEngagementDocs(data, target) {
283
298
  const docs = Array.isArray(data.docs) ? data.docs : [];
284
299
  if (!docs.length) {
285
- target.innerHTML = '<p class="watch-empty">No engagement reports filed. Upload one via POST /api/bongos/security/docs.</p>';
300
+ target.innerHTML = emptyStateHtml('No engagement reports filed. Upload one via POST /api/bongos/security/docs.');
286
301
  return;
287
302
  }
288
303
  const rows = docs
@@ -338,7 +353,7 @@
338
353
  function renderOverrides(data, target) {
339
354
  const requests = Array.isArray(data.requests) ? data.requests : [];
340
355
  if (!requests.length) {
341
- target.innerHTML = '<p class="watch-empty">No overrides sought.</p>';
356
+ target.innerHTML = emptyStateHtml('No overrides sought.');
342
357
  return;
343
358
  }
344
359
  const rows = requests
@@ -380,15 +395,19 @@
380
395
  });
381
396
  }
382
397
 
383
- // 2c. Audit log (recent 50).
398
+ // 2c. Audit log (recent 50) — a read-only long table, so it adopts the kit
399
+ // ledger + pager (task 2173). Rebuilt per load; the pager pref persists.
384
400
  function renderAudit(data, target) {
385
401
  const rows = Array.isArray(data.rows) ? data.rows : [];
386
402
  if (!rows.length) {
387
- target.innerHTML = '<p class="watch-empty">No audit entries yet.</p>';
403
+ target.innerHTML = emptyStateHtml('No audit entries yet.');
388
404
  return;
389
405
  }
390
- const body = rows
391
- .map((r) => {
406
+ const pager = makePager('watch-audit', 'otb.watch.auditPerPage');
407
+ const ledger = makeLedger({
408
+ mount: target,
409
+ columns: ['When', 'Builder', 'Method', 'Route', { label: 'Status', class: 'num' }],
410
+ rowHtml: (r) => {
392
411
  const status = num(r.response_status);
393
412
  const warn = status >= 400 ? ' warn' : '';
394
413
  return (
@@ -402,11 +421,11 @@
402
421
  `<td class="num${warn}">${escapeHtml(r.response_status ?? '—')}</td>` +
403
422
  '</tr>'
404
423
  );
405
- })
406
- .join('');
407
- target.innerHTML =
408
- renderTable(['When', 'Builder', 'Method', 'Route', 'Status'], body) +
409
- '<p class="watch-note">Newest first. The fifty most recent write operations.</p>';
424
+ },
425
+ pager,
426
+ });
427
+ ledger.render(rows);
428
+ target.insertAdjacentHTML('beforeend', '<p class="watch-note">Newest first. The fifty most recent write operations.</p>');
410
429
  }
411
430
 
412
431
  function loadAudit() {
@@ -428,8 +447,9 @@
428
447
 
429
448
  function renderRoster(data, target) {
430
449
  const builders = Array.isArray(data.builders) ? data.builders : [];
450
+ setAttention('att-stale', builders.filter((b) => b.stale_claim).length);
431
451
  if (!builders.length) {
432
- target.innerHTML = '<p class="watch-empty">No builders on the roster.</p>';
452
+ target.innerHTML = emptyStateHtml('No builders on the roster.');
433
453
  return;
434
454
  }
435
455
  const rows = builders
@@ -533,7 +553,7 @@
533
553
  const ledger = Array.isArray(data.ledger) ? data.ledger : [];
534
554
  const usd = (n) => `$${(Number(n) || 0).toFixed(2)}`;
535
555
  if (!totals.length) {
536
- target.innerHTML = '<p class="watch-empty">No dev-box costs accrued yet.</p>';
556
+ target.innerHTML = emptyStateHtml('No dev-box costs accrued yet.');
537
557
  return;
538
558
  }
539
559
  const totalRows = totals
@@ -576,7 +596,7 @@
576
596
  const data = await fetchJson('/grades/by-builder?days=30');
577
597
  const builders = Array.isArray(data.builders) ? data.builders : [];
578
598
  if (!builders.length) {
579
- target.innerHTML = '<p class="watch-empty">No reviews recorded in this window.</p>';
599
+ target.innerHTML = emptyStateHtml('No reviews recorded in this window.');
580
600
  return;
581
601
  }
582
602
  const sorted = builders.slice().sort((a, b) => num(b.n) - num(a.n));
@@ -606,7 +626,7 @@
606
626
  if (err && err.kind === '403') { renderUnavailable(target, 'Archons only. Review data is not visible at your rank.'); return; }
607
627
  // 404 / not-yet-built / any other failure → graceful empty state.
608
628
  console.error('[watch] marks-by-builder failed', err);
609
- target.innerHTML = '<p class="watch-empty">No reviews recorded in this window.</p>';
629
+ target.innerHTML = emptyStateHtml('No reviews recorded in this window.');
610
630
  }
611
631
  }
612
632
 
@@ -628,7 +648,7 @@
628
648
 
629
649
  const trend = Array.isArray(data.trend) ? data.trend.slice(-14) : [];
630
650
  if (!trend.length) {
631
- trendEl.innerHTML = '<p class="watch-empty">No trend to show yet.</p>';
651
+ trendEl.innerHTML = emptyStateHtml('No trend to show yet.');
632
652
  return;
633
653
  }
634
654
  const maxN = Math.max(1, ...trend.map((t) => num(t.n)));
@@ -671,7 +691,7 @@
671
691
  ]);
672
692
  const byVersion = Array.isArray(data.by_version) ? data.by_version : [];
673
693
  if (!byVersion.length) {
674
- body.innerHTML = '<p class="watch-empty">No drift recorded by version yet.</p>';
694
+ body.innerHTML = emptyStateHtml('No drift recorded by version yet.');
675
695
  return;
676
696
  }
677
697
  const rows = byVersion
@@ -778,6 +798,7 @@
778
798
 
779
799
  function renderSessions(data, stats) {
780
800
  const t = data.totals || {};
801
+ setAttention('att-sessions', t.total);
781
802
  stats.innerHTML = statCards([
782
803
  { num: fmtInt(t.total), label: 'Sessions' },
783
804
  { num: fmtInt(t.builders), label: 'Builders' },
@@ -805,15 +826,32 @@
805
826
  gate.hidden = false;
806
827
  }
807
828
 
829
+ // task 2173 — the nine surfaces sit behind three hash-synced kit tabs
830
+ // (/watch#security deep-links; the "#/…" record-overlay namespace is left
831
+ // alone by makeTabs). Built only for a verified Archon in revealSections.
832
+ let watchTabs = null;
833
+
808
834
  function revealSections() {
809
835
  [
810
- 'watch-nav', 'gate-scroll', 'watch-scroll', 'roster-scroll', 'box-costs-scroll', 'marks-scroll',
811
- 'mason-marks-scroll', 'surveyor-scroll', 'ledger-scroll', 'sessions-scroll',
836
+ 'watch-attention', 'watch-tabs', 'gate-scroll', 'watch-scroll', 'roster-scroll', 'box-costs-scroll',
837
+ 'marks-scroll', 'mason-marks-scroll', 'surveyor-scroll', 'ledger-scroll', 'sessions-scroll',
812
838
  ].forEach((id) => {
813
839
  const el = document.getElementById(id);
814
840
  if (el) el.hidden = false;
815
841
  });
816
842
  $('#sealed-scroll').hidden = true;
843
+ if (!watchTabs) {
844
+ watchTabs = makeTabs({
845
+ mount: $('#watch-tabs'),
846
+ hash: true,
847
+ initial: 'people',
848
+ tabs: [
849
+ { id: 'people', label: 'People', panel: 'pane-people' },
850
+ { id: 'operations', label: 'Operations', panel: 'pane-operations' },
851
+ { id: 'security', label: 'Security', panel: 'pane-security' },
852
+ ],
853
+ });
854
+ }
817
855
  }
818
856
 
819
857
  async function load() {