@bongos/core 1.19.511 → 1.19.513

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 (50) hide show
  1. package/.bongos-core.json +85 -50
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +3 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/config/branding.neutral.json +3 -2
  8. package/docs/adr/0238-the-join-door-is-a-provisioning-settings-policy-key.md +57 -0
  9. package/docs/adr/0239-a-recommendation-is-a-table-that-carries-its-own-reason.md +177 -0
  10. package/docs/adr/README.md +2 -0
  11. package/docs/api/openapi.json +40 -3
  12. package/docs/api-reference.md +3 -2
  13. package/docs/copy-inventory.md +158 -151
  14. package/docs/copy-registry.json +313 -229
  15. package/docs/module-api-changelog.md +4 -0
  16. package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
  17. package/modules/hall-ui/public/hall-kit.js +309 -0
  18. package/modules/hall-ui/public/hall-render.js +10 -38
  19. package/modules/hall-ui/public/profile-charts.js +58 -65
  20. package/modules/hall-ui/public/profile.css +9 -18
  21. package/modules/hall-ui/public/style.css +101 -15
  22. package/modules/hall-ui/public/watch.css +6 -14
  23. package/modules/hall-ui/public/watch.html +1 -1
  24. package/modules/hall-ui/public/watch.js +139 -54
  25. package/modules/provisioning/provisioning.js +27 -1
  26. package/modules/provisioning/recommendations.js +260 -0
  27. package/modules/provisioning/routes/provisioning.js +29 -0
  28. package/modules/provisioning/tests/provisioning.mjs +17 -14
  29. package/modules/public-landing/public/projects.html +125 -0
  30. package/modules/ui-design/kit/serve.js +4 -3
  31. package/package-lock.json +2 -2
  32. package/package.json +1 -1
  33. package/src/branding.js +9 -0
  34. package/src/module-api.js +1 -1
  35. package/tests/currency_label.mjs +5 -4
  36. package/tests/hall_audit.mjs +84 -0
  37. package/tests/hall_kit_charts.mjs +328 -0
  38. package/tests/hall_profile_world.mjs +27 -6
  39. package/tests/helpers.mjs +161 -0
  40. package/tests/joinability_invite_only.mjs +10 -2
  41. package/tests/project_door_ui.mjs +267 -0
  42. package/tests/projects_hub.mjs +73 -1
  43. package/tests/projects_hub_pre_uat.mjs +4 -1
  44. package/tests/provision_settings_apply.mjs +8 -7
  45. package/tests/provisioning_recommendations.mjs +448 -0
  46. package/tests/provisioning_settings.mjs +65 -6
  47. package/tests/provisioning_settings_apply.mjs +3 -3
  48. package/tests/provisioning_settings_env.mjs +10 -8
  49. package/tests/task_detail_ui.mjs +1 -153
  50. package/tests/watch_applications_queue.mjs +381 -0
@@ -134,17 +134,11 @@
134
134
  }
135
135
  .pchart__title { margin: 0; font-size: 13.5px; font-weight: 500; color: var(--ink); }
136
136
  .pchart__read { margin: 2px 0 var(--sp-hair); font-family: var(--label-face); font-size: var(--label-size); letter-spacing: var(--label-space); text-transform: var(--label-case); color: var(--ink-soft); min-height: 1.4em; }
