@bongos/core 1.20.47 → 1.20.49
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 +108 -73
- package/docs/copy-inventory.md +200 -184
- package/docs/copy-registry.json +364 -220
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +11 -4
- package/docs/page-readings.json +411 -386
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/criterion-uat-ui.js +280 -0
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +5 -3
- package/modules/hall-ui/public/goals-page.js +23 -44
- package/modules/hall-ui/public/goals-render.js +24 -15
- package/modules/hall-ui/public/goals.css +45 -0
- package/modules/hall-ui/public/goals.html +6 -3
- package/modules/hall-ui/public/goals.states.json +38 -0
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/hall-render.js +3 -0
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +82 -1
- package/modules/hall-ui/public/sky-data.js +9 -1
- package/modules/hall-ui/public/sky-interaction.js +1 -1
- package/modules/hall-ui/public/sky-panel.js +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +36 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +6 -4
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work-founding.states.json +39 -0
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/founding-mode.md +31 -0
- package/modules/lifecycle/done-when.js +3 -1
- package/modules/lifecycle/sky.js +23 -8
- package/modules/onboarding/founding-mode.js +185 -0
- package/modules/onboarding/port.js +7 -0
- package/modules/ui-design/kit/fixtures/me-founding.json +24 -0
- package/modules/ui-design/kit/serve.js +3 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/hall-preview/server.js +2 -0
- package/src/bongos/routes/me.js +18 -1
- package/src/module-api.js +1 -1
- package/tests/claim_action.mjs +6 -1
- package/tests/criterion_uat_ui.mjs +113 -0
- package/tests/founding_mode.mjs +294 -0
- package/tests/goal_criterion_confirm_ui.mjs +188 -91
- package/tests/goal_map_page.mjs +2 -1
- package/tests/hall_sky.mjs +16 -0
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/helpers.mjs +3 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/sky_read.mjs +7 -1
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
|
|
92
92
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
93
93
|
|
|
94
|
-
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
94
|
+
<script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
|
|
95
95
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
96
96
|
<script src="/builders/palette.js" defer></script>
|
|
97
97
|
<script src="/builders/atlas.js?v=2026-09-02-index" defer></script>
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
52
52
|
chrome exists before the page script runs. -->
|
|
53
|
-
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
53
|
+
<script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
|
|
54
54
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
55
55
|
<script src="/builders/palette.js" defer></script>
|
|
56
56
|
<!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
|
|
61
61
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
62
62
|
|
|
63
|
-
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
63
|
+
<script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
|
|
64
64
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
65
65
|
<script src="/builders/palette.js" defer></script>
|
|
66
66
|
<script src="/builders/dom-utils.js?v=2026-06-16-board"></script>
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
|
|
149
149
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
150
150
|
|
|
151
|
-
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
151
|
+
<script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
|
|
152
152
|
<script src="/builders/palette.js" defer></script>
|
|
153
153
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
154
154
|
<script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
|
|
55
55
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
56
56
|
|
|
57
|
-
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
57
|
+
<script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
|
|
58
58
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
59
59
|
<script src="/builders/palette.js" defer></script>
|
|
60
60
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
|
|
178
178
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
179
179
|
|
|
180
|
-
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
180
|
+
<script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
|
|
181
181
|
<script src="/builders/palette.js" defer></script>
|
|
182
182
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
183
183
|
<script src="/builders/hall-kit.js"></script>
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// criterion-uat-ui.js — the UAT on a goal's criteria, in the hall (task 1004402,
|
|
2
|
+
// piece 2 of ADR 0351; piece 1, task 1004392, is the rule and the API).
|
|
3
|
+
//
|
|
4
|
+
// WHY. A criterion no longer closes because its linked tasks shipped. Once they
|
|
5
|
+
// have, it reads AWAITING UAT, and it closes only when someone who did not ship
|
|
6
|
+
// the work tries it on the live site and signs it off with a recording (or, for a
|
|
7
|
+
// criterion marked backend-only, a sign-off without one). Before this file that
|
|
8
|
+
// was reachable only from a Claude session (/goal-uat); this is the same act for
|
|
9
|
+
// a person in a browser.
|
|
10
|
+
//
|
|
11
|
+
// WHAT IT DRAWS, per criterion row (goals-render.js critListHtml asks it):
|
|
12
|
+
// - a STATE pill in the server's own words (Awaiting UAT, Satisfied (UAT), …)
|
|
13
|
+
// and a Backend-only tag — never re-derived here: `uat_state` and
|
|
14
|
+
// `backend_only` come from GET /goals/:id, and a server too old to send them
|
|
15
|
+
// falls back to the plain satisfied/open reading;
|
|
16
|
+
// - the ACTION: "Test and sign off" on an Awaiting UAT row, "See the UAT" on a
|
|
17
|
+
// row a UAT closed, and on an OPEN row in the full view the backend-only
|
|
18
|
+
// switch (the server refuses it once work has shipped, and says why).
|
|
19
|
+
// Opening a row loads GET /done-when/:id/uat into a panel under it: the three
|
|
20
|
+
// checks (Code, Live, UAT), the recording if there is one, and the sign-off form.
|
|
21
|
+
//
|
|
22
|
+
// THE SERVER IS THE WALL. Every refusal (you shipped this work, not live yet,
|
|
23
|
+
// too big, wrong file type, backend-only locked) is shown in the words the server
|
|
24
|
+
// sent; nothing here decides who may sign. A control shown to someone who may not
|
|
25
|
+
// use it gets an honest refusal, never a silent no-op.
|
|
26
|
+
//
|
|
27
|
+
// Loaded before goals-render.js (goals.html); pure browser code, no build step.
|
|
28
|
+
// The vocabulary helpers are exported for the page test (tests/criterion_uat_ui.mjs).
|
|
29
|
+
(function () {
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
// The labels, identical to modules/lifecycle/criterion-uat.js stateLabel (the
|
|
33
|
+
// browser cannot require server code; tests/criterion_uat_ui.mjs holds the two
|
|
34
|
+
// equal).
|
|
35
|
+
const STATE_LABELS = {
|
|
36
|
+
open: 'Open',
|
|
37
|
+
awaiting_uat: 'Awaiting UAT',
|
|
38
|
+
satisfied_uat: 'Satisfied (UAT)',
|
|
39
|
+
satisfied_backend: 'Satisfied (backend sign-off)',
|
|
40
|
+
satisfied_override: 'Satisfied (override)',
|
|
41
|
+
closed_before_uat: 'Closed before UAT',
|
|
42
|
+
};
|
|
43
|
+
const MAX_BYTES = 100 * 1024 * 1024;
|
|
44
|
+
const TYPES = ['video/mp4', 'video/webm'];
|
|
45
|
+
|
|
46
|
+
function esc(s) {
|
|
47
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The state a row reads as. A server older than the UAT rule sends no
|
|
51
|
+
// uat_state; then satisfied/open is the whole truth and no UAT control shows.
|
|
52
|
+
function stateOf(c) {
|
|
53
|
+
if (c && c.uat_state && STATE_LABELS[c.uat_state]) return c.uat_state;
|
|
54
|
+
return c && c.satisfied ? 'legacy_satisfied' : 'open';
|
|
55
|
+
}
|
|
56
|
+
function isAwaiting(c) { return stateOf(c) === 'awaiting_uat'; }
|
|
57
|
+
|
|
58
|
+
// The pill + tag that follow the criterion text. Open rows carry no pill (the
|
|
59
|
+
// empty mark already says open), and a legacy server gets nothing new.
|
|
60
|
+
function badgesHtml(c) {
|
|
61
|
+
const s = stateOf(c);
|
|
62
|
+
const pill = (s === 'open' || s === 'legacy_satisfied') ? ''
|
|
63
|
+
: `<span class="crit-state crit-state--${esc(s)}">${esc(STATE_LABELS[s])}</span>`;
|
|
64
|
+
const tag = c && c.backend_only ? '<span class="crit-tag" title="No screen to record: this is signed off without a video">Backend-only</span>' : '';
|
|
65
|
+
return pill + tag;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The trailing action. canReview = the viewer may sign off (Metic+; the server
|
|
69
|
+
// re-checks). full = the dedicated completion view, where the backend-only
|
|
70
|
+
// switch lives (it is an authoring act, not a glance).
|
|
71
|
+
function actionHtml(c, { canReview, full }) {
|
|
72
|
+
const id = esc(String(c.id));
|
|
73
|
+
const s = stateOf(c);
|
|
74
|
+
if (s === 'awaiting_uat') {
|
|
75
|
+
return canReview
|
|
76
|
+
? `<button type="button" class="crit__confirm" data-uat-open="${id}" aria-expanded="false">Test and sign off</button>`
|
|
77
|
+
: '<span class="crit__flag" title="Its work has shipped; someone who did not build it needs to try it on the live site">Awaiting UAT</span>';
|
|
78
|
+
}
|
|
79
|
+
if (s === 'satisfied_uat' || s === 'satisfied_backend') {
|
|
80
|
+
return `<button type="button" class="crit__link" data-uat-open="${id}" aria-expanded="false">See the UAT</button>`;
|
|
81
|
+
}
|
|
82
|
+
if (s === 'open' && full && canReview && c.uat_state) {
|
|
83
|
+
return c.backend_only
|
|
84
|
+
? `<button type="button" class="crit__link" data-uat-backend="${id}" data-on="0" title="It needs a recording again">Not backend-only</button>`
|
|
85
|
+
: `<button type="button" class="crit__link" data-uat-backend="${id}" data-on="1" title="For a criterion with nothing to see on screen: it will be signed off without a recording">Mark backend-only</button>`;
|
|
86
|
+
}
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── the panel ──────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function fmtDate(v) {
|
|
93
|
+
const d = new Date(v);
|
|
94
|
+
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function liveLine(live) {
|
|
98
|
+
if (live == null) return '';
|
|
99
|
+
if (!live.available) return 'This site cannot tell on its own. Confirm below that you tried it on the live site.';
|
|
100
|
+
if (live.live) return 'Running on the live site.';
|
|
101
|
+
const ids = (live.not_live || []).map((t) => `#${esc(t.task_id)}`).join(', ');
|
|
102
|
+
return `Shipped but not on the live site yet${ids ? ` (${ids})` : ''}. Sign off once it is deployed.`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function checkRow(ok, label, text) {
|
|
106
|
+
return `<li class="uat-check${ok ? ' uat-check--ok' : ''}"><span class="uat-check__mark" aria-hidden="true">${ok ? '✓' : '·'}</span>`
|
|
107
|
+
+ `<span class="uat-check__label">${label}</span><span class="uat-check__text">${text}</span></li>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function signoffText(s) {
|
|
111
|
+
if (!s) return '';
|
|
112
|
+
const who = esc(s.signed_off_by_login || `builder ${s.signed_off_by}`);
|
|
113
|
+
const kind = s.kind === 'uat' ? 'Tried on the live site' : 'Signed off (backend)';
|
|
114
|
+
return `${kind} by ${who} on ${esc(fmtDate(s.created_at))}${s.note ? ` — “${esc(s.note)}”` : ''}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function recordingHtml(s, apiBase) {
|
|
118
|
+
if (!s || !s.recording_file) return '';
|
|
119
|
+
const src = `${apiBase}/criterion-uats/recordings/${encodeURIComponent(s.recording_file)}`;
|
|
120
|
+
return `<video class="uat-video" controls preload="metadata" src="${esc(src)}">`
|
|
121
|
+
+ `<a href="${esc(src)}">Open the recording</a></video>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The panel body, from GET /done-when/:id/uat. Pure so the test can render it.
|
|
125
|
+
function panelHtml(view, { apiBase, canReview }) {
|
|
126
|
+
const c = view.checks || {};
|
|
127
|
+
const awaiting = view.uat_state === 'awaiting_uat';
|
|
128
|
+
const uatText = c.uat ? signoffText(c.uat)
|
|
129
|
+
: (c.stale_uat ? `An earlier sign-off (${signoffText(c.stale_uat)}) no longer counts: more work shipped after it.` : 'Not yet.');
|
|
130
|
+
const codeText = c.code ? 'Every linked task is done.' : 'Some linked work is still open.';
|
|
131
|
+
const checks = `<ul class="uat-checks">`
|
|
132
|
+
+ checkRow(!!c.code, 'Code', esc(codeText))
|
|
133
|
+
+ (awaiting ? checkRow(!!(c.live && c.live.available && c.live.live), 'Live', liveLine(c.live)) : '')
|
|
134
|
+
+ checkRow(!!c.uat, view.backend_only ? 'Sign-off' : 'UAT', uatText)
|
|
135
|
+
+ '</ul>';
|
|
136
|
+
const video = recordingHtml(c.uat || c.stale_uat, apiBase);
|
|
137
|
+
if (!awaiting || !canReview) return checks + video;
|
|
138
|
+
|
|
139
|
+
const id = esc(String(view.criterion.id));
|
|
140
|
+
const needsAttest = !(c.live && c.live.available);
|
|
141
|
+
const file = view.backend_only ? ''
|
|
142
|
+
: `<label class="uat-field"><span>Screen recording of you doing this on the live site (mp4 or webm, up to 100 MB)</span>`
|
|
143
|
+
+ `<input type="file" accept="video/mp4,video/webm" data-uat-file="${id}"></label>`;
|
|
144
|
+
const attest = needsAttest
|
|
145
|
+
? `<label class="uat-check-input"><input type="checkbox" data-uat-attest="${id}"> I tried this on the live site</label>` : '';
|
|
146
|
+
return checks + video + `<form class="uat-form" data-uat-form="${id}" novalidate>`
|
|
147
|
+
+ (view.backend_only ? '<p class="uat-hint">Backend-only: nothing to record. Check it the way it can be checked (a log, a report, a test against the live site) and say how below.</p>' : '')
|
|
148
|
+
+ file
|
|
149
|
+
+ `<label class="uat-field"><span>${view.backend_only ? 'How you checked it' : 'What you checked (optional)'}</span>`
|
|
150
|
+
+ `<textarea rows="2" maxlength="2000" data-uat-note="${id}"></textarea></label>`
|
|
151
|
+
+ attest
|
|
152
|
+
+ `<p class="uat-error" role="alert" data-uat-error="${id}" hidden></p>`
|
|
153
|
+
+ `<button type="submit" class="crit__confirm">${view.backend_only ? 'Sign off' : 'Upload and sign off'}</button>`
|
|
154
|
+
+ '</form>';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The server's own sentence for a refusal, else a plain fallback. The envelope
|
|
158
|
+
// is { error: { code, message } }; the API client may also hand it back flat.
|
|
159
|
+
function refusalText(data, status) {
|
|
160
|
+
const e = data && data.error;
|
|
161
|
+
const msg = e && typeof e === 'object' ? e.message : null;
|
|
162
|
+
if (msg && msg !== (e && e.code)) return msg;
|
|
163
|
+
if (status === 413) return 'The recording is over the 100 MB cap. Record a shorter pass, or lower the resolution.';
|
|
164
|
+
if (status === 403) return 'Only a Metic or an Archon who did not ship this work can sign it off. Ask someone else on the goal to try it.';
|
|
165
|
+
return `That did not go through (${status || 'error'}). Try again.`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Screen a chosen file before sending 100 MB anywhere.
|
|
169
|
+
function checkFile(file) {
|
|
170
|
+
if (!file) return 'Choose the recording first.';
|
|
171
|
+
if (!TYPES.includes(file.type)) return 'A UAT recording must be an mp4 or webm video.';
|
|
172
|
+
if (file.size > MAX_BYTES) return 'The recording is over the 100 MB cap. Record a shorter pass, or lower the resolution.';
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// wire(root, deps) — one delegated listener set on a criteria container.
|
|
177
|
+
// deps: { apiBase, getJSON, postJSON, putJSON, canReview(), toast, reload }
|
|
178
|
+
function wire(root, deps) {
|
|
179
|
+
if (!root || root.__uatWired) return;
|
|
180
|
+
root.__uatWired = true;
|
|
181
|
+
|
|
182
|
+
const showError = (id, text) => {
|
|
183
|
+
const el = root.querySelector(`[data-uat-error="${CSS.escape(id)}"]`);
|
|
184
|
+
if (el) { el.textContent = text; el.hidden = !text; }
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
async function openPanel(btn) {
|
|
188
|
+
const id = btn.dataset.uatOpen;
|
|
189
|
+
const row = btn.closest('.crit');
|
|
190
|
+
if (!row) return;
|
|
191
|
+
const existing = row.querySelector('.uat-panel');
|
|
192
|
+
if (existing) { existing.remove(); btn.setAttribute('aria-expanded', 'false'); return; }
|
|
193
|
+
const panel = document.createElement('div');
|
|
194
|
+
panel.className = 'uat-panel';
|
|
195
|
+
panel.textContent = 'Loading…';
|
|
196
|
+
row.appendChild(panel);
|
|
197
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
198
|
+
try {
|
|
199
|
+
const view = await deps.getJSON(`${deps.apiBase}/done-when/${encodeURIComponent(id)}/uat`);
|
|
200
|
+
panel.innerHTML = panelHtml(view, { apiBase: deps.apiBase, canReview: deps.canReview() });
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (err && err.message === 'redirecting to auth') return;
|
|
203
|
+
panel.textContent = refusalText(err && err.data, err && err.status);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function submit(form) {
|
|
208
|
+
const id = form.dataset.uatForm;
|
|
209
|
+
const btn = form.querySelector('button[type="submit"]');
|
|
210
|
+
const fileInput = form.querySelector('[data-uat-file]');
|
|
211
|
+
const note = (form.querySelector('[data-uat-note]') || {}).value || '';
|
|
212
|
+
const attestBox = form.querySelector('[data-uat-attest]');
|
|
213
|
+
showError(id, '');
|
|
214
|
+
const backend = !fileInput;
|
|
215
|
+
const file = fileInput && fileInput.files && fileInput.files[0];
|
|
216
|
+
if (!backend) { const bad = checkFile(file); if (bad) return showError(id, bad); }
|
|
217
|
+
if (backend && !note.trim()) return showError(id, 'Say how you checked it.');
|
|
218
|
+
if (attestBox && !attestBox.checked) return showError(id, 'Confirm that you tried it on the live site.');
|
|
219
|
+
const label = btn.textContent;
|
|
220
|
+
btn.disabled = true;
|
|
221
|
+
try {
|
|
222
|
+
let recording = null;
|
|
223
|
+
if (!backend) {
|
|
224
|
+
btn.textContent = 'Uploading…';
|
|
225
|
+
const res = await fetch(`${deps.apiBase}/done-when/${encodeURIComponent(id)}/uat/recording`, {
|
|
226
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': file.type }, body: file,
|
|
227
|
+
});
|
|
228
|
+
const data = await res.json().catch(() => ({}));
|
|
229
|
+
if (!res.ok) { showError(id, refusalText(data, res.status)); return; }
|
|
230
|
+
recording = data.recording;
|
|
231
|
+
}
|
|
232
|
+
btn.textContent = 'Signing off…';
|
|
233
|
+
const body = { kind: backend ? 'backend_signoff' : 'uat' };
|
|
234
|
+
if (recording) body.recording = recording;
|
|
235
|
+
if (note.trim()) body.note = note.trim();
|
|
236
|
+
if (attestBox && attestBox.checked) body.live_attested = true;
|
|
237
|
+
const out = await deps.postJSON(`${deps.apiBase}/done-when/${encodeURIComponent(id)}/uat`, body);
|
|
238
|
+
deps.toast(out && out.achieved_goals && out.achieved_goals.length
|
|
239
|
+
? 'Signed off. That was the last criterion, so this goal is achieved.'
|
|
240
|
+
: (out && out.criterion_closed ? 'Signed off. The criterion is closed.' : 'Signed off.'));
|
|
241
|
+
await deps.reload();
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (err && err.message === 'redirecting to auth') return;
|
|
244
|
+
showError(id, refusalText(err && err.data, err && err.status));
|
|
245
|
+
} finally {
|
|
246
|
+
if (btn.isConnected) { btn.disabled = false; btn.textContent = label; }
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function toggleBackend(btn) {
|
|
251
|
+
const id = btn.dataset.uatBackend;
|
|
252
|
+
const on = btn.dataset.on === '1';
|
|
253
|
+
btn.disabled = true;
|
|
254
|
+
try {
|
|
255
|
+
await deps.putJSON(`${deps.apiBase}/done-when/${encodeURIComponent(id)}/backend-only`, { backend_only: on });
|
|
256
|
+
deps.toast(on ? 'Marked backend-only: it will be signed off without a recording.' : 'It needs a recording again.');
|
|
257
|
+
await deps.reload();
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if (err && err.message === 'redirecting to auth') return;
|
|
260
|
+
deps.toast(refusalText(err && err.data, err && err.status));
|
|
261
|
+
btn.disabled = false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
root.addEventListener('click', (e) => {
|
|
266
|
+
const open = e.target.closest('[data-uat-open]');
|
|
267
|
+
if (open) { e.preventDefault(); openPanel(open); return; }
|
|
268
|
+
const flag = e.target.closest('[data-uat-backend]');
|
|
269
|
+
if (flag) { e.preventDefault(); toggleBackend(flag); }
|
|
270
|
+
});
|
|
271
|
+
root.addEventListener('submit', (e) => {
|
|
272
|
+
const form = e.target.closest('[data-uat-form]');
|
|
273
|
+
if (form) { e.preventDefault(); submit(form); }
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const api = { STATE_LABELS, stateOf, isAwaiting, badgesHtml, actionHtml, panelHtml, refusalText, checkFile, wire };
|
|
278
|
+
if (typeof window !== 'undefined') window.OTBCriterionUat = api;
|
|
279
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
280
|
+
})();
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
76
76
|
|
|
77
|
-
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
77
|
+
<script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
|
|
78
78
|
<script src="/builders/palette.js" defer></script>
|
|
79
79
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
80
80
|
<script src="/builders/hall-kit.js"></script>
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
|
|
98
98
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
99
99
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
100
|
-
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
100
|
+
<script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
|
|
101
101
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
102
102
|
<script src="/builders/palette.js" defer></script>
|
|
103
103
|
<script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
|
|
99
99
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
100
100
|
|
|
101
|
-
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
101
|
+
<script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
|
|
102
102
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
103
103
|
<script src="/builders/palette.js" defer></script>
|
|
104
104
|
<script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
|
|
55
55
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
56
56
|
|
|
57
|
-
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
57
|
+
<script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
|
|
58
58
|
<script src="/builders/palette.js" defer></script>
|
|
59
59
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
60
60
|
<script src="/builders/hall-kit.js"></script>
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
<div id="gate-modal-root"></div>
|
|
77
77
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
78
78
|
|
|
79
|
-
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
79
|
+
<script src="/builders/shell.js?v=2026-09-30-founding-commands"></script>
|
|
80
80
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
81
81
|
<script src="/builders/palette.js" defer></script>
|
|
82
82
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
@@ -126,11 +126,13 @@
|
|
|
126
126
|
</footer>
|
|
127
127
|
|
|
128
128
|
<script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
|
|
129
|
-
|
|
129
|
+
<!-- task 1004402: the UAT state words the rings share with the goal page. -->
|
|
130
|
+
<script src="/builders/criterion-uat-ui.js?v=2026-09-30-uat1"></script>
|
|
131
|
+
<script src="/builders/sky-data.js?v=2026-09-30-uat1"></script>
|
|
130
132
|
<script src="/builders/sky-layout.js?v=2026-09-27-split"></script>
|
|
131
133
|
<script src="/builders/sky-draw.js?v=2026-09-27-split"></script>
|
|
132
|
-
<script src="/builders/sky-interaction.js?v=2026-09-
|
|
133
|
-
<script src="/builders/sky-panel.js?v=2026-09-
|
|
134
|
+
<script src="/builders/sky-interaction.js?v=2026-09-30-uat1"></script>
|
|
135
|
+
<script src="/builders/sky-panel.js?v=2026-09-30-uat1"></script>
|
|
134
136
|
<script src="/builders/sky-composer.js?v=2026-09-27-split"></script>
|
|
135
137
|
<script src="/builders/sky-tour.js?v=2026-09-27-split"></script>
|
|
136
138
|
</body>
|
|
@@ -450,7 +450,7 @@
|
|
|
450
450
|
// is passed straight through; workTabs is a GETTER because it is reassigned
|
|
451
451
|
// after boot and a renderer must see the current controller, not the null it
|
|
452
452
|
// held here at startup. This call sits AFTER every declaration it passes.
|
|
453
|
-
const { GOAL_SORTS, WORK_SORT_HEADERS, bulk, errCode, errDetails, friendlyClaimError, goalSel, goalSelTotal, loadDetail, loadDirectory, loadGoalTasks, pagers, reflectSortHeaders, renderDetail, renderGoalBulkToolbar, renderList, renderTodoCards, renderWork } = window.OTBGoalsRender({
|
|
453
|
+
const { GOAL_SORTS, WORK_SORT_HEADERS, bulk, canConfirmCriteria, errCode, errDetails, friendlyClaimError, goalSel, goalSelTotal, loadDetail, loadDirectory, loadGoalTasks, pagers, reflectSortHeaders, renderDetail, renderGoalBulkToolbar, renderList, renderTodoCards, renderWork } = window.OTBGoalsRender({
|
|
454
454
|
API, claimAction, copyText, curLabel, getJSON, patchJSON, postJSON, sameId, selfId, state,
|
|
455
455
|
get workTabs() { return workTabs; },
|
|
456
456
|
});
|
|
@@ -550,44 +550,26 @@
|
|
|
550
550
|
// (withDetail=false) instead reveals detail via a per-criterion <details> expand
|
|
551
551
|
// (BV1.R79) so the full text is one click away, not hover-only.
|
|
552
552
|
|
|
553
|
-
// task
|
|
554
|
-
// (POST /
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (btn) { btn.disabled = true; btn.textContent = 'Confirming…'; }
|
|
563
|
-
try {
|
|
564
|
-
const res = await postJSON(`${API}/done-when/${encodeURIComponent(critId)}/satisfy`, {});
|
|
565
|
-
toast(res && res.goal_achieved ? 'Criterion confirmed — that was the last one, so this goal is achieved.' : 'Criterion confirmed.');
|
|
566
|
-
if (state.current != null) {
|
|
567
|
-
await loadDetail(state.current, { force: true });
|
|
568
|
-
renderDetail(state.current);
|
|
569
|
-
}
|
|
570
|
-
} catch (err) {
|
|
571
|
-
if (err && err.message === 'redirecting to auth') return;
|
|
572
|
-
const code = err && err.data && err.data.error;
|
|
573
|
-
// POST /done-when/:id/satisfy gates on criterion.satisfy since BV1.R105, so the
|
|
574
|
-
// denial code is now 'permission_forbidden'; 'rank_forbidden' is kept for an
|
|
575
|
-
// instance still on a pre-R105 core (the doorway is versioned, ADR 0083).
|
|
576
|
-
const denial = code && (code.code || code);
|
|
577
|
-
// task 1004392 (ADR 0351): a criterion closes on a UAT now, and a bare
|
|
578
|
-
// confirm is refused as an override with no reason. Say that, rather than
|
|
579
|
-
// "could not confirm (400)"; the UAT controls themselves are task 1004402.
|
|
580
|
-
const reason = denial === 'override_reason_required'
|
|
581
|
-
? 'Criteria now close on a UAT: someone who did not ship the work tests it on the live site and signs it off with /goal-uat.'
|
|
582
|
-
: (denial === 'permission_forbidden' || denial === 'rank_forbidden')
|
|
583
|
-
? 'Confirming a criterion needs Metic rank or higher.'
|
|
584
|
-
: `Could not confirm that criterion (${(err && err.status) || 'error'}).`;
|
|
585
|
-
toast(reason);
|
|
586
|
-
if (btn) { btn.disabled = false; btn.textContent = label; }
|
|
587
|
-
} finally {
|
|
588
|
-
pendingConfirms.delete(critId);
|
|
589
|
-
}
|
|
553
|
+
// task 1004402 (ADR 0351) — the UAT on a criterion, from the hall. The old
|
|
554
|
+
// Confirm button (a bare POST /satisfy) is gone: a criterion closes on a UAT
|
|
555
|
+
// sign-off now, and /satisfy is the recorded override. criterion-uat-ui.js
|
|
556
|
+
// owns the panel, the upload and the sign-off; this page hands it its I/O.
|
|
557
|
+
async function putJSON(path, body) {
|
|
558
|
+
const res = await api.request('PUT', path, { body: body || {} });
|
|
559
|
+
const data = res.data || {};
|
|
560
|
+
if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = data; throw e; }
|
|
561
|
+
return data;
|
|
590
562
|
}
|
|
563
|
+
const uatDeps = {
|
|
564
|
+
apiBase: API, getJSON, postJSON, putJSON, toast,
|
|
565
|
+
// The one Metic+ gate the criteria rows already use (goals-render.js).
|
|
566
|
+
canReview: () => canConfirmCriteria(),
|
|
567
|
+
reload: async () => {
|
|
568
|
+
if (state.current == null) return;
|
|
569
|
+
await loadDetail(state.current, { force: true });
|
|
570
|
+
renderDetail(state.current);
|
|
571
|
+
},
|
|
572
|
+
};
|
|
591
573
|
|
|
592
574
|
/* ----- Members (band column + #/goal/N/members full view) ----- */
|
|
593
575
|
|
|
@@ -1082,13 +1064,10 @@
|
|
|
1082
1064
|
});
|
|
1083
1065
|
// task 1002425 — the goal's bulk-action toolbar.
|
|
1084
1066
|
bulk.wire();
|
|
1085
|
-
// task
|
|
1086
|
-
//
|
|
1067
|
+
// task 1004402 — the UAT controls, in both the capped band column and the
|
|
1068
|
+
// dedicated #/goal/N/completion view.
|
|
1087
1069
|
['#goal-completion-body', '#goal-completion-full'].forEach((sel) => {
|
|
1088
|
-
$(sel)
|
|
1089
|
-
const btn = e.target.closest('[data-confirm-crit]');
|
|
1090
|
-
if (btn) { e.preventDefault(); confirmCriterion(btn.dataset.confirmCrit, btn); }
|
|
1091
|
-
});
|
|
1070
|
+
if (window.OTBCriterionUat) window.OTBCriterionUat.wire($(sel), uatDeps);
|
|
1092
1071
|
});
|
|
1093
1072
|
// task 1953 — Approve/Deny on the goal's own pending-requests panel.
|
|
1094
1073
|
$('#goal-pending')?.addEventListener('click', (e) => {
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
readinessOf, EMPTY_ROLLUP, goalRollLine, matchesTodoSearch,
|
|
31
31
|
msOf, taskActivityMs, subtitleAttr, subtitleLine, applyGoalProse } = window.OTBGoalsLib;
|
|
32
32
|
const forestRender = window.OTBGoalForestRender(ctx);
|
|
33
|
+
// task 1004402 — criterion-uat-ui.js. If that script failed to load, the page
|
|
34
|
+
// still draws its criteria (state by satisfied/open, no UAT controls).
|
|
35
|
+
const uatUi = window.OTBCriterionUat || { isAwaiting: () => false, badgesHtml: () => '', actionHtml: () => '' };
|
|
33
36
|
|
|
34
37
|
const CONFLICT_CAP = 3;
|
|
35
38
|
// task 1828 — the band's Completion / Members / Conflicts columns each cap
|
|
@@ -673,7 +676,10 @@
|
|
|
673
676
|
</div>`;
|
|
674
677
|
}
|
|
675
678
|
function completionSummaryHtml(g, crit, sat, ringSize, criterionlessTaskCount, openTasks) {
|
|
679
|
+
// task 1004402: "met and ready to confirm" became Awaiting UAT (ADR 0351). A
|
|
680
|
+
// server too old to send uat_state still gets the old sentence.
|
|
676
681
|
const pending = crit.filter(isPending).length;
|
|
682
|
+
const awaiting = crit.filter(uatUi.isAwaiting).length;
|
|
677
683
|
// task 1002164 — an open goal used to punt confirmation to the /goal-review
|
|
678
684
|
// CLI skill here; confirming now happens on this page, so the line reports
|
|
679
685
|
// what is actually actionable instead of naming a command.
|
|
@@ -682,11 +688,14 @@
|
|
|
682
688
|
// repeating the criterion sentence. Satisfied criteria plus an open goal is
|
|
683
689
|
// precisely the state that reads as a bug when the page will not explain it.
|
|
684
690
|
const held = openTasks && openTasks.total ? openTasks.total : 0;
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
:
|
|
688
|
-
|
|
689
|
-
:
|
|
691
|
+
const uatMode = crit.some((c) => c.uat_state);
|
|
692
|
+
const openLine = awaiting
|
|
693
|
+
? `${fmtNum(awaiting)} criteri${awaiting === 1 ? 'on is' : 'a are'} awaiting UAT: the work has shipped, and someone who did not build it now tries it on the live site.`
|
|
694
|
+
: (pending && !uatMode ? `${fmtNum(pending)} criteri${pending === 1 ? 'on is' : 'a are'} met and ready to confirm.`
|
|
695
|
+
: (held
|
|
696
|
+
? `Open — every criterion can be satisfied and the goal still will not close while work is unfinished.`
|
|
697
|
+
: (uatMode ? 'Open — a criterion is ready for its UAT once every task linked to it has shipped.'
|
|
698
|
+
: 'Open — a criterion becomes confirmable once every task linked to it has shipped.')));
|
|
690
699
|
const line = g.status === 'achieved' ? 'Goal achieved — every criterion confirmed.'
|
|
691
700
|
: (g.status === 'archived' ? 'Archived.' : openLine);
|
|
692
701
|
const unlinked = crit.length > 0 && criterionlessTaskCount > 0
|
|
@@ -706,18 +715,18 @@
|
|
|
706
715
|
if (!list.length) return emptyStateHtml('No done-when criteria yet.');
|
|
707
716
|
return `<ul class="crit-list${withDetail ? ' crit-list--full' : ''}">${list.map((c) => {
|
|
708
717
|
const parts = window.OTB.splitCriterion ? window.OTB.splitCriterion(c.criterion_md) : { label: c.criterion_md, detail: '' };
|
|
718
|
+
// task 1004402 (ADR 0351): the state pill, the tag and the UAT action are
|
|
719
|
+
// criterion-uat-ui.js's, and the badges sit WITH the label (inside the
|
|
720
|
+
// summary, never after it, where a closed <details> would hide them).
|
|
721
|
+
const badges = uatUi.badgesHtml(c);
|
|
722
|
+
const label = escapeHtml(parts.label || '') + (badges ? ` <span class="crit__badges">${badges}</span>` : '');
|
|
709
723
|
const body = withDetail
|
|
710
|
-
? `<div class="crit__col"><span class="crit__text">${
|
|
724
|
+
? `<div class="crit__col"><span class="crit__text">${label}</span>${parts.detail ? `<span class="crit__detail">${escapeHtml(parts.detail)}</span>` : ''}</div>`
|
|
711
725
|
: (parts.detail
|
|
712
|
-
? `<details class="crit-d"><summary class="crit__text">${
|
|
713
|
-
: `<span class="crit__text">${
|
|
726
|
+
? `<details class="crit-d"><summary class="crit__text">${label}</summary><div class="crit__detail">${escapeHtml(parts.detail)}</div></details>`
|
|
727
|
+
: `<span class="crit__text">${label}</span>`);
|
|
714
728
|
const pending = isPending(c);
|
|
715
|
-
|
|
716
|
-
const action = !pending ? ''
|
|
717
|
-
: (canConfirm
|
|
718
|
-
? `<button type="button" class="crit__confirm" data-confirm-crit="${escapeHtml(String(c.id))}"
|
|
719
|
-
title="Every task linked to this criterion has shipped — confirm it as satisfied">Confirm</button>`
|
|
720
|
-
: '<span class="crit__flag" title="Every task linked to this criterion has shipped — a Metic can confirm it">Ready</span>');
|
|
729
|
+
const action = uatUi.actionHtml(c, { canReview: canConfirm, full: withDetail });
|
|
721
730
|
const cls = c.satisfied ? ' crit--done' : (pending ? ' crit--pending' : '');
|
|
722
731
|
return `<li class="crit${cls}"><span class="crit__mark">${c.satisfied ? '✓' : (pending ? '!' : '')}</span>${body}<span class="crit__act">${action}</span></li>`;
|
|
723
732
|
}).join('')}</ul>`;
|
|
@@ -1476,6 +1485,6 @@
|
|
|
1476
1485
|
return d;
|
|
1477
1486
|
}
|
|
1478
1487
|
|
|
1479
|
-
return { GOAL_SORTS, WORK_SORT_HEADERS, errCode, errDetails, friendlyClaimError, bulk, goalSel, goalSelTotal, loadDependencyForest, loadDetail, loadDirectory, loadGoalTasks, pagers, reflectSortHeaders, renderDependencyForest, renderDetail, renderGoalBulkToolbar, renderList, renderTodoCards, renderWork };
|
|
1488
|
+
return { GOAL_SORTS, WORK_SORT_HEADERS, canConfirmCriteria, errCode, errDetails, friendlyClaimError, bulk, goalSel, goalSelTotal, loadDependencyForest, loadDetail, loadDirectory, loadGoalTasks, pagers, reflectSortHeaders, renderDependencyForest, renderDetail, renderGoalBulkToolbar, renderList, renderTodoCards, renderWork };
|
|
1480
1489
|
};
|
|
1481
1490
|
})();
|