@bongos/core 1.19.280 → 1.19.281
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 +25 -15
- package/docs/copy-inventory.md +91 -90
- package/docs/copy-registry.json +114 -105
- package/modules/hall-ui/public/goals-lib.js +99 -0
- package/modules/hall-ui/public/goals-page.js +67 -72
- package/modules/hall-ui/public/goals.html +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +3 -1
- package/tests/goal_criterion_confirm_ui.mjs +4 -0
- package/tests/goals_lib.mjs +70 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// goals-lib.js — the Goals page's PURE presentational helpers, exposed as
|
|
2
|
+
// window.OTBGoalsLib. Carved out of the oversized goals-page.js (task 1003135;
|
|
3
|
+
// the fitness excess-lines ratchet). Everything here is a pure function over
|
|
4
|
+
// its arguments (or a constant) — no `state`, no DOM reads — so it can move
|
|
5
|
+
// without dragging the page's IIFE state along. Loaded by goals.html BEFORE
|
|
6
|
+
// goals-page.js (the task-lib.js / hall-kit.js pattern, tasks 1002463 / 2171).
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// Settled task statuses (paid or awaiting land) vs terminal "deliberately
|
|
11
|
+
// cut" states — neither work-to-do nor done. CUT is excluded from the todo
|
|
12
|
+
// list and the conflict scan the same way migration 017 keeps them out of
|
|
13
|
+
// version_progress ("abandoned is a terminal state (deliberately cut), not
|
|
14
|
+
// pending work"). Without this, an abandoned task shows up under a goal's
|
|
15
|
+
// "Work to do" as if it were still queued.
|
|
16
|
+
const DONE_STATUSES = new Set(['completed', 'confirmed', 'shipped']);
|
|
17
|
+
const CUT_STATUSES = new Set(['abandoned', 'dropped']);
|
|
18
|
+
// Genuinely-open work under a goal: not done, not cut → {backlog, ready,
|
|
19
|
+
// active, blocked}.
|
|
20
|
+
const isTodoStatus = (s) => !DONE_STATUSES.has(s) && !CUT_STATUSES.has(s);
|
|
21
|
+
|
|
22
|
+
// The criteria progress ring (satisfied/total), green once achieved.
|
|
23
|
+
function ring(satisfied, total, achieved, size = 42) {
|
|
24
|
+
const pct = total > 0 ? satisfied / total : (achieved ? 1 : 0);
|
|
25
|
+
const r = 16;
|
|
26
|
+
const circ = 2 * Math.PI * r;
|
|
27
|
+
const dash = (pct * circ).toFixed(1);
|
|
28
|
+
const gap = (circ - pct * circ).toFixed(1);
|
|
29
|
+
const stroke = achieved ? 'var(--ok)' : 'var(--accent)';
|
|
30
|
+
return `<svg class="goal-ring" width="${size}" height="${size}" viewBox="0 0 42 42" role="img" aria-label="${satisfied} of ${total} criteria satisfied">
|
|
31
|
+
<circle cx="21" cy="21" r="${r}" fill="none" stroke="var(--rule-soft)" stroke-width="4"></circle>
|
|
32
|
+
<circle cx="21" cy="21" r="${r}" fill="none" stroke="${stroke}" stroke-width="4" stroke-linecap="round" stroke-dasharray="${dash} ${gap}" transform="rotate(-90 21 21)"></circle>
|
|
33
|
+
<text x="21" y="25" text-anchor="middle" class="goal-ring__label">${satisfied}/${total}</text>
|
|
34
|
+
</svg>`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function memberName(m) {
|
|
38
|
+
return m.display_name || (m.github_login ? `@${m.github_login}` : `builder #${m.builder_id}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---- task-readiness rollup (hall redesign task 1002439) -------------------
|
|
42
|
+
// Dependencies are a TASK property; a goal is only "blocked" in the derived
|
|
43
|
+
// sense that all its actionable tasks wait on upstream tasks (the Tasks page,
|
|
44
|
+
// not this one, owns the actual task→task graph). So the Goals page rolls each
|
|
45
|
+
// goal's tasks into these buckets:
|
|
46
|
+
// done — settled (shipped/confirmed/completed)
|
|
47
|
+
// active — a live claim ("in progress")
|
|
48
|
+
// ready — claimable now (deps met + promoted)
|
|
49
|
+
// blocked — backlog with ≥1 unshipped dependency
|
|
50
|
+
// backlog — backlog, no unmet deps (just not started/promoted)
|
|
51
|
+
function readinessOf(t) {
|
|
52
|
+
const s = t.status;
|
|
53
|
+
if (DONE_STATUSES.has(s)) return 'done';
|
|
54
|
+
if (CUT_STATUSES.has(s)) return 'cut';
|
|
55
|
+
if (s === 'active') return 'active';
|
|
56
|
+
if (s === 'ready') return 'ready';
|
|
57
|
+
if (s === 'backlog' && (t.blocked_by || []).some((d) => !d.satisfied)) return 'blocked';
|
|
58
|
+
return 'backlog';
|
|
59
|
+
}
|
|
60
|
+
const EMPTY_ROLLUP = { done: 0, active: 0, ready: 0, blocked: 0, backlog: 0, cut: 0, total: 0, nextReady: null };
|
|
61
|
+
|
|
62
|
+
// task 1002439 — a goal row's readiness rollup line (only the non-zero buckets).
|
|
63
|
+
const ROLL_DOTS = [
|
|
64
|
+
['ready', 'ready', 'var(--ok)'],
|
|
65
|
+
['active', 'in progress', 'var(--info)'],
|
|
66
|
+
['blocked', 'blocked', 'var(--terracotta)'],
|
|
67
|
+
['backlog', 'backlog', 'var(--ink-faint)'],
|
|
68
|
+
];
|
|
69
|
+
function goalRollLine(r) {
|
|
70
|
+
const parts = ROLL_DOTS.filter(([k]) => r[k] > 0)
|
|
71
|
+
.map(([k, label, c]) => `<span class="gr-d"><i style="background:${c}"></i><b>${r[k]}</b> ${label}</span>`);
|
|
72
|
+
return parts.length ? `<div class="gr-roll">${parts.join('')}</div>` : '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Free-text search over a goal's todo list (work.js:261-274, minus the
|
|
76
|
+
// goal-title match — a single goal makes that redundant). '#N', 'N' and a
|
|
77
|
+
// bare number all match the id.
|
|
78
|
+
function matchesTodoSearch(t, q) {
|
|
79
|
+
if (!q) return true;
|
|
80
|
+
const needle = q.replace(/^#/, '');
|
|
81
|
+
if (!needle) return true;
|
|
82
|
+
if (String(t.id).includes(needle)) return true;
|
|
83
|
+
if ((t.title || '').toLowerCase().includes(needle)) return true;
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ms helpers for the goal ordering (task 1002965): updated_at moves on every
|
|
88
|
+
// task write, shipped_at folded in because a ship path can stamp it without
|
|
89
|
+
// touching updated_at.
|
|
90
|
+
const msOf = (iso) => { const t = iso ? Date.parse(iso) : NaN; return Number.isNaN(t) ? 0 : t; };
|
|
91
|
+
const taskActivityMs = (t) => Math.max(msOf(t.updated_at), msOf(t.shipped_at));
|
|
92
|
+
|
|
93
|
+
window.OTBGoalsLib = {
|
|
94
|
+
DONE_STATUSES, CUT_STATUSES, isTodoStatus,
|
|
95
|
+
ring, memberName,
|
|
96
|
+
readinessOf, EMPTY_ROLLUP, goalRollLine,
|
|
97
|
+
matchesTodoSearch, msOf, taskActivityMs,
|
|
98
|
+
};
|
|
99
|
+
})();
|
|
@@ -35,13 +35,18 @@
|
|
|
35
35
|
// empty/skeleton states — adopted here by task 2171.
|
|
36
36
|
const { makePager, paginate, wirePager, pagerBarHtml, makeTabs, makeFilterBar,
|
|
37
37
|
emptyStateHtml, skeletonRows } = window.OTBKit;
|
|
38
|
+
// This page's pure presentational helpers (goals-lib.js, task 1003135 — carved
|
|
39
|
+
// out of this oversized file; the fitness excess-lines ratchet). Destructured
|
|
40
|
+
// so every call site below reads exactly as before.
|
|
41
|
+
const { DONE_STATUSES, CUT_STATUSES, isTodoStatus, ring, memberName,
|
|
42
|
+
readinessOf, EMPTY_ROLLUP, goalRollLine, matchesTodoSearch,
|
|
43
|
+
msOf, taskActivityMs } = window.OTBGoalsLib;
|
|
38
44
|
const API = '/api/bongos';
|
|
39
45
|
// Generated typed client (task 1996 / R09), served as window.BongosClient by
|
|
40
46
|
// /lib/bongos-client.js. baseUrl:'' passes the full same-origin paths this file
|
|
41
47
|
// builds through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
|
|
42
48
|
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
43
49
|
|
|
44
|
-
const DONE_STATUSES = new Set(['completed', 'confirmed', 'shipped']);
|
|
45
50
|
// task 1888 — the Work tables and the conflicts list render capped; the pager
|
|
46
51
|
// under each expands to the full set.
|
|
47
52
|
const CONFLICT_CAP = 3;
|
|
@@ -59,15 +64,6 @@
|
|
|
59
64
|
todo: makePager('goal-todo', 'otb.goals.todoPerPage'),
|
|
60
65
|
done: makePager('goal-done', 'otb.goals.donePerPage'),
|
|
61
66
|
};
|
|
62
|
-
// Terminal "deliberately cut" states — neither work-to-do nor done. Excluded
|
|
63
|
-
// from the todo list and the conflict scan the same way migration 017 keeps
|
|
64
|
-
// them out of version_progress ("abandoned is a terminal state (deliberately
|
|
65
|
-
// cut), not pending work"). Without this, an abandoned task shows up under a
|
|
66
|
-
// goal's "Work to do" as if it were still queued.
|
|
67
|
-
const CUT_STATUSES = new Set(['abandoned', 'dropped']);
|
|
68
|
-
// Genuinely-open work under a goal: not done, not cut → {backlog, ready,
|
|
69
|
-
// active, blocked}.
|
|
70
|
-
const isTodoStatus = (s) => !DONE_STATUSES.has(s) && !CUT_STATUSES.has(s);
|
|
71
67
|
// Record deep-links (#/task|idea|blocker/N) are owned by the shared overlay
|
|
72
68
|
// (task-detail.js). This page must NOT treat them as a route change (Bug 2).
|
|
73
69
|
const DEEPLINK_RE = /^#\/(?:task|idea|blocker)\/\d+$/;
|
|
@@ -148,44 +144,7 @@
|
|
|
148
144
|
return `<span class="goal-badge ${cls}">${escapeHtml(status)}</span>`;
|
|
149
145
|
}
|
|
150
146
|
|
|
151
|
-
|
|
152
|
-
const pct = total > 0 ? satisfied / total : (achieved ? 1 : 0);
|
|
153
|
-
const r = 16;
|
|
154
|
-
const circ = 2 * Math.PI * r;
|
|
155
|
-
const dash = (pct * circ).toFixed(1);
|
|
156
|
-
const gap = (circ - pct * circ).toFixed(1);
|
|
157
|
-
const stroke = achieved ? 'var(--ok)' : 'var(--accent)';
|
|
158
|
-
return `<svg class="goal-ring" width="${size}" height="${size}" viewBox="0 0 42 42" role="img" aria-label="${satisfied} of ${total} criteria satisfied">
|
|
159
|
-
<circle cx="21" cy="21" r="${r}" fill="none" stroke="var(--rule-soft)" stroke-width="4"></circle>
|
|
160
|
-
<circle cx="21" cy="21" r="${r}" fill="none" stroke="${stroke}" stroke-width="4" stroke-linecap="round" stroke-dasharray="${dash} ${gap}" transform="rotate(-90 21 21)"></circle>
|
|
161
|
-
<text x="21" y="25" text-anchor="middle" class="goal-ring__label">${satisfied}/${total}</text>
|
|
162
|
-
</svg>`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function memberName(m) {
|
|
166
|
-
return m.display_name || (m.github_login ? `@${m.github_login}` : `builder #${m.builder_id}`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ---- task-readiness rollup (hall redesign task 1002439) --------------------
|
|
170
|
-
// Dependencies are a TASK property; a goal is only "blocked" in the derived
|
|
171
|
-
// sense that all its actionable tasks wait on upstream tasks (the Tasks page,
|
|
172
|
-
// not this one, owns the actual task→task graph). So the Goals page rolls each
|
|
173
|
-
// goal's tasks into these buckets:
|
|
174
|
-
// done — settled (shipped/confirmed/completed)
|
|
175
|
-
// active — a live claim ("in progress")
|
|
176
|
-
// ready — claimable now (deps met + promoted)
|
|
177
|
-
// blocked — backlog with ≥1 unshipped dependency
|
|
178
|
-
// backlog — backlog, no unmet deps (just not started/promoted)
|
|
179
|
-
function readinessOf(t) {
|
|
180
|
-
const s = t.status;
|
|
181
|
-
if (DONE_STATUSES.has(s)) return 'done';
|
|
182
|
-
if (CUT_STATUSES.has(s)) return 'cut';
|
|
183
|
-
if (s === 'active') return 'active';
|
|
184
|
-
if (s === 'ready') return 'ready';
|
|
185
|
-
if (s === 'backlog' && (t.blocked_by || []).some((d) => !d.satisfied)) return 'blocked';
|
|
186
|
-
return 'backlog';
|
|
187
|
-
}
|
|
188
|
-
const EMPTY_ROLLUP = { done: 0, active: 0, ready: 0, blocked: 0, backlog: 0, cut: 0, total: 0, nextReady: null };
|
|
147
|
+
// ring / memberName / readinessOf / EMPTY_ROLLUP → goals-lib.js (task 1003135).
|
|
189
148
|
const rollupOf = (goalId) => state.rollups.get(String(goalId)) || EMPTY_ROLLUP;
|
|
190
149
|
|
|
191
150
|
// ---- goal ordering (task 1002965) -----------------------------------------
|
|
@@ -197,8 +156,6 @@
|
|
|
197
156
|
// falling back to the goal row's own updated_at when the goal
|
|
198
157
|
// has no tasks at all, so a task-less goal still orders sanely
|
|
199
158
|
// instead of sinking to the bottom forever.
|
|
200
|
-
const msOf = (iso) => { const t = iso ? Date.parse(iso) : NaN; return Number.isNaN(t) ? 0 : t; };
|
|
201
|
-
const taskActivityMs = (t) => Math.max(msOf(t.updated_at), msOf(t.shipped_at));
|
|
202
159
|
const goalCreatedMs = (g) => msOf(g.created_at);
|
|
203
160
|
const goalActivityMs = (g) => state.activity.get(String(g.id)) || msOf(g.updated_at);
|
|
204
161
|
|
|
@@ -271,6 +228,40 @@
|
|
|
271
228
|
}
|
|
272
229
|
}
|
|
273
230
|
|
|
231
|
+
// task 1003135 — the Owner dropdown. 'Goal owner' is goals.created_by (the
|
|
232
|
+
// fixed authority, migration 160), returned by GET /goals as a raw builder id;
|
|
233
|
+
// labels come from the directory the page already lazily caches (loadDirectory,
|
|
234
|
+
// task 1877), same label idiom as the invite dropdown. created_by is NULLABLE
|
|
235
|
+
// (system-attributed — e.g. the migration-160 '<version> — general' catch-alls),
|
|
236
|
+
// hence the explicit 'system / unattributed' option. Mirrors
|
|
237
|
+
// populateVersionFilter above; fired after boot's fetch resolves (async because
|
|
238
|
+
// of the directory fetch — fire-and-forget, the select fills when ready).
|
|
239
|
+
async function populateOwnerFilter() {
|
|
240
|
+
const sel = $('#f-goal-owner');
|
|
241
|
+
if (!sel) return;
|
|
242
|
+
const ownerIds = Array.from(new Set(
|
|
243
|
+
state.goals.filter((g) => g.created_by != null).map((g) => String(g.created_by))));
|
|
244
|
+
const hasSystem = state.goals.some((g) => g.created_by == null);
|
|
245
|
+
const dir = ownerIds.length ? await loadDirectory() : [];
|
|
246
|
+
const byId = new Map((dir || []).map((b) => [String(b.id), b]));
|
|
247
|
+
const label = (id) => {
|
|
248
|
+
const b = byId.get(id);
|
|
249
|
+
return b ? (b.display_name || '@' + b.github_login) : `builder #${id}`;
|
|
250
|
+
};
|
|
251
|
+
for (const id of ownerIds.sort((a, b) => label(a).localeCompare(label(b)))) {
|
|
252
|
+
const opt = document.createElement('option');
|
|
253
|
+
opt.value = id;
|
|
254
|
+
opt.textContent = label(id);
|
|
255
|
+
sel.appendChild(opt);
|
|
256
|
+
}
|
|
257
|
+
if (hasSystem) {
|
|
258
|
+
const opt = document.createElement('option');
|
|
259
|
+
opt.value = 'none';
|
|
260
|
+
opt.textContent = 'system / unattributed';
|
|
261
|
+
sel.appendChild(opt);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
274
265
|
// task 1760 — priority is a WITHIN-version ordering, so the drag affordance
|
|
275
266
|
// only makes sense once the grid is narrowed to exactly one version (the
|
|
276
267
|
// "any" filter mixes versions and dragging across them would be meaningless).
|
|
@@ -292,6 +283,18 @@
|
|
|
292
283
|
const ver = $('#f-goal-version')?.value || '';
|
|
293
284
|
const status = $('#f-goal-status')?.value || '';
|
|
294
285
|
const mineOnly = $('#f-goal-mine')?.value === 'mine';
|
|
286
|
+
// task 1003135 — free-text search (goal title OR id; '#N', 'N' and a bare
|
|
287
|
+
// number all match — the id is already rendered on the row, task 1002163)
|
|
288
|
+
// and the Owner dropdown ('none' = created_by NULL, system-attributed).
|
|
289
|
+
// Reads the DOM ids directly, matching this function's existing style.
|
|
290
|
+
const q = ($('#f-goals-search')?.value || '').trim().toLowerCase();
|
|
291
|
+
const needle = q.replace(/^#/, '');
|
|
292
|
+
const owner = $('#f-goal-owner')?.value || '';
|
|
293
|
+
const matchesSearch = (g) => !needle
|
|
294
|
+
|| String(g.id).includes(needle)
|
|
295
|
+
|| (g.title || '').toLowerCase().includes(needle);
|
|
296
|
+
const matchesOwner = (g) => !owner
|
|
297
|
+
|| (owner === 'none' ? g.created_by == null : String(g.created_by) === owner);
|
|
295
298
|
const myId = selfId();
|
|
296
299
|
const iAmMember = (g) => {
|
|
297
300
|
const d = state.details.get(String(g.id));
|
|
@@ -300,22 +303,11 @@
|
|
|
300
303
|
return state.goals.filter((g) =>
|
|
301
304
|
(!ver || g.version_id === ver) &&
|
|
302
305
|
(!status || g.status === status) &&
|
|
306
|
+
matchesSearch(g) &&
|
|
307
|
+
matchesOwner(g) &&
|
|
303
308
|
(!mineOnly || iAmMember(g)));
|
|
304
309
|
}
|
|
305
310
|
|
|
306
|
-
// task 1002439 — a goal row's readiness rollup line (only the non-zero buckets).
|
|
307
|
-
const ROLL_DOTS = [
|
|
308
|
-
['ready', 'ready', 'var(--ok)'],
|
|
309
|
-
['active', 'in progress', 'var(--info)'],
|
|
310
|
-
['blocked', 'blocked', 'var(--terracotta)'],
|
|
311
|
-
['backlog', 'backlog', 'var(--ink-faint)'],
|
|
312
|
-
];
|
|
313
|
-
function goalRollLine(r) {
|
|
314
|
-
const parts = ROLL_DOTS.filter(([k]) => r[k] > 0)
|
|
315
|
-
.map(([k, label, c]) => `<span class="gr-d"><i style="background:${c}"></i><b>${r[k]}</b> ${label}</span>`);
|
|
316
|
-
return parts.length ? `<div class="gr-roll">${parts.join('')}</div>` : '';
|
|
317
|
-
}
|
|
318
|
-
|
|
319
311
|
// task 1002439 — one goal, rendered as a rollup row: criteria ring · title +
|
|
320
312
|
// meta + readiness rollup · one next action (claim the next ready task, drill
|
|
321
313
|
// into what's blocking, or a review/achieved note).
|
|
@@ -515,15 +507,6 @@
|
|
|
515
507
|
const el = $('#f-goal-search');
|
|
516
508
|
return el ? el.value.trim().toLowerCase() : '';
|
|
517
509
|
}
|
|
518
|
-
function matchesTodoSearch(t, q) {
|
|
519
|
-
if (!q) return true;
|
|
520
|
-
const needle = q.replace(/^#/, '');
|
|
521
|
-
if (!needle) return true;
|
|
522
|
-
if (String(t.id).includes(needle)) return true;
|
|
523
|
-
if ((t.title || '').toLowerCase().includes(needle)) return true;
|
|
524
|
-
return false;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
510
|
// Copy-prompt (work.js:454-503). deHashRefs stops a bare #NNN autolinking to
|
|
528
511
|
// GitHub in a pasted session; the prompt points the builder at the hall task URL.
|
|
529
512
|
async function copyPromptForTask(taskId, btn) {
|
|
@@ -1810,6 +1793,9 @@
|
|
|
1810
1793
|
const gres = await getJSON(`${API}/goals`);
|
|
1811
1794
|
state.goals = gres.goals || [];
|
|
1812
1795
|
populateVersionFilter();
|
|
1796
|
+
// task 1003135 — fire-and-forget: the Owner options fill when the (cached)
|
|
1797
|
+
// directory resolves; boot never waits on a label lookup.
|
|
1798
|
+
populateOwnerFilter().catch(() => {});
|
|
1813
1799
|
// Prefetch details (ring + members on the cards). Sequentially-parallel,
|
|
1814
1800
|
// failure-tolerant: a card without detail still renders.
|
|
1815
1801
|
// task 1002439 — also fold in per-goal task-readiness rollups (one request
|
|
@@ -1848,12 +1834,21 @@
|
|
|
1848
1834
|
// taken by the goal-detail To-do bar further down this file).
|
|
1849
1835
|
let savedGoalSort = null;
|
|
1850
1836
|
try { savedGoalSort = window.localStorage.getItem(GOAL_SORT_PREF); } catch (_) { /* private mode */ }
|
|
1837
|
+
// task 1003135 — Search declared on the kit spec (live 'input' listener comes
|
|
1838
|
+
// free, hall-kit.js:273; no debounce needed, the match is pure client-side over
|
|
1839
|
+
// the already-loaded state.goals). Id is f-goals-search — f-goal-search is
|
|
1840
|
+
// already taken by the goal-detail To-do bar further down this file. Owner
|
|
1841
|
+
// options are filled after boot's fetch (populateOwnerFilter), the
|
|
1842
|
+
// populateVersionFilter pattern. No URL riding (url defaults off here and this
|
|
1843
|
+
// page is deliberately url:false — it owns the #/goal/N hash route).
|
|
1851
1844
|
makeFilterBar({
|
|
1852
1845
|
mount: $('#goals-filters'),
|
|
1846
|
+
search: { id: 'f-goals-search', label: 'Search', placeholder: 'goal # or name…' },
|
|
1853
1847
|
primary: [
|
|
1854
1848
|
{ id: 'f-goal-version', label: 'Version', options: [{ value: '', label: '— any —' }] },
|
|
1855
1849
|
{ id: 'f-goal-status', label: 'Status', options: [{ value: '', label: '— any —' }, 'open', 'achieved', 'archived'] },
|
|
1856
1850
|
{ id: 'f-goal-mine', label: 'Membership', options: [{ value: '', label: 'All goals' }, { value: 'mine', label: 'My goals' }] },
|
|
1851
|
+
{ id: 'f-goal-owner', label: 'Owner', options: [{ value: '', label: '— any —' }] },
|
|
1857
1852
|
{ id: 'f-goal-order',
|
|
1858
1853
|
label: 'Sort',
|
|
1859
1854
|
value: GOAL_SORTS[savedGoalSort] ? savedGoalSort : undefined,
|
|
@@ -198,6 +198,7 @@
|
|
|
198
198
|
<script src="/builders/task-lib.js"></script>
|
|
199
199
|
<script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
|
|
200
200
|
<script src="/builders/brand-apply.js?v=2026-06-27-r75"></script>
|
|
201
|
-
<script src="/builders/goals-
|
|
201
|
+
<script src="/builders/goals-lib.js?v=2026-08-21-goalslib1"></script>
|
|
202
|
+
<script src="/builders/goals-page.js?v=2026-08-21-goals6"></script>
|
|
202
203
|
</body>
|
|
203
204
|
</html>
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.281",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.281",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.281",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -987,8 +987,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
987
987
|
// landed since 1.19.278 with no explicit bump. run 32521664476. (task 1002620)
|
|
988
988
|
// 1.19.280 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
989
989
|
// landed since 1.19.279 with no explicit bump. run 32523185119. (task 1002620)
|
|
990
|
+
// 1.19.281 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
991
|
+
// landed since 1.19.280 with no explicit bump. run 32525071763. (task 1002620)
|
|
990
992
|
// ---------------------------------------------------------------------------
|
|
991
|
-
const CORE_VERSION = '1.19.
|
|
993
|
+
const CORE_VERSION = '1.19.281'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
992
994
|
|
|
993
995
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
994
996
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -37,6 +37,9 @@ const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-page.j
|
|
|
37
37
|
// the REAL file in the same context rather than stubbing it — stubbing would let
|
|
38
38
|
// the extraction rot without this, the only test that executes either board.
|
|
39
39
|
const TASK_LIB_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/task-lib.js'), 'utf8');
|
|
40
|
+
// task 1003135: the page's pure presentational helpers moved to goals-lib.js
|
|
41
|
+
// (window.OTBGoalsLib) — run the REAL file too, same reasoning as task-lib above.
|
|
42
|
+
const GOALS_LIB_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-lib.js'), 'utf8');
|
|
40
43
|
|
|
41
44
|
const { test, summary } = makeRunner();
|
|
42
45
|
|
|
@@ -132,6 +135,7 @@ async function render({ rank = 'metic', goalStatus = 'open', criteria = Object.v
|
|
|
132
135
|
sandbox.globalThis = sandbox;
|
|
133
136
|
vm.createContext(sandbox);
|
|
134
137
|
vm.runInContext(TASK_LIB_SRC, sandbox, { filename: 'task-lib.js' });
|
|
138
|
+
vm.runInContext(GOALS_LIB_SRC, sandbox, { filename: 'goals-lib.js' });
|
|
135
139
|
vm.runInContext(SRC, sandbox, { filename: 'goals-page.js' });
|
|
136
140
|
|
|
137
141
|
// Let boot()'s promise chain settle (me → goals → details → route → render).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// tests/goals_lib.mjs — the Goals page's extracted pure helpers (goals-lib.js,
|
|
2
|
+
// task 1003135). Pins (a) that the lib loads standalone in a bare window — it
|
|
3
|
+
// must not silently grow a dependency on the page's IIFE state, which is the
|
|
4
|
+
// whole reason it could move out of the oversized goals-page.js — and (b) the
|
|
5
|
+
// behaviour of the helpers the page destructures.
|
|
6
|
+
//
|
|
7
|
+
// Run: node tests/goals_lib.mjs
|
|
8
|
+
|
|
9
|
+
import { strict as assert } from 'node:assert';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import vm from 'node:vm';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { makeRunner } from './helpers.mjs';
|
|
15
|
+
|
|
16
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-lib.js'), 'utf8');
|
|
18
|
+
const { test, summary } = makeRunner();
|
|
19
|
+
|
|
20
|
+
const windowObj = {};
|
|
21
|
+
const sandbox = { window: windowObj, Set, Map, Math, Number, String, Array, Object, Date, JSON };
|
|
22
|
+
sandbox.globalThis = sandbox;
|
|
23
|
+
vm.createContext(sandbox);
|
|
24
|
+
vm.runInContext(SRC, sandbox, { filename: 'goals-lib.js' });
|
|
25
|
+
const lib = windowObj.OTBGoalsLib;
|
|
26
|
+
|
|
27
|
+
await test('loads standalone in a bare window (no page-state dependency) and exports the full surface', () => {
|
|
28
|
+
assert.ok(lib, 'window.OTBGoalsLib must be set');
|
|
29
|
+
for (const k of ['DONE_STATUSES', 'CUT_STATUSES', 'isTodoStatus', 'ring', 'memberName',
|
|
30
|
+
'readinessOf', 'EMPTY_ROLLUP', 'goalRollLine', 'matchesTodoSearch', 'msOf', 'taskActivityMs']) {
|
|
31
|
+
assert.ok(k in lib, `missing export: ${k}`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await test('readinessOf buckets match the task-1002439 rules', () => {
|
|
36
|
+
assert.equal(lib.readinessOf({ status: 'shipped' }), 'done');
|
|
37
|
+
assert.equal(lib.readinessOf({ status: 'abandoned' }), 'cut');
|
|
38
|
+
assert.equal(lib.readinessOf({ status: 'active' }), 'active');
|
|
39
|
+
assert.equal(lib.readinessOf({ status: 'ready' }), 'ready');
|
|
40
|
+
assert.equal(lib.readinessOf({ status: 'backlog', blocked_by: [{ satisfied: false }] }), 'blocked');
|
|
41
|
+
assert.equal(lib.readinessOf({ status: 'backlog' }), 'backlog');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await test('matchesTodoSearch: bare number, #N and title fragments all match (task 1003135 search semantics)', () => {
|
|
45
|
+
const t = { id: 1002163, title: 'Show the goal id on the row' };
|
|
46
|
+
assert.ok(lib.matchesTodoSearch(t, ''));
|
|
47
|
+
assert.ok(lib.matchesTodoSearch(t, '1002163'));
|
|
48
|
+
assert.ok(lib.matchesTodoSearch(t, '#1002163'));
|
|
49
|
+
assert.ok(lib.matchesTodoSearch(t, '2163'), 'substring id match');
|
|
50
|
+
assert.ok(lib.matchesTodoSearch(t, 'goal id'));
|
|
51
|
+
assert.ok(!lib.matchesTodoSearch(t, 'zzz'));
|
|
52
|
+
assert.ok(lib.matchesTodoSearch(t, '#'), 'a lone # narrows nothing');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await test('goalRollLine renders only non-zero buckets; ring is well-formed SVG', () => {
|
|
56
|
+
const line = lib.goalRollLine({ ready: 2, active: 0, blocked: 1, backlog: 0 });
|
|
57
|
+
assert.match(line, /<b>2<\/b> ready/);
|
|
58
|
+
assert.match(line, /<b>1<\/b> blocked/);
|
|
59
|
+
assert.ok(!line.includes('in progress'), 'zero buckets are omitted');
|
|
60
|
+
assert.equal(lib.goalRollLine({ ready: 0, active: 0, blocked: 0, backlog: 0 }), '');
|
|
61
|
+
assert.match(lib.ring(2, 5, false), /aria-label="2 of 5 criteria satisfied"/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await test('memberName label idiom: display name, then @login, then builder #id', () => {
|
|
65
|
+
assert.equal(lib.memberName({ display_name: 'A', github_login: 'a', builder_id: 1 }), 'A');
|
|
66
|
+
assert.equal(lib.memberName({ github_login: 'a', builder_id: 1 }), '@a');
|
|
67
|
+
assert.equal(lib.memberName({ builder_id: 1 }), 'builder #1');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
summary();
|