137
- .pchart__svg { display: block; width: 100%; height: auto; overflow: visible; border-radius: var(--radius-sm); }
138
- .pchart__svg:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
139
- .pchart__line { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
140
- .pchart__area { fill: color-mix(in srgb, var(--accent) 10%, transparent); stroke: none; }
141
- .pchart__axis, .pchart__hair { stroke: var(--rule-soft); stroke-width: 1; }
142
- .pchart__hair { stroke: var(--ink-faint); }
143
- .pchart__dot { fill: var(--accent); stroke: var(--bg-card); stroke-width: 2; }
144
- .pchart__col { fill: var(--accent); }
145
- .pchart__col--empty { fill: var(--rule-soft); }
146
- .pchart__end { font-family: var(--font-body); font-size: 11.5px; font-weight: 500; fill: var(--ink); }
147
- .pchart__tick { font-family: var(--label-face); font-size: 10px; letter-spacing: var(--label-space); text-transform: var(--label-case); fill: var(--ink-faint); }
137
+ /* The plot itself — the svg, the line, the columns, the axis, the crosshair,
138
+ the ticks — is OTBKit's `.kit-plot*` in style.css (task 1003445). It was
139
+ written twice in this file's own script, and the goal's next charts want it
140
+ again. The dataviz spec moved verbatim; what stays below is this page's
141
+ FIGURE: the caption, the live readout, the table twin and the foot. */
148
142
  .pchart__twin { margin-top: var(--sp-hair); }
149
143
  .pchart__twin > summary { display: inline-flex; align-items: center; min-height: 24px; padding: 0 4px; margin: 0 -4px; font-family: var(--label-face); font-size: var(--label-size); letter-spacing: var(--label-space); text-transform: var(--label-case); color: var(--ink-soft); cursor: pointer; border-radius: var(--radius-sm); }
150
144
  .pchart__twin > summary:hover { color: var(--ink); background: var(--glass-strong); }
@@ -153,13 +147,10 @@
153
147
  .pchart__foot { margin: var(--sp-hair) 0 0; font-size: 12px; color: var(--ink-faint); }
154
148
  .pchart__note { margin: 0; font-size: 14px; color: var(--ink-soft); }
155
149
  /* the craft mix: label, bar, count — a row is its own table row */
156
- .pmix { display: flex; flex-direction: column; gap: 6px; margin: var(--sp-hair) 0; }
157
- .pmix__row { display: grid; grid-template-columns: minmax(72px, 110px) minmax(0, 1fr) auto; align-items: center; gap: 10px; font-size: 12.5px; min-height: 24px; }
158
- .pmix__label { color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
159
- .pmix__track { display: block; height: 8px; background: var(--rule-soft); border-radius: 0 4px 4px 0; overflow: hidden; }
160
- .pmix__fill { display: block; height: 100%; background: var(--accent); border-radius: 0 4px 4px 0; }
161
- .pmix__row:hover .pmix__fill { opacity: .8; }
162
- .pmix__n { color: var(--ink-faint); font-variant-numeric: tabular-nums; }
150
+ /* The craft mix is OTBKit.barSeriesHtml (task 1003445) — the .pmix family was
151
+ the hall's third copy of one horizontal bar. Only the vertical air around it
152
+ is this page's; the row, the track and the 24px floor are the kit's. */
153
+ .pchart .kit-bars { margin: var(--sp-hair) 0; }
163
154
  @media (max-width: 720px) {
164
155
  .pcharts { grid-template-columns: minmax(0, 1fr); }
165
156
  }
@@ -1944,24 +1944,110 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
1944
1944
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
1945
1945
  gap: 12px;
1946
1946
  }
1947
- .curve__chart {
1948
- background: var(--surface-2);
1949
- border-radius: var(--radius-sm);
1950
- padding: 10px 14px 12px;
1947
+ /* ---------- hall-kit charts (task 1003445) ----------
1948
+ The hall's ONE chart vocabulary. Before this the same horizontal bar was
1949
+ written three times — the Curve's kinds, Watch's Review-quality trend, and
1950
+ the profile's craft mix — each with its own class family, geometry and tone,
1951
+ and the line chart a fourth family beside them.
1952
+
1953
+ Three deliberate changes came with the merge, all of them the kit's defaults
1954
+ winning over a copy that had drifted: every row now holds the hall's 24px
1955
+ FLOOR (only the profile's copy remembered it, so the Curve's kind rows grow
1956
+ a few px), the hover dim applies to all three rather than the profile alone,
1957
+ and the value cell right-aligns (a no-op where its column is `auto`).
1958
+ Everything else is declaration-for-declaration what the three families
1959
+ said. */
1960
+
1961
+ .kit-chart { background: var(--surface-2); border-radius: var(--radius-sm); padding: 10px 14px 12px; }
1962
+ .kit-chart__title { font-size: 12px; font-weight: 600; color: var(--ink-soft); margin: 0 0 6px; }
1963
+ .kit-chart__empty { font-size: 12px; color: var(--ink-faint); }
1964
+ .kit-chart__foot { font-size: 11.5px; color: var(--ink-faint); margin-top: 4px; }
1965
+
1966
+ .kit-spark { width: 100%; height: 44px; display: block; }
1967
+ /* The stroke/fill here are the FALLBACK: sparklineHtml writes the caller's
1968
+ colour inline, because the Curve spends a different accent per chart. */
1969
+ .kit-spark__line { fill: none; stroke: var(--accent); stroke-width: 2; }
1970
+ .kit-spark__area { fill: color-mix(in srgb, var(--accent) 14%, transparent); stroke: none; }
1971
+
1972
+ /* Every difference between the three call sites is one of these six custom
1973
+ properties, which is what makes the fifth chart free: pass barSeriesHtml's
1974
+ labelWidth / valueWidth / barHeight / radius / track options, never a new
1975
+ class. */
1976
+ .kit-bars {
1977
+ --kit-bars-label-w: 110px;
1978
+ --kit-bars-val-w: auto;
1979
+ --kit-bars-h: 8px;
1980
+ --kit-bars-radius: 4px;
1981
+ --kit-bars-track: var(--surface-2);
1982
+ --kit-bars-fill: var(--accent);
1983
+ display: flex;
1984
+ flex-direction: column;
1985
+ gap: 6px;
1986
+ }
1987
+ .kit-bars__row {
1988
+ display: grid;
1989
+ grid-template-columns: var(--kit-bars-label-w) minmax(0, 1fr) var(--kit-bars-val-w);
1990
+ align-items: center;
1991
+ gap: 10px;
1992
+ /* The hall's 24px floor, held here so no call site has to remember it —
1993
+ the profile's mix was the only one of the three that did. */
1994
+ min-height: 24px;
1995
+ font-size: 12.5px;
1996
+ font-variant-numeric: tabular-nums;
1951
1997
  }
1952
- .curve__chart-title { font-size: 12px; font-weight: 600; color: var(--ink-soft); margin: 0 0 6px; }
1953
- .curve__chart-empty { font-size: 12px; color: var(--ink-faint); }
1954
- .curve__chart-foot { font-size: 11.5px; color: var(--ink-faint); margin-top: 4px; }
1955
- .spark { width: 100%; height: 44px; display: block; }
1956
- .spark__line { fill: none; stroke: var(--accent); stroke-width: 2; }
1957
- .spark__area { fill: color-mix(in srgb, var(--accent) 14%, transparent); stroke: none; }
1998
+ .kit-bars__label { color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1999
+ .kit-bars__track {
2000
+ display: block;
2001
+ height: var(--kit-bars-h);
2002
+ background: var(--kit-bars-track);
2003
+ border-radius: var(--kit-bars-radius);
2004
+ overflow: hidden;
2005
+ }
2006
+ .kit-bars__fill {
2007
+ display: block;
2008
+ height: 100%;
2009
+ background: var(--kit-bars-fill);
2010
+ border-radius: var(--kit-bars-radius);
2011
+ /* A re-render (an SSE nudge, a lookback change) grows the bar to its new
2012
+ width rather than jumping. First paint does not animate: a freshly
2013
+ inserted element has no previous width to transition from. The hall's ONE
2014
+ reduced-motion kill switch (§the kill switch, at the end of this sheet)
2015
+ already zeroes this with !important — do NOT add a second @media block
2016
+ here, which is what `tests/hall_tokens.mjs` forbids and why the rule
2017
+ cannot rot in two places. */
2018
+ transition: width 200ms ease;
2019
+ }
2020
+ .kit-bars__row:hover .kit-bars__fill { opacity: .8; }
2021
+ .kit-bars__val { text-align: right; color: var(--ink-faint); }
2022
+ /* The neutral tone: a series read as data rather than as the page's own
2023
+ accent — Watch's trend, where the accent belongs to the pressable. */
2024
+ .kit-bars--neutral {
2025
+ --kit-bars-track: var(--rule-soft);
2026
+ --kit-bars-fill: var(--ink-soft);
2027
+ }
2028
+ .kit-bars--neutral .kit-bars__val { color: var(--ink); }
2029
+
2030
+ /* The PLOT: a step line or a column series on a time axis. Moved verbatim from
2031
+ profile.css's .pchart__* (task 1003445), where the same frame was drawn twice
2032
+ by profile-charts.js. The dataviz spec here is measured, not chosen — a 2px
2033
+ accent line, a 10% wash, a hairline solid axis, an r=4 end dot with a 2px
2034
+ card-coloured ring, and the ONE labelled value in ink rather than the series
2035
+ colour. tests/hall_kit_charts.mjs pins all five; do not hand-tune one.
2036
+ `tabindex` is on the svg because the chart is walkable by arrow key, so the
2037
+ focus ring is a real control's, not decoration. */
2038
+ .kit-plot { display: block; width: 100%; height: auto; overflow: visible; border-radius: var(--radius-sm); }
2039
+ .kit-plot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
2040
+ .kit-plot__line { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
2041
+ .kit-plot__area { fill: color-mix(in srgb, var(--accent) 10%, transparent); stroke: none; }
2042
+ .kit-plot__axis, .kit-plot__hair { stroke: var(--rule-soft); stroke-width: 1; }
2043
+ .kit-plot__hair { stroke: var(--ink-faint); }
2044
+ .kit-plot__dot { fill: var(--accent); stroke: var(--bg-card); stroke-width: 2; }
2045
+ .kit-plot__col { fill: var(--accent); }
2046
+ .kit-plot__col--empty { fill: var(--rule-soft); }
2047
+ .kit-plot__end { font-family: var(--font-body); font-size: 11.5px; font-weight: 500; fill: var(--ink); }
2048
+ .kit-plot__tick { font-family: var(--label-face); font-size: 10px; letter-spacing: var(--label-space); text-transform: var(--label-case); fill: var(--ink-faint); }
1958
2049
 
1959
2050
  .curve__kinds { display: flex; flex-direction: column; gap: 6px; margin-top: 16px; }
1960
- .curve__kind { display: grid; grid-template-columns: 110px minmax(0, 1fr) auto; align-items: center; gap: 10px; font-size: 12.5px; }
1961
- .curve__kind-label { color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1962
- .curve__kind-bar { height: 8px; background: var(--surface-2); border-radius: 4px; overflow: hidden; }
1963
- .curve__kind-fill { height: 100%; background: var(--accent); border-radius: 4px; }
1964
- .curve__kind-n { color: var(--ink-faint); font-variant-numeric: tabular-nums; }
1965
2051
 
1966
2052
  /* ---------- onboarding checklist ---------- */
1967
2053
 
@@ -55,20 +55,12 @@
55
55
  /* The 14-day grade trend: a day, a bar, then the count and score. The bar is a
56
56
  measurement, not a control, so it is ink on a hairline track and never the
57
57
  accent. */
58
- .watch-trend { display: flex; flex-direction: column; margin-top: 4px; }
59
- .watch-trend__row {
60
- display: grid;
61
- grid-template-columns: 64px minmax(0, 1fr) 96px;
62
- gap: 12px;
63
- align-items: center;
64
- min-height: 28px;
65
- font-size: 13px;
66
- color: var(--ink-soft);
67
- font-variant-numeric: tabular-nums;
68
- }
69
- .watch-trend__bar-track { height: 6px; overflow: hidden; border-radius: var(--radius-pill); background: var(--rule-soft); }
70
- .watch-trend__bar-fill { display: block; height: 100%; background: var(--ink-soft); }
71
- .watch-trend__val { text-align: right; color: var(--ink); }
58
+ /* The Review-quality trend is OTBKit.barSeriesHtml in its neutral tone (task
59
+ 1003445) — the .watch-trend family was the hall's second copy of one
60
+ horizontal bar. What stays here is this page's own density: a taller row and
61
+ more air between the three columns than the kit's default. */
62
+ #mason-marks-trend .kit-bars { margin-top: 4px; gap: 0; }
63
+ #mason-marks-trend .kit-bars__row { min-height: 28px; gap: 12px; font-size: 13px; }
72
64
 
73
65
  /* A link inside a ledger cell (a session's tasks) is a control at a 13.5px line
74
66
  box: the padding buys the 24px floor and the negative margin hands it back. */
@@ -59,7 +59,7 @@
59
59
  <!-- 1. ACCESS REQUESTS -->
60
60
  <section class="scroll" id="gate-scroll" aria-labelledby="gate-h" hidden>
61
61
  <h2 id="gate-h" class="scroll__h">Access requests</h2>
62
- <p class="scroll__lede">People asking to join. Approve or dismiss each request.</p>
62
+ <p class="scroll__lede">People asking to join. Approve or dismiss each request, re-read the ones you already decided, and take back an approval before that person has signed in.</p>
63
63
  <div id="gate-body"><span class="kit-skel"></span></div>
64
64
  </section>
65
65
 
@@ -26,6 +26,14 @@
26
26
  // family's .ov-figs, every sub-section is an .ov-group head, the access requests
27
27
  // are oversight rows, and the five most recent sessions the page always fetched
28
28
  // and threw away are now rendered under the tally.
29
+ //
30
+ // TASK 1002332 made the access-requests section a REVIEW queue rather than a
31
+ // decision button: every row says whether the platform vouched for the name on
32
+ // it (ADR 0201 — the public unauthenticated write means anyone may apply as
33
+ // anyone, so vouched/unvouched is load-bearing, not decoration), the three
34
+ // lifecycle states sit behind one switch, and an approval can be RESCINDED
35
+ // before its holder ever signs in — a transition the route has always taken and
36
+ // nothing on this page offered (ADR 0208).
29
37
 
30
38
  (() => {
31
39
  'use strict';
@@ -43,7 +51,7 @@
43
51
  const { escapeHtml, $, num, fmtInt, fmtCompact } = window.OTB;
44
52
  // Shared component kit (hall-kit.js, task 2166): tabs, ledger, pager, empty
45
53
  // states — adopted here by task 2173, and by every list since task 1003469.
46
- const { makeTabs, makeLedger, makePager, emptyStateHtml } = window.OTBKit;
54
+ const { makeTabs, makeLedger, makePager, makeFilterBar, emptyStateHtml } = window.OTBKit;
47
55
  const fmtDate = (iso) => window.OTB.fmtDate(iso, { time: true });
48
56
  const rankLabel = (rank) => window.OTB.rankLabel(rank, { empty: '' });
49
57
  const toast = (msg) => window.OTB.toast(msg);
@@ -291,50 +299,131 @@
291
299
  });
292
300
  }
293
301
 
294
- // One request, one oversight row: who, their note as the sub line, when they
295
- // asked as the fact, and the two decisions on the right.
302
+ // The three lists the queue is, behind one switch. Reviewing is not only
303
+ // deciding: an owner has to re-read what they already decided, and RESCIND an
304
+ // approval given by mistake before the applicant ever signed in — the route
305
+ // has always taken that transition (ADR 0201 D4.2) and nothing offered it,
306
+ // because this page only ever fetched `?status=pending` (ADR 0208).
307
+ const GATE_VIEWS = [
308
+ { value: 'pending', label: 'Waiting', empty: 'No pending access requests.' },
309
+ { value: 'invited', label: 'Admitted', empty: 'Nobody has been admitted yet.' },
310
+ { value: 'dismissed', label: 'Declined', empty: 'No declined requests.' },
311
+ ];
312
+ let gateView = 'pending';
313
+
314
+ function gateEmptyMsg() {
315
+ return (GATE_VIEWS.find((v) => v.value === gateView) || GATE_VIEWS[0]).empty;
316
+ }
317
+
318
+ // The switch itself, rebuilt on every render because `section` replaces the
319
+ // whole body — the same lifecycle wireInviteForm already lives with.
320
+ function wireGateView(target) {
321
+ const mount = target.querySelector('#gate-view');
322
+ if (!mount) return;
323
+ makeFilterBar({
324
+ mount,
325
+ primary: [{
326
+ id: 'gate-status',
327
+ label: 'Show',
328
+ value: gateView,
329
+ options: GATE_VIEWS.map((v) => ({ value: v.value, label: v.label })),
330
+ }],
331
+ onChange: (vals) => {
332
+ const next = vals['gate-status'];
333
+ if (next === gateView) return;
334
+ gateView = next;
335
+ loadGate();
336
+ },
337
+ });
338
+ }
339
+
340
+ // Whether the platform vouched for this applicant, said on the row — and the
341
+ // single thing that makes the queue safe to read (ADR 0201, Consequences).
342
+ // POST /access-requests is the one PUBLIC unauthenticated write, so anyone may
343
+ // file a request under anyone's name; a row that merely LOOKED vouched is the
344
+ // whole impersonation surface the vouch exists to close. It is never authority
345
+ // (ADR 0016) — an unvouched applicant is admitted by the identical statement —
346
+ // it only says whether anyone confirmed who is asking.
347
+ function vouchPill(vouched) {
348
+ return vouched
349
+ ? '<span class="fact-pill fact-pill--ok" title="The platform confirmed this request came from the account it names.">Identity vouched</span>'
350
+ : '<span class="fact-pill fact-pill--warn" title="Nobody vouched for this name — anyone can file a request naming anyone. Confirm who is asking before you admit them.">Name unvouched</span>';
351
+ }
352
+
353
+ // What this row still offers. A waiting request has two decisions; an admitted
354
+ // one can still be rescinded (until they sign in, after which the server
355
+ // refuses and says so); a declined one is terminal here — re-admitting is the
356
+ // invite form above, which is what the server's own refusal names.
357
+ function gateRowActions(r) {
358
+ const id = escapeHtml(r.id);
359
+ if (r.status === 'invited') {
360
+ return ghost(`data-gate-action="dismissed" data-gate-from="invited" data-id="${id}"`, 'Rescind', true);
361
+ }
362
+ if (r.status === 'dismissed') return '';
363
+ return ghost(`data-gate-action="invited" data-id="${id}"`, 'Invite')
364
+ + ghost(`data-gate-action="dismissed" data-id="${id}"`, 'Dismiss', true);
365
+ }
366
+
367
+ // One request, one oversight row: who, whether anyone vouched for them, their
368
+ // note as the sub line, when they asked (and who decided, once someone has) as
369
+ // the facts, and whatever decisions are still open on the right.
370
+ function gateRowHtml(r) {
371
+ const who = r.display_name
372
+ ? `${escapeHtml(r.display_name)}<span class="ov-mono">@${escapeHtml(r.github_login)}</span>`
373
+ : `<span class="ov-mono">@${escapeHtml(r.github_login)}</span>`;
374
+ const note = r.note ? `<p class="ov-row__sub">“${escapeHtml(r.note)}”</p>` : '';
375
+ // A rescind overwrites the approver with the rescinder (ADR 0208), so this
376
+ // names who put the row in the state it is in now — which is what it says.
377
+ const by = r.resolved_by_login ? ` by <span class="ov-mono">@${escapeHtml(r.resolved_by_login)}</span>` : '';
378
+ const decided = r.resolved_at
379
+ ? `<span class="ov-row__when">${r.status === 'invited' ? 'Admitted' : 'Declined'} ${escapeHtml(fmtDate(r.resolved_at))}${by}</span>`
380
+ : '';
381
+ const actions = gateRowActions(r);
382
+ return (
383
+ `<li class="ov-row" data-req-id="${escapeHtml(r.id)}">` +
384
+ '<div class="ov-row__main">' +
385
+ `<span class="ov-row__title">${who}</span>` +
386
+ note +
387
+ '<div class="ov-row__facts">' +
388
+ vouchPill(!!r.vouched) +
389
+ `<span class="ov-row__when">Asked ${escapeHtml(fmtDate(r.created_at))}</span>` +
390
+ decided +
391
+ '</div>' +
392
+ '</div>' +
393
+ (actions ? `<div class="ov-row__right"><div class="ov-row__actions">${actions}</div></div>` : '') +
394
+ '</li>'
395
+ );
396
+ }
397
+
296
398
  function renderGate(data, target) {
297
399
  const requests = Array.isArray(data.requests) ? data.requests : [];
298
- setAttention('att-access', requests.length); // tile count = table content
299
- if (!requests.length) {
300
- target.innerHTML = inviteFormHtml() + emptyStateHtml('No pending access requests.');
301
- wireInviteForm(target);
302
- return;
400
+ // The attention tile counts WAITING work, never whatever list is on screen:
401
+ // reading the decided history must not repaint the board with a number that
402
+ // means something else. Its own sub line says "pending".
403
+ if (gateView === 'pending') setAttention('att-access', requests.length);
404
+ const head = inviteFormHtml() + '<div id="gate-view"></div>';
405
+ target.innerHTML = requests.length
406
+ ? head + '<ul class="ov-rows">' + requests.map(gateRowHtml).join('') + '</ul>'
407
+ : head + emptyStateHtml(gateEmptyMsg());
408
+
409
+ if (requests.length) {
410
+ wireActions(target, '[data-gate-action]', async (btn) => {
411
+ const id = btn.getAttribute('data-id');
412
+ const status = btn.getAttribute('data-gate-action');
413
+ const rescinding = btn.getAttribute('data-gate-from') === 'invited';
414
+ await patchJson(`/access-requests/${encodeURIComponent(id)}`, { status });
415
+ toast(status === 'invited' ? 'Builder invited.'
416
+ : rescinding ? 'Approval rescinded — that sign-in no longer gets in.'
417
+ : 'Request dismissed.');
418
+ await loadGate();
419
+ }, 'Could not resolve');
303
420
  }
304
- target.innerHTML = inviteFormHtml() + '<ul class="ov-rows">' + requests
305
- .map((r) => {
306
- const who = r.display_name
307
- ? `${escapeHtml(r.display_name)}<span class="ov-mono">@${escapeHtml(r.github_login)}</span>`
308
- : `<span class="ov-mono">@${escapeHtml(r.github_login)}</span>`;
309
- const note = r.note ? `<p class="ov-row__sub">“${escapeHtml(r.note)}”</p>` : '';
310
- return (
311
- `<li class="ov-row" data-req-id="${escapeHtml(r.id)}">` +
312
- '<div class="ov-row__main">' +
313
- `<span class="ov-row__title">${who}</span>` +
314
- note +
315
- `<div class="ov-row__facts"><span class="ov-row__when">Asked ${escapeHtml(fmtDate(r.created_at))}</span></div>` +
316
- '</div>' +
317
- '<div class="ov-row__right"><div class="ov-row__actions">' +
318
- ghost(`data-gate-action="invited" data-id="${escapeHtml(r.id)}"`, 'Invite') +
319
- ghost(`data-gate-action="dismissed" data-id="${escapeHtml(r.id)}"`, 'Dismiss', true) +
320
- '</div></div>' +
321
- '</li>'
322
- );
323
- })
324
- .join('') + '</ul>';
325
-
326
- wireActions(target, '[data-gate-action]', async (btn) => {
327
- const id = btn.getAttribute('data-id');
328
- const status = btn.getAttribute('data-gate-action');
329
- await patchJson(`/access-requests/${encodeURIComponent(id)}`, { status });
330
- toast(status === 'invited' ? 'Builder invited.' : 'Request dismissed.');
331
- await loadGate();
332
- }, 'Could not resolve');
333
421
  wireInviteForm(target);
422
+ wireGateView(target);
334
423
  }
335
424
 
336
425
  function loadGate() {
337
- return section($('#gate-body'), () => fetchJson('/access-requests?status=pending'), renderGate, {
426
+ return section($('#gate-body'), () => fetchJson(`/access-requests?status=${encodeURIComponent(gateView)}`), renderGate, {
338
427
  sealedMsg: 'Archons only. Access requests are not visible at your rank.',
339
428
  errMsg: 'Could not load access requests.',
340
429
  name: 'gate',
@@ -782,23 +871,19 @@
782
871
  trendEl.innerHTML = emptyStateHtml('No trend to show yet.');
783
872
  return;
784
873
  }
785
- const maxN = Math.max(1, ...trend.map((t) => num(t.n)));
874
+ // task 1003445: this was the hall's third copy of the same horizontal bar.
875
+ // It draws through OTBKit.barSeriesHtml now, in the NEUTRAL tone — on an
876
+ // oversight page the accent belongs to the pressable, not to the data —
877
+ // with its own geometry passed as options rather than a second class family.
786
878
  trendEl.innerHTML =
787
- '<div class="watch-trend">' +
788
- trend
789
- .map((t) => {
790
- const w = Math.round((num(t.n) / maxN) * 100);
791
- const avg = (t.avg_score ?? '—') === '—' ? '—' : escapeHtml(String(t.avg_score));
792
- return (
793
- '<div class="watch-trend__row">' +
794
- `<span>${escapeHtml(fmtDay(t.day))}</span>` +
795
- `<span class="watch-trend__bar-track"><span class="watch-trend__bar-fill" style="width:${w}%"></span></span>` +
796
- `<span class="watch-trend__val">${fmtInt(t.n)} · ${avg}</span>` +
797
- '</div>'
798
- );
799
- })
800
- .join('') +
801
- '</div>' +
879
+ window.OTBKit.barSeriesHtml(
880
+ trend.map((t) => ({
881
+ label: fmtDay(t.day),
882
+ value: num(t.n),
883
+ valueLabel: `${fmtInt(t.n)} · ${(t.avg_score ?? '—') === '—' ? '—' : String(t.avg_score)}`,
884
+ })),
885
+ { tone: 'neutral', labelWidth: '64px', valueWidth: '96px', barHeight: '6px', pill: true },
886
+ ) +
802
887
  foot('Per day: ships graded · avg score.');
803
888
  }
804
889
 
@@ -118,11 +118,37 @@ const RESTART_SHAPES = new Set(['co-tenant', 'standalone']);
118
118
  // (migration provisioning_009) is a dumb store with room for future keys.
119
119
  // DEFAULT_SETTINGS is TODAY's behavior (public surfaces, apply-to-join),
120
120
  // applied at read time so pre-existing rows need no data backfill.
121
+ //
122
+ // `visibility` is ADR 0182's STORED JOIN DOOR (BV1.R05, task 1002324) and it is a
123
+ // third axis, not a synonym of its neighbours — the three answer three different
124
+ // questions and an owner may set them independently:
125
+ //
126
+ // platform_visibility who may READ this project's own pages (its sign-in door)
127
+ // joinability HOW someone becomes a member (self-join / apply / invite)
128
+ // visibility how the project stands on the PLATFORM's map: named and
129
+ // open (public), named and closed (private), or present but
130
+ // anonymous (stealth)
131
+ //
132
+ // The name is ADR 0182 D1's deliberate choice — `goals.visibility` established the
133
+ // word and one word across both planes beat an accurate name used in one place —
134
+ // and D1 also records that it is a mild lie: this key does NOT decide whether the
135
+ // project is on the map. PUBLISHABILITY does (a name + a description, ADR 0182 D4),
136
+ // it is derived and never stored, and no value here can put a husk on the map.
137
+ //
138
+ // It is a POLICY, not a platform-drawn key: ADR 0182 D6 makes the door the CEILING
139
+ // that `joinability` may only narrow, and D1/D5 put that authority on the instance —
140
+ // so it rides web.env like its two neighbours and a restart applies it. The instance
141
+ // reads it back as `project.visibility` (src/branding.js), which is what lets a later
142
+ // task enforce the door there and report the value to the hub catalog (R08).
121
143
  const SETTINGS_VOCAB = Object.freeze({
122
144
  platform_visibility: ['public', 'gated'],
123
145
  joinability: ['open', 'apply', 'invite_only'],
146
+ visibility: ['public', 'private', 'stealth'],
124
147
  });
125
- const DEFAULT_SETTINGS = Object.freeze({ platform_visibility: 'public', joinability: 'apply' });
148
+ // Default = today's behavior on every axis, including the join door: ADR 0182's
149
+ // migration gives the hub column the same `public` default, "existing rows keep
150
+ // today's behaviour".
151
+ const DEFAULT_SETTINGS = Object.freeze({ platform_visibility: 'public', joinability: 'apply', visibility: 'public' });
126
152
 
127
153
  // The project's PLANET (task 1003347, direction record §16): how the platform
128
154
  // DRAWS it on the sky, in the owner's list and on the card — its template