@bongos/core 1.19.280 → 1.19.282
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 +31 -16
- package/docs/copy-inventory.md +89 -89
- package/docs/copy-registry.json +109 -109
- package/modules/hall-ui/public/goals-lib.js +99 -0
- package/modules/hall-ui/public/goals-page.js +137 -141
- package/modules/hall-ui/public/goals.css +12 -15
- 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 +5 -1
- package/tests/goal_criterion_confirm_ui.mjs +4 -0
- package/tests/goals_card_view.mjs +61 -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
|
|
|
@@ -211,6 +168,8 @@
|
|
|
211
168
|
active: { label: 'recently active', cmp: (a, b) => goalActivityMs(b) - goalActivityMs(a), stamp: (g) => ({ label: 'last work', ms: goalActivityMs(g) }) },
|
|
212
169
|
};
|
|
213
170
|
const GOAL_SORT_PREF = 'otb.goals.sort';
|
|
171
|
+
// task 1003136 — the list|cards view choice, persisted like the sort above.
|
|
172
|
+
const GOAL_VIEW_PREF = 'otb.goals.view';
|
|
214
173
|
const currentGoalSort = () => GOAL_SORTS[$('#f-goal-order')?.value] || GOAL_SORTS.priority;
|
|
215
174
|
|
|
216
175
|
// Stable: filteredGoals already returns the server's priority order, and
|
|
@@ -271,11 +230,44 @@
|
|
|
271
230
|
}
|
|
272
231
|
}
|
|
273
232
|
|
|
274
|
-
// task
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
233
|
+
// task 1003135 — the Owner dropdown. 'Goal owner' is goals.created_by (the
|
|
234
|
+
// fixed authority, migration 160), returned by GET /goals as a raw builder id;
|
|
235
|
+
// labels come from the directory the page already lazily caches (loadDirectory,
|
|
236
|
+
// task 1877), same label idiom as the invite dropdown. created_by is NULLABLE
|
|
237
|
+
// (system-attributed — e.g. the migration-160 '<version> — general' catch-alls),
|
|
238
|
+
// hence the explicit 'system / unattributed' option. Mirrors
|
|
239
|
+
// populateVersionFilter above; fired after boot's fetch resolves (async because
|
|
240
|
+
// of the directory fetch — fire-and-forget, the select fills when ready).
|
|
241
|
+
async function populateOwnerFilter() {
|
|
242
|
+
const sel = $('#f-goal-owner');
|
|
243
|
+
if (!sel) return;
|
|
244
|
+
const ownerIds = Array.from(new Set(
|
|
245
|
+
state.goals.filter((g) => g.created_by != null).map((g) => String(g.created_by))));
|
|
246
|
+
const hasSystem = state.goals.some((g) => g.created_by == null);
|
|
247
|
+
const dir = ownerIds.length ? await loadDirectory() : [];
|
|
248
|
+
const byId = new Map((dir || []).map((b) => [String(b.id), b]));
|
|
249
|
+
const label = (id) => {
|
|
250
|
+
const b = byId.get(id);
|
|
251
|
+
return b ? (b.display_name || '@' + b.github_login) : `builder #${id}`;
|
|
252
|
+
};
|
|
253
|
+
for (const id of ownerIds.sort((a, b) => label(a).localeCompare(label(b)))) {
|
|
254
|
+
const opt = document.createElement('option');
|
|
255
|
+
opt.value = id;
|
|
256
|
+
opt.textContent = label(id);
|
|
257
|
+
sel.appendChild(opt);
|
|
258
|
+
}
|
|
259
|
+
if (hasSystem) {
|
|
260
|
+
const opt = document.createElement('option');
|
|
261
|
+
opt.value = 'none';
|
|
262
|
+
opt.textContent = 'system / unattributed';
|
|
263
|
+
sel.appendChild(opt);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Archon check for the detail view's manage/visibility affordances (the
|
|
268
|
+
// server-side rank gates are the real enforcement). The task-1760 drag-reorder
|
|
269
|
+
// affordance this once also served is retired — see the note where
|
|
270
|
+
// wireDragReorder used to live (task 1003136).
|
|
279
271
|
const isArchon = () => (state.me?.builder?.rank || '').toLowerCase() === 'archon';
|
|
280
272
|
|
|
281
273
|
// task 1002164 — confirming a criterion is Metic+ (POST /done-when/:id/satisfy,
|
|
@@ -292,6 +284,18 @@
|
|
|
292
284
|
const ver = $('#f-goal-version')?.value || '';
|
|
293
285
|
const status = $('#f-goal-status')?.value || '';
|
|
294
286
|
const mineOnly = $('#f-goal-mine')?.value === 'mine';
|
|
287
|
+
// task 1003135 — free-text search (goal title OR id; '#N', 'N' and a bare
|
|
288
|
+
// number all match — the id is already rendered on the row, task 1002163)
|
|
289
|
+
// and the Owner dropdown ('none' = created_by NULL, system-attributed).
|
|
290
|
+
// Reads the DOM ids directly, matching this function's existing style.
|
|
291
|
+
const q = ($('#f-goals-search')?.value || '').trim().toLowerCase();
|
|
292
|
+
const needle = q.replace(/^#/, '');
|
|
293
|
+
const owner = $('#f-goal-owner')?.value || '';
|
|
294
|
+
const matchesSearch = (g) => !needle
|
|
295
|
+
|| String(g.id).includes(needle)
|
|
296
|
+
|| (g.title || '').toLowerCase().includes(needle);
|
|
297
|
+
const matchesOwner = (g) => !owner
|
|
298
|
+
|| (owner === 'none' ? g.created_by == null : String(g.created_by) === owner);
|
|
295
299
|
const myId = selfId();
|
|
296
300
|
const iAmMember = (g) => {
|
|
297
301
|
const d = state.details.get(String(g.id));
|
|
@@ -300,22 +304,11 @@
|
|
|
300
304
|
return state.goals.filter((g) =>
|
|
301
305
|
(!ver || g.version_id === ver) &&
|
|
302
306
|
(!status || g.status === status) &&
|
|
307
|
+
matchesSearch(g) &&
|
|
308
|
+
matchesOwner(g) &&
|
|
303
309
|
(!mineOnly || iAmMember(g)));
|
|
304
310
|
}
|
|
305
311
|
|
|
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
312
|
// task 1002439 — one goal, rendered as a rollup row: criteria ring · title +
|
|
320
313
|
// meta + readiness rollup · one next action (claim the next ready task, drill
|
|
321
314
|
// into what's blocking, or a review/achieved note).
|
|
@@ -329,13 +322,16 @@
|
|
|
329
322
|
return ` · ${escapeHtml(label)} ${escapeHtml(ms ? fmtDate(new Date(ms).toISOString()) : '—')}`;
|
|
330
323
|
}
|
|
331
324
|
|
|
332
|
-
|
|
325
|
+
// The facts a goal row/card renders, computed ONCE so the two views (task
|
|
326
|
+
// 1003136) can't drift: ring, meta line, roll line, and the single
|
|
327
|
+
// next-action ladder (achieved → criteria-met → claim-next → drill → quiet).
|
|
328
|
+
function goalRenderFacts(g, ringSize) {
|
|
333
329
|
const d = state.details.get(String(g.id));
|
|
334
330
|
const crit = d ? (d.criteria || []) : [];
|
|
335
331
|
const sat = crit.filter((c) => c.satisfied).length;
|
|
336
332
|
const r = rollupOf(g.id);
|
|
337
333
|
const achieved = g.status === 'achieved';
|
|
338
|
-
const ringHtml = crit.length ? ring(sat, crit.length, achieved,
|
|
334
|
+
const ringHtml = crit.length ? ring(sat, crit.length, achieved, ringSize)
|
|
339
335
|
: `<span class="gr-crit-none" aria-hidden="true">—</span>`;
|
|
340
336
|
const goalHref = `#/goal/${escapeHtml(String(g.id))}`;
|
|
341
337
|
let right;
|
|
@@ -351,6 +347,12 @@
|
|
|
351
347
|
} else {
|
|
352
348
|
right = `<span class="gr-quiet">No tasks yet</span>`;
|
|
353
349
|
}
|
|
350
|
+
const metaHtml = `#${escapeHtml(String(g.id))} · ${escapeHtml(g.version_id || '')}${(achieved || g.status === 'open') ? '' : ' · ' + escapeHtml(g.status)}${goalStampHtml(g)}`;
|
|
351
|
+
return { r, achieved, ringHtml, goalHref, right, metaHtml };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function goalRowHtml(g) {
|
|
355
|
+
const { r, achieved, ringHtml, goalHref, right, metaHtml } = goalRenderFacts(g, 44);
|
|
354
356
|
// NOTE: a rollup row nests real links (title → goal, Claim → task overlay,
|
|
355
357
|
// drill → goal), so the row itself is a <div>, not an <a> — nested anchors
|
|
356
358
|
// are invalid. Only the explicit links navigate.
|
|
@@ -358,13 +360,31 @@
|
|
|
358
360
|
<div class="gr-ringcol">${ringHtml}</div>
|
|
359
361
|
<div class="gr-main">
|
|
360
362
|
<a class="gr-title" href="${goalHref}">${escapeHtml(g.title || 'Untitled goal')}</a>
|
|
361
|
-
<div class="gr-meta"
|
|
363
|
+
<div class="gr-meta">${metaHtml}</div>
|
|
362
364
|
${achieved ? '' : goalRollLine(r)}
|
|
363
365
|
</div>
|
|
364
366
|
<div class="gr-right">${right}</div>
|
|
365
367
|
</div>`;
|
|
366
368
|
}
|
|
367
369
|
|
|
370
|
+
// task 1003136 — the same goal as a CARD: identical facts as the row via
|
|
371
|
+
// goalRenderFacts, so nothing is lost switching views. Reuses the surviving
|
|
372
|
+
// .goal-card* CSS (goals.css — task 1002439 neutralised it, never deleted it).
|
|
373
|
+
// Deliberately emits NO draggable attribute: drag-to-reorder is retired, see
|
|
374
|
+
// the note where wireDragReorder used to live.
|
|
375
|
+
function goalCardHtml(g) {
|
|
376
|
+
const { r, achieved, ringHtml, goalHref, right, metaHtml } = goalRenderFacts(g, 36);
|
|
377
|
+
return `<div class="goal-card" data-goal-id="${escapeHtml(String(g.id))}">
|
|
378
|
+
<div class="goal-card__head">
|
|
379
|
+
<div class="gr-ringcol">${ringHtml}</div>
|
|
380
|
+
<a class="goal-card__title gr-title" href="${goalHref}">${escapeHtml(g.title || 'Untitled goal')}</a>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="goal-card__meta">${metaHtml}</div>
|
|
383
|
+
${achieved ? '' : goalRollLine(r)}
|
|
384
|
+
<div class="goal-card__foot">${right}</div>
|
|
385
|
+
</div>`;
|
|
386
|
+
}
|
|
387
|
+
|
|
368
388
|
// task 1002439 — the Goals page, reorganised around momentum: a task-readiness
|
|
369
389
|
// summary, then goals grouped Active (has ready/in-progress work) · Blocked
|
|
370
390
|
// (only waiting-on-upstream tasks) · Achieved. "Blocked" here is a DERIVED
|
|
@@ -407,7 +427,15 @@
|
|
|
407
427
|
</div>`;
|
|
408
428
|
|
|
409
429
|
const sec = (label, n) => `<div class="gr-sec"><b>${label}</b><span class="gr-sec__n">${n}</span></div>`;
|
|
410
|
-
|
|
430
|
+
// task 1003136 — the list|cards toggle swaps how the open groups render;
|
|
431
|
+
// grouping, filters, and sort run above the fork so both views share them.
|
|
432
|
+
// Achieved/archived pills stay pills (already the compact form). The inner
|
|
433
|
+
// div is class-only .goal-grid on purpose: task 1002439's block override
|
|
434
|
+
// targets #goal-grid.goal-grid, so the original card-grid CSS applies here.
|
|
435
|
+
const cardsView = ($('#f-goal-view')?.value || 'list') === 'cards';
|
|
436
|
+
const listOf = cardsView
|
|
437
|
+
? (arr) => `<div class="goal-grid">${arr.map(goalCardHtml).join('')}</div>`
|
|
438
|
+
: (arr) => `<div class="gr-list">${arr.map(goalRowHtml).join('')}</div>`;
|
|
411
439
|
// task 1002965 — an achieved/archived goal is a compact pill, so its sort
|
|
412
440
|
// date rides as its own muted span (the row's `gr-meta` equivalent). The
|
|
413
441
|
// pills row already wraps, so the extra text can't widen the page.
|
|
@@ -426,63 +454,14 @@
|
|
|
426
454
|
+ (archived.length ? sec('Archived', archived.length) + `<div class="gr-pills gr-pills--muted">${archived.map(pill).join('')}</div>` : '');
|
|
427
455
|
}
|
|
428
456
|
|
|
429
|
-
// ---- drag-to-reorder (task
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (!el || !el.hasAttribute('draggable')) return;
|
|
438
|
-
dragEl = el;
|
|
439
|
-
el.classList.add('goal-card--dragging');
|
|
440
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
441
|
-
});
|
|
442
|
-
grid.addEventListener('dragover', (e) => {
|
|
443
|
-
if (!dragEl) return;
|
|
444
|
-
const over = e.target.closest('.goal-card');
|
|
445
|
-
if (!over || over === dragEl || !grid.contains(over)) return;
|
|
446
|
-
e.preventDefault();
|
|
447
|
-
const rect = over.getBoundingClientRect();
|
|
448
|
-
const after = (e.clientX - rect.left) > rect.width / 2;
|
|
449
|
-
grid.insertBefore(dragEl, after ? over.nextSibling : over);
|
|
450
|
-
});
|
|
451
|
-
grid.addEventListener('drop', (e) => { if (dragEl) e.preventDefault(); });
|
|
452
|
-
grid.addEventListener('dragend', async () => {
|
|
453
|
-
if (!dragEl) return;
|
|
454
|
-
const el = dragEl;
|
|
455
|
-
el.classList.remove('goal-card--dragging');
|
|
456
|
-
dragEl = null;
|
|
457
|
-
const ver = $('#f-goal-version')?.value || '';
|
|
458
|
-
if (!ver) return;
|
|
459
|
-
// task 1932 — with pagination the grid shows only the current PAGE, so the
|
|
460
|
-
// reorder must submit the FULL version order: the goals before this page +
|
|
461
|
-
// the page's cards in their new DOM order + the goals after it. Without
|
|
462
|
-
// this, POSTing only the visible page's ids would corrupt the priorities
|
|
463
|
-
// of the goals on other pages. Priority numbers reflect the global
|
|
464
|
-
// position (start + i + 1).
|
|
465
|
-
const filtered = filteredGoals();
|
|
466
|
-
const start = (pagers.list.page - 1) * pagers.list.perPage;
|
|
467
|
-
const cards = [...grid.querySelectorAll('.goal-card')];
|
|
468
|
-
cards.forEach((card, i) => {
|
|
469
|
-
const num = card.querySelector('.goal-card__priority');
|
|
470
|
-
if (num) num.textContent = String(start + i + 1);
|
|
471
|
-
});
|
|
472
|
-
const pageIds = cards.map((card) => Number(card.dataset.goalId));
|
|
473
|
-
const beforeIds = filtered.slice(0, start).map((g) => Number(g.id));
|
|
474
|
-
const afterIds = filtered.slice(start + pageIds.length).map((g) => Number(g.id));
|
|
475
|
-
const orderedIds = [...beforeIds, ...pageIds, ...afterIds];
|
|
476
|
-
try {
|
|
477
|
-
await postJSON(`${API}/goals/reorder`, { version_id: ver, ordered_ids: orderedIds });
|
|
478
|
-
toast('Priority order saved.');
|
|
479
|
-
} catch (err) {
|
|
480
|
-
toast((err && err.data && err.data.message) || 'Could not save the new order — reloading.');
|
|
481
|
-
state.goals = (await getJSON(`${API}/goals`).catch(() => ({ goals: state.goals }))).goals || state.goals;
|
|
482
|
-
renderList();
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
}
|
|
457
|
+
// ---- drag-to-reorder: RETIRED (task 1003136; was task 1760) ---------------
|
|
458
|
+
// wireDragReorder() lived here, dead since task 1002439's grouped rollup (its
|
|
459
|
+
// .goal-card selector matched nothing). The card view revives .goal-card
|
|
460
|
+
// markup, so the decision is now DELIBERATE: the listener is DELETED — both
|
|
461
|
+
// views render grouped, sorted, filtered sets, so DOM order no longer
|
|
462
|
+
// expresses the version priority the old dragend POSTed to /goals/reorder;
|
|
463
|
+
// re-enabling it would submit a corrupted order. Cards emit no draggable
|
|
464
|
+
// attribute. POST /goals/reorder itself is untouched (Archon, pinned).
|
|
486
465
|
|
|
487
466
|
/* ---------------- detail view ---------------- */
|
|
488
467
|
|
|
@@ -515,15 +494,6 @@
|
|
|
515
494
|
const el = $('#f-goal-search');
|
|
516
495
|
return el ? el.value.trim().toLowerCase() : '';
|
|
517
496
|
}
|
|
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
497
|
// Copy-prompt (work.js:454-503). deHashRefs stops a bare #NNN autolinking to
|
|
528
498
|
// GitHub in a pasted session; the prompt points the builder at the hall task URL.
|
|
529
499
|
async function copyPromptForTask(taskId, btn) {
|
|
@@ -1810,6 +1780,9 @@
|
|
|
1810
1780
|
const gres = await getJSON(`${API}/goals`);
|
|
1811
1781
|
state.goals = gres.goals || [];
|
|
1812
1782
|
populateVersionFilter();
|
|
1783
|
+
// task 1003135 — fire-and-forget: the Owner options fill when the (cached)
|
|
1784
|
+
// directory resolves; boot never waits on a label lookup.
|
|
1785
|
+
populateOwnerFilter().catch(() => {});
|
|
1813
1786
|
// Prefetch details (ring + members on the cards). Sequentially-parallel,
|
|
1814
1787
|
// failure-tolerant: a card without detail still renders.
|
|
1815
1788
|
// task 1002439 — also fold in per-goal task-readiness rollups (one request
|
|
@@ -1836,7 +1809,12 @@
|
|
|
1836
1809
|
// reads it, so keeping the pref off the URL leaves deep links shareable and
|
|
1837
1810
|
// unchanged. Private-mode writes throw — swallowed, the pref is a nicety.
|
|
1838
1811
|
const onListFilterChange = () => {
|
|
1839
|
-
try {
|
|
1812
|
+
try {
|
|
1813
|
+
window.localStorage.setItem(GOAL_SORT_PREF, $('#f-goal-order')?.value || 'priority');
|
|
1814
|
+
// task 1003136 — the list|cards view choice survives reload, same
|
|
1815
|
+
// precedent (localStorage, never the URL).
|
|
1816
|
+
window.localStorage.setItem(GOAL_VIEW_PREF, $('#f-goal-view')?.value || 'list');
|
|
1817
|
+
} catch (_) { /* private mode */ }
|
|
1840
1818
|
pagers.list.page = 1;
|
|
1841
1819
|
renderList();
|
|
1842
1820
|
};
|
|
@@ -1847,17 +1825,36 @@
|
|
|
1847
1825
|
// task 1002965 adds Sort (f-goal-order — NOT f-goal-sort, which is already
|
|
1848
1826
|
// taken by the goal-detail To-do bar further down this file).
|
|
1849
1827
|
let savedGoalSort = null;
|
|
1850
|
-
|
|
1828
|
+
let savedGoalView = null;
|
|
1829
|
+
try {
|
|
1830
|
+
savedGoalSort = window.localStorage.getItem(GOAL_SORT_PREF);
|
|
1831
|
+
savedGoalView = window.localStorage.getItem(GOAL_VIEW_PREF);
|
|
1832
|
+
} catch (_) { /* private mode */ }
|
|
1833
|
+
// task 1003135 — Search declared on the kit spec (live 'input' listener comes
|
|
1834
|
+
// free, hall-kit.js:273; no debounce needed, the match is pure client-side over
|
|
1835
|
+
// the already-loaded state.goals). Id is f-goals-search — f-goal-search is
|
|
1836
|
+
// already taken by the goal-detail To-do bar further down this file. Owner
|
|
1837
|
+
// options are filled after boot's fetch (populateOwnerFilter), the
|
|
1838
|
+
// populateVersionFilter pattern. No URL riding (url defaults off here and this
|
|
1839
|
+
// page is deliberately url:false — it owns the #/goal/N hash route).
|
|
1851
1840
|
makeFilterBar({
|
|
1852
1841
|
mount: $('#goals-filters'),
|
|
1842
|
+
search: { id: 'f-goals-search', label: 'Search', placeholder: 'goal # or name…' },
|
|
1853
1843
|
primary: [
|
|
1854
1844
|
{ id: 'f-goal-version', label: 'Version', options: [{ value: '', label: '— any —' }] },
|
|
1855
1845
|
{ id: 'f-goal-status', label: 'Status', options: [{ value: '', label: '— any —' }, 'open', 'achieved', 'archived'] },
|
|
1856
1846
|
{ id: 'f-goal-mine', label: 'Membership', options: [{ value: '', label: 'All goals' }, { value: 'mine', label: 'My goals' }] },
|
|
1847
|
+
{ id: 'f-goal-owner', label: 'Owner', options: [{ value: '', label: '— any —' }] },
|
|
1857
1848
|
{ id: 'f-goal-order',
|
|
1858
1849
|
label: 'Sort',
|
|
1859
1850
|
value: GOAL_SORTS[savedGoalSort] ? savedGoalSort : undefined,
|
|
1860
1851
|
options: Object.entries(GOAL_SORTS).map(([value, { label }]) => ({ value, label })) },
|
|
1852
|
+
// task 1003136 — list|cards toggle; renderList forks its group renderer
|
|
1853
|
+
// on this. Persisted via GOAL_VIEW_PREF, never the URL.
|
|
1854
|
+
{ id: 'f-goal-view',
|
|
1855
|
+
label: 'View',
|
|
1856
|
+
value: savedGoalView === 'cards' ? 'cards' : undefined,
|
|
1857
|
+
options: [{ value: 'list', label: 'list' }, { value: 'cards', label: 'cards' }] },
|
|
1861
1858
|
],
|
|
1862
1859
|
onChange: onListFilterChange,
|
|
1863
1860
|
});
|
|
@@ -1986,7 +1983,6 @@
|
|
|
1986
1983
|
if (denyBtn) { respondToJoinRequest(state.current, denyBtn.dataset.denyReq, 'deny', denyBtn); return; }
|
|
1987
1984
|
});
|
|
1988
1985
|
window.addEventListener('hashchange', route);
|
|
1989
|
-
wireDragReorder();
|
|
1990
1986
|
|
|
1991
1987
|
boot();
|
|
1992
1988
|
})();
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
are in style.css's hall-kit section. Everything derives from the shared
|
|
7
7
|
design tokens so instance packs reskin this page along with the rest. */
|
|
8
8
|
|
|
9
|
-
/* ---- goal-card grid (
|
|
9
|
+
/* ---- goal-card grid (the CARDS view) ----
|
|
10
10
|
task 2171 density pass: minmax 300→230 (four across at desktop width),
|
|
11
|
-
tighter gaps/padding/type
|
|
12
|
-
|
|
11
|
+
tighter gaps/padding/type. Retired by task 1002439's grouped rollup, REVIVED
|
|
12
|
+
by task 1003136 as the list|cards toggle's card view: renderList nests
|
|
13
|
+
id-less .goal-grid wrappers per group, which this rule styles — the
|
|
14
|
+
#goal-grid override further down only flattens the OUTER container. */
|
|
13
15
|
.goal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }
|
|
14
16
|
.goal-card {
|
|
15
17
|
display: flex; flex-direction: column; gap: 7px;
|
|
@@ -19,16 +21,9 @@
|
|
|
19
21
|
}
|
|
20
22
|
a.goal-card:hover { text-decoration: none; border-color: var(--border-strong); background: var(--surface-2); }
|
|
21
23
|
.goal-card__head { display: flex; align-items: flex-start; gap: 9px; }
|
|
22
|
-
/* task 1760
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.goal-card__handle:active { cursor: grabbing; }
|
|
26
|
-
.goal-card__priority {
|
|
27
|
-
flex: none; min-width: 1.4em; text-align: center;
|
|
28
|
-
font-size: 11.5px; font-weight: 600; color: var(--ink-faint);
|
|
29
|
-
font-variant-numeric: tabular-nums;
|
|
30
|
-
}
|
|
31
|
-
.goal-card--dragging { opacity: 0.45; }
|
|
24
|
+
/* task 1003136: the task-1760 drag handle/priority/dragging rules that lived
|
|
25
|
+
here are deleted with wireDragReorder (see goals-page.js) — the revived
|
|
26
|
+
cards deliberately carry no drag affordance. */
|
|
32
27
|
/* task 1761: a goal blocked by an unachieved prerequisite is dimmed + hue-shifted. */
|
|
33
28
|
.goal-card--blocked { opacity: 0.62; border-style: dashed; }
|
|
34
29
|
.goal-card--blocked:hover { opacity: 0.85; }
|
|
@@ -209,8 +204,10 @@ a.goal-card:hover { text-decoration: none; border-color: var(--border-strong); b
|
|
|
209
204
|
tasks up into ready/in-progress/blocked; the task→task dependency
|
|
210
205
|
graph itself lives on the Tasks page. Uses the shared style.css tokens.
|
|
211
206
|
============================================================ */
|
|
212
|
-
/* The
|
|
213
|
-
|
|
207
|
+
/* The OUTER container renders grouped full-width blocks (summary + section
|
|
208
|
+
heads), so flip it to normal block flow. Id-selected on purpose: the cards
|
|
209
|
+
view (task 1003136) nests id-less .goal-grid wrappers per group, which keep
|
|
210
|
+
the real card grid above. */
|
|
214
211
|
#goal-grid.goal-grid { display: block; gap: 0; }
|
|
215
212
|
.gr-summary { display: flex; flex-wrap: wrap; gap: 22px; margin: 6px 0 20px; }
|
|
216
213
|
.gr-sm { display: flex; align-items: baseline; gap: 8px; font-size: 13px; color: var(--ink-soft); }
|
|
@@ -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-goals7"></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.282",
|
|
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.282",
|
|
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.282",
|
|
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,12 @@ 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)
|
|
992
|
+
// 1.19.282 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
993
|
+
// landed since 1.19.281 with no explicit bump. run 32526392955. (task 1002620)
|
|
990
994
|
// ---------------------------------------------------------------------------
|
|
991
|
-
const CORE_VERSION = '1.19.
|
|
995
|
+
const CORE_VERSION = '1.19.282'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
992
996
|
|
|
993
997
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
994
998
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|