@bongos/core 1.17.2 → 1.18.0
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 +426 -96
- package/.devcontainer/healthcheck.sh +0 -0
- package/.devcontainer/init-firewall.sh +0 -0
- package/.devcontainer/post-create.sh +0 -0
- package/GOVERNANCE.md +1 -1
- package/README.md +26 -22
- package/bin/bongos.js +0 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docker-entrypoint.sh +0 -0
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +94 -0
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +43 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +68 -370
- package/docs/api-reference.md +7 -18
- package/docs/file-map.md +7 -25
- package/docs/onboarding/diagrams/05-versioning.mmd +4 -6
- package/docs/onboarding/diagrams/assertions.json +5 -9
- package/modules/hall-ui/public/builders.js +132 -645
- package/modules/hall-ui/public/goal-inbox.js +67 -26
- package/modules/hall-ui/public/goals-page.js +102 -177
- package/modules/hall-ui/public/goals.css +183 -0
- package/modules/hall-ui/public/goals.html +35 -253
- package/modules/hall-ui/public/goals.js +7 -2
- package/modules/hall-ui/public/hall-kit.js +474 -0
- package/modules/hall-ui/public/hall-widgets.js +24 -20
- package/modules/hall-ui/public/instances-panel.js +24 -3
- package/modules/hall-ui/public/new-project-wizard.js +6 -5
- package/modules/hall-ui/public/settings.css +355 -0
- package/modules/hall-ui/public/settings.html +86 -414
- package/modules/hall-ui/public/settings.js +96 -35
- package/modules/hall-ui/public/style.css +156 -0
- package/modules/hall-ui/public/watch.css +184 -0
- package/modules/hall-ui/public/watch.html +111 -285
- package/modules/hall-ui/public/watch.js +59 -21
- package/modules/hall-ui/public/work.css +194 -0
- package/modules/hall-ui/public/work.html +46 -374
- package/modules/hall-ui/public/work.js +137 -102
- package/modules/lifecycle/criterion-suggest.js +80 -0
- package/modules/lifecycle/db.js +42 -5
- package/modules/lifecycle/routes/goals.js +26 -0
- package/modules/lifecycle/routes/tasks.js +21 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +83 -7
- package/public-landing/index.html +11 -76
- package/scripts/cron/style_guide_audit.sh +0 -0
- package/scripts/gds/api.js +0 -0
- package/scripts/gds/architect-audit.js +0 -0
- package/scripts/gds/audit-authorship.sh +0 -0
- package/scripts/gds/audit-replay.sh +0 -0
- package/scripts/gds/box.js +0 -0
- package/scripts/gds/capture.js +0 -0
- package/scripts/gds/conductor-dispatch.js +0 -0
- package/scripts/gds/coreB-carve-proof.sh +0 -0
- package/scripts/gds/db-backup-nightly.sh +0 -0
- package/scripts/gds/db-backup-offsite.sh +0 -0
- package/scripts/gds/doctor.js +0 -0
- package/scripts/gds/grade-smoke.js +0 -0
- package/scripts/gds/hooks/pre-push.js +0 -0
- package/scripts/gds/install-git-hooks.js +0 -0
- package/scripts/gds/main-audit.js +0 -0
- package/scripts/gds/oauth-secret.js +0 -0
- package/scripts/gds/onboarding-analyze.js +0 -0
- package/scripts/gds/onboarding-ship.js +0 -0
- package/scripts/gds/paste-token.js +0 -0
- package/scripts/gds/portability-smoke-art.sh +0 -0
- package/scripts/gds/portability-smoke-memory.sh +0 -0
- package/scripts/gds/provision.js +125 -4
- package/scripts/gds/push-main.js +0 -0
- package/scripts/gds/render-ideas.js +0 -0
- package/scripts/gds/security-baseline-check.sh +0 -0
- package/scripts/gds/session.js +0 -0
- package/scripts/gds/ship.js +11 -1
- package/scripts/gds/smoke-batch-claim.sh +0 -0
- package/scripts/gds/smoke-batch-release.sh +0 -0
- package/scripts/gds/smoke-box.sh +0 -0
- package/scripts/gds/smoke-claim-consistency.sh +0 -0
- package/scripts/gds/smoke-dependencies.sh +0 -0
- package/scripts/gds/smoke-write.sh +0 -0
- package/scripts/hall-preview/README.md +103 -0
- package/scripts/hall-preview/fixtures/access-requests.json +20 -0
- package/scripts/hall-preview/fixtures/achievements.json +76 -0
- package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
- package/scripts/hall-preview/fixtures/audit-log.json +68 -0
- package/scripts/hall-preview/fixtures/blockers__detail.json +12 -0
- package/scripts/hall-preview/fixtures/box__me.json +5 -0
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +69 -0
- package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
- package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
- package/scripts/hall-preview/fixtures/builders__roster.json +48 -0
- package/scripts/hall-preview/fixtures/goals.json +502 -0
- package/scripts/hall-preview/fixtures/goals__25.json +96 -0
- package/scripts/hall-preview/fixtures/goals__26.json +67 -0
- package/scripts/hall-preview/fixtures/goals__3.json +103 -0
- package/scripts/hall-preview/fixtures/goals__35.json +49 -0
- package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
- package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
- package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
- package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
- package/scripts/hall-preview/fixtures/grades__by-builder.json +32 -0
- package/scripts/hall-preview/fixtures/inbox.json +330 -0
- package/scripts/hall-preview/fixtures/inbox__detail.json +15 -0
- package/scripts/hall-preview/fixtures/instance.json +124 -0
- package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
- package/scripts/hall-preview/fixtures/me.json +476 -0
- package/scripts/hall-preview/fixtures/me__art-key__own.json +6 -0
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +209 -0
- package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
- package/scripts/hall-preview/fixtures/me__render.json +5 -0
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +143 -0
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +67 -0
- package/scripts/hall-preview/fixtures/me__wandering.json +43 -0
- package/scripts/hall-preview/fixtures/override-requests.json +15 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
- package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
- package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
- package/scripts/hall-preview/fixtures/public__grades.json +50 -0
- package/scripts/hall-preview/fixtures/public__progress.json +124 -0
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
- package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
- package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
- package/scripts/hall-preview/fixtures/security__docs.json +10 -0
- package/scripts/hall-preview/fixtures/security__reports.json +31 -0
- package/scripts/hall-preview/fixtures/sessions__search.json +9 -0
- package/scripts/hall-preview/fixtures/tasks.json +2420 -0
- package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
- package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
- package/scripts/hall-preview/fixtures/versions.json +89 -0
- package/scripts/hall-preview/refresh-fixtures.js +77 -0
- package/scripts/hall-preview/server.js +195 -0
- package/scripts/migrate.sh +29 -4
- package/scripts/migrate_cost_ledger.sh +0 -0
- package/scripts/render-diagrams.sh +0 -0
- package/scripts/security/dep-audit.js +0 -0
- package/scripts/setup-builder.js +0 -0
- package/scripts/setup-builder.sh +0 -0
- package/scripts/snapshots/maybe-render-on-deploy.sh +0 -0
- package/src/bongos/optimizer.js +37 -2
- package/src/module-api.js +20 -1
- package/tests/criterion_suggest.mjs +106 -0
- package/tests/migrate_root_resolution.mjs +97 -0
- package/tests/provision.mjs +69 -0
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
// goal-inbox.js — the
|
|
1
|
+
// goal-inbox.js — the Home "Needs your attention" card (task 2169), grown from
|
|
2
|
+
// the two-sided goal-membership inbox (task 1954, ADR 0112).
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
+
// This card is the SINGLE landing place for actionable items on Home: it owns
|
|
5
|
+
// the section (id 'attention-scroll'), renders the goal-membership slice
|
|
6
|
+
// itself, and hosts the idea-triage slice (#attention-ideas, rendered by
|
|
7
|
+
// builders.js). New actionable item classes join as line-item slices here —
|
|
8
|
+
// never as new Home cards (the anti-accretion rule).
|
|
9
|
+
//
|
|
10
|
+
// The goal slice surfaces a builder's private-goals queue:
|
|
4
11
|
// • "Invitations to you" — pending invites addressed to me (Accept / Decline).
|
|
5
12
|
// • "Requests to approve" — pending join-requests on goals I own or manage
|
|
6
13
|
// (Approve / Deny).
|
|
@@ -81,32 +88,41 @@
|
|
|
81
88
|
return `<p class="ginbox__pending card-foot">Your pending join ${noun}: ${links} — awaiting approval.</p>`;
|
|
82
89
|
}
|
|
83
90
|
|
|
91
|
+
// task 2169: this render fills ONLY the goals slice (#attention-goals) — the
|
|
92
|
+
// ideas slice (#attention-ideas) belongs to builders.js's triage renderer.
|
|
93
|
+
// Card visibility + the headline count derive from BOTH via OTBAttention.
|
|
94
|
+
const MAX_GOAL_ROWS = 5;
|
|
84
95
|
function render(inbox) {
|
|
96
|
+
const box = host && host.querySelector('#attention-goals');
|
|
97
|
+
if (!box) return;
|
|
85
98
|
const invitations = inbox.invitations || [];
|
|
86
99
|
const joinRequests = inbox.joinRequests || [];
|
|
87
100
|
const myPending = inbox.myPendingRequests || [];
|
|
88
|
-
// Hide the whole card when there is nothing to see OR act on.
|
|
89
101
|
if (!invitations.length && !joinRequests.length && !myPending.length) {
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
box.innerHTML = '';
|
|
103
|
+
window.OTBAttention.refresh();
|
|
92
104
|
return;
|
|
93
105
|
}
|
|
94
|
-
|
|
95
|
-
let
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
// Cap the card at a handful of rows — it is a summary, not the queue.
|
|
107
|
+
let budget = MAX_GOAL_ROWS;
|
|
108
|
+
const inv = invitations.slice(0, budget); budget -= inv.length;
|
|
109
|
+
const reqs = joinRequests.slice(0, Math.max(0, budget));
|
|
110
|
+
const overflow = (invitations.length - inv.length) + (joinRequests.length - reqs.length);
|
|
111
|
+
let html = '';
|
|
112
|
+
if (inv.length) {
|
|
113
|
+
html += '<h3 class="ginbox__sec-h">Goal invitations to you</h3>';
|
|
114
|
+
html += `<ul class="ginbox">${inv.map(invitationRow).join('')}</ul>`;
|
|
98
115
|
}
|
|
99
|
-
if (
|
|
100
|
-
html += '<h3 class="ginbox__sec-h">
|
|
101
|
-
html += `<ul class="ginbox">${
|
|
116
|
+
if (reqs.length) {
|
|
117
|
+
html += '<h3 class="ginbox__sec-h">Join-requests to approve</h3>';
|
|
118
|
+
html += `<ul class="ginbox">${reqs.map(requestRow).join('')}</ul>`;
|
|
102
119
|
}
|
|
103
|
-
if (
|
|
104
|
-
html +=
|
|
105
|
-
html += `<ul class="ginbox">${joinRequests.map(requestRow).join('')}</ul>`;
|
|
120
|
+
if (overflow > 0) {
|
|
121
|
+
html += `<p class="ginbox__pending">…and ${overflow} more waiting — <a href="/goals">open your goals</a>.</p>`;
|
|
106
122
|
}
|
|
107
123
|
html += pendingLine(myPending);
|
|
108
|
-
|
|
109
|
-
|
|
124
|
+
box.innerHTML = html;
|
|
125
|
+
window.OTBAttention.refresh();
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
async function load() {
|
|
@@ -115,9 +131,11 @@
|
|
|
115
131
|
const inbox = await getJSON(`${API}/goals/inbox`);
|
|
116
132
|
render(inbox);
|
|
117
133
|
} catch (err) {
|
|
118
|
-
// Signed out (401) or an older server without the route (404) → render
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
// Signed out (401) or an older server without the route (404) → render
|
|
135
|
+
// nothing in the goals slice; the ideas slice may still have content.
|
|
136
|
+
const box = host.querySelector('#attention-goals');
|
|
137
|
+
if (box) box.innerHTML = '';
|
|
138
|
+
window.OTBAttention.refresh();
|
|
121
139
|
if (err.code !== 'auth-needed' && err.code !== 'not-found') {
|
|
122
140
|
console.error('[hall] goal inbox load failed', err);
|
|
123
141
|
}
|
|
@@ -174,18 +192,41 @@
|
|
|
174
192
|
}
|
|
175
193
|
}
|
|
176
194
|
|
|
195
|
+
// task 2169: this widget IS the "Needs your attention" card — ONE capped
|
|
196
|
+
// summary hosting every actionable item class: goal invitations +
|
|
197
|
+
// join-requests (this file) and ideas awaiting triage (builders.js renders
|
|
198
|
+
// into #attention-ideas). Future actionable item classes add a slice div
|
|
199
|
+
// here + call OTBAttention.refresh() — never a new Home card.
|
|
177
200
|
function mount(hostEl) {
|
|
178
201
|
host = hostEl;
|
|
179
|
-
host.hidden = true; // stays hidden until
|
|
202
|
+
host.hidden = true; // stays hidden until a slice finds something actionable
|
|
203
|
+
host.innerHTML =
|
|
204
|
+
'<h2 id="attention-h" class="scroll__h">Needs your attention<span class="count-pill" id="attention-count" hidden></span></h2>' +
|
|
205
|
+
'<div id="attention-goals"></div>' +
|
|
206
|
+
'<div id="attention-ideas"></div>';
|
|
180
207
|
host.addEventListener('click', onClick);
|
|
181
208
|
load();
|
|
182
209
|
}
|
|
183
210
|
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
211
|
+
// The card's shared visibility contract: any slice renderer calls refresh()
|
|
212
|
+
// after painting; the card shows iff any slice has actionable rows, and the
|
|
213
|
+
// headline pill is the cross-slice total.
|
|
214
|
+
window.OTBAttention = {
|
|
215
|
+
refresh() {
|
|
216
|
+
const sec = document.getElementById('attention-scroll');
|
|
217
|
+
if (!sec) return;
|
|
218
|
+
const n = sec.querySelectorAll('.ginbox__item, .triage-inbox__item').length;
|
|
219
|
+
const pending = sec.querySelector('.ginbox__pending');
|
|
220
|
+
const pill = document.getElementById('attention-count');
|
|
221
|
+
if (pill) { pill.hidden = !n; pill.textContent = String(n); }
|
|
222
|
+
sec.hidden = n === 0 && !pending;
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// order 20 → the top of the Home main zone: personal + actionable beats
|
|
227
|
+
// informational. hiddenByDefault so a quiet inbox costs nothing.
|
|
187
228
|
window.OTB.contributeHallWidget({
|
|
188
|
-
id: '
|
|
189
|
-
labelledby: '
|
|
229
|
+
id: 'attention-scroll', label: 'Needs your attention', order: 20, mount,
|
|
230
|
+
labelledby: 'attention-h', view: 'home', zone: 'home-main', hiddenByDefault: true,
|
|
190
231
|
});
|
|
191
232
|
})();
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
(function () {
|
|
24
24
|
'use strict';
|
|
25
25
|
const { escapeHtml, $, fmtNum, fmtDate, toast, rankLabel } = window.OTB;
|
|
26
|
+
// Shared component kit (hall-kit.js, task 2166): pager, filter bar, tabs,
|
|
27
|
+
// empty/skeleton states — adopted here by task 2171.
|
|
28
|
+
const { makePager, paginate, wirePager, pagerBarHtml, makeTabs, makeFilterBar,
|
|
29
|
+
emptyStateHtml, skeletonRows } = window.OTBKit;
|
|
26
30
|
const API = '/api/bongos';
|
|
27
31
|
// Generated typed client (task 1996 / R09), served as window.BongosClient by
|
|
28
32
|
// /lib/bongos-client.js. baseUrl:'' passes the full same-origin paths this file
|
|
@@ -38,27 +42,15 @@
|
|
|
38
42
|
// more members, more criteria). Conflicts uses CONFLICT_CAP above.
|
|
39
43
|
const MEMBER_CAP = 5;
|
|
40
44
|
const CRITERIA_CAP = 4;
|
|
41
|
-
// task 1932
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return LIST_PER_PAGE_OPTIONS.includes(v) ? v : 25;
|
|
51
|
-
} catch (_) { return 25; }
|
|
52
|
-
}
|
|
53
|
-
// task 1956 — the goal-detail Work tables (To do / Completed) paginate like the
|
|
54
|
-
// grid; each reads/persists its own per-page key.
|
|
55
|
-
const WORK_PER_PAGE_KEY = { todo: 'otb.goals.todoPerPage', done: 'otb.goals.donePerPage' };
|
|
56
|
-
function loadPerPageFor(key) {
|
|
57
|
-
try {
|
|
58
|
-
const v = Number(window.localStorage.getItem(key));
|
|
59
|
-
return LIST_PER_PAGE_OPTIONS.includes(v) ? v : 25;
|
|
60
|
-
} catch (_) { return 25; }
|
|
61
|
-
}
|
|
45
|
+
// task 1932/1956 → kit (task 2171): the goal-card grid + the goal-detail Work
|
|
46
|
+
// tables (To do / Completed) all paginate through the shared OTBKit pager.
|
|
47
|
+
// localStorage keys are unchanged so builders' per-page prefs survive; the bar
|
|
48
|
+
// markup is stamped into the #…-pager-mount divs at boot via pagerBarHtml.
|
|
49
|
+
const pagers = {
|
|
50
|
+
list: makePager('goals', 'otb.goals.perPage'),
|
|
51
|
+
todo: makePager('goal-todo', 'otb.goals.todoPerPage'),
|
|
52
|
+
done: makePager('goal-done', 'otb.goals.donePerPage'),
|
|
53
|
+
};
|
|
62
54
|
// Terminal "deliberately cut" states — neither work-to-do nor done. Excluded
|
|
63
55
|
// from the todo list and the conflict scan the same way migration 017 keeps
|
|
64
56
|
// them out of version_progress ("abandoned is a terminal state (deliberately
|
|
@@ -79,7 +71,6 @@
|
|
|
79
71
|
view: 'list', // task 1828 — 'list' | 'detail' | 'conflicts'
|
|
80
72
|
inviteOpen: false, // task 1762 — the invite form's open/closed state
|
|
81
73
|
directory: null, // task 1877 — GET /builders/directory, fetched once and cached
|
|
82
|
-
workTab: 'todo', // task 1888 — active pane in the Work card ('todo' | 'done')
|
|
83
74
|
// task 1828 — per-table column sort. by=null → natural order (todo: server
|
|
84
75
|
// order; done: shipped-desc, applied at load). dir: 'desc' | 'asc'.
|
|
85
76
|
workSort: { todo: { by: null, dir: 'desc' }, done: { by: null, dir: 'desc' } },
|
|
@@ -88,12 +79,6 @@
|
|
|
88
79
|
claimsByTask: new Map(), // task 1828 — String(task_id) → {claim_id, builder_id, login} (active claims; powers the Release button)
|
|
89
80
|
conflictPairs: null, // task 1888 — unified pairs (server or client estimate)
|
|
90
81
|
conflictSource: null, // 'server' | 'client' | null (while loading / on error)
|
|
91
|
-
listPager: { page: 1, perPage: loadGoalsPerPage() }, // task 1932 — goal-grid pagination
|
|
92
|
-
// task 1956 — goal-detail Work-table pagination (per table: To do / Completed).
|
|
93
|
-
workPager: {
|
|
94
|
-
todo: { page: 1, perPage: loadPerPageFor(WORK_PER_PAGE_KEY.todo) },
|
|
95
|
-
done: { page: 1, perPage: loadPerPageFor(WORK_PER_PAGE_KEY.done) },
|
|
96
|
-
},
|
|
97
82
|
// task 1953 — private goals: my own pending join-request on the OPEN goal (row
|
|
98
83
|
// from GET /goals/inbox's myPendingRequests, filtered to this goal), or null.
|
|
99
84
|
myPendingRequest: null,
|
|
@@ -204,29 +189,6 @@
|
|
|
204
189
|
(!mineOnly || iAmMember(g)));
|
|
205
190
|
}
|
|
206
191
|
|
|
207
|
-
// task 1932 — the goal grid's pagination bar (range, Per-page <select>,
|
|
208
|
-
// Prev/Next around "Page X of Y"). Hidden when the whole set fits one page
|
|
209
|
-
// (total <= LIST_MIN_PER_PAGE); otherwise shown so the per-page control stays
|
|
210
|
-
// reachable. Pure render from the slice math renderList computed.
|
|
211
|
-
function renderGoalsPager(total, page, totalPages, start, shownCount) {
|
|
212
|
-
const pager = $('#goals-pager');
|
|
213
|
-
if (!pager) return;
|
|
214
|
-
if (total <= LIST_MIN_PER_PAGE) { pager.hidden = true; return; }
|
|
215
|
-
pager.hidden = false;
|
|
216
|
-
const range = $('#goals-range');
|
|
217
|
-
if (range) range.textContent = total
|
|
218
|
-
? `Showing ${start + 1}–${start + shownCount} of ${total}`
|
|
219
|
-
: 'Showing 0 of 0';
|
|
220
|
-
const ind = $('#goals-page-ind');
|
|
221
|
-
if (ind) ind.textContent = `Page ${page} of ${totalPages}`;
|
|
222
|
-
const prev = $('#goals-prev');
|
|
223
|
-
if (prev) prev.disabled = page <= 1;
|
|
224
|
-
const next = $('#goals-next');
|
|
225
|
-
if (next) next.disabled = page >= totalPages;
|
|
226
|
-
const sel = $('#goals-per-page');
|
|
227
|
-
if (sel && sel.value !== String(state.listPager.perPage)) sel.value = String(state.listPager.perPage);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
192
|
function renderList() {
|
|
231
193
|
const grid = $('#goal-grid');
|
|
232
194
|
if (!grid) return;
|
|
@@ -239,24 +201,20 @@
|
|
|
239
201
|
if (count) count.textContent = `${rows.length} goal${rows.length === 1 ? '' : 's'}`;
|
|
240
202
|
|
|
241
203
|
if (!rows.length) {
|
|
242
|
-
grid.innerHTML = '
|
|
243
|
-
|
|
204
|
+
grid.innerHTML = emptyStateHtml('No goals match these filters.');
|
|
205
|
+
pagers.list.page = 1;
|
|
244
206
|
const emptyPager = $('#goals-pager');
|
|
245
207
|
if (emptyPager) emptyPager.hidden = true;
|
|
246
208
|
return;
|
|
247
209
|
}
|
|
248
210
|
|
|
249
|
-
// task 1932
|
|
250
|
-
//
|
|
251
|
-
// Priority numbers reflect the GLOBAL position
|
|
252
|
-
// page-local index, so drag-reorder priorities
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
if (state.listPager.page > totalPages) state.listPager.page = totalPages;
|
|
257
|
-
if (state.listPager.page < 1) state.listPager.page = 1;
|
|
258
|
-
const start = (state.listPager.page - 1) * perPage;
|
|
259
|
-
const pageRows = rows.slice(start, start + perPage);
|
|
211
|
+
// task 1932 → kit (2171): paginate the grid through the shared pager (it
|
|
212
|
+
// clamps the page — the filtered set may have shrunk under a re-render —
|
|
213
|
+
// and paints the bar). Priority numbers reflect the GLOBAL position
|
|
214
|
+
// (start + i + 1), not the page-local index, so drag-reorder priorities
|
|
215
|
+
// stay meaningful across pages.
|
|
216
|
+
const pageRows = paginate(rows, pagers.list);
|
|
217
|
+
const start = (pagers.list.page - 1) * pagers.list.perPage;
|
|
260
218
|
grid.innerHTML = pageRows.map((g, i) => {
|
|
261
219
|
const d = state.details.get(String(g.id));
|
|
262
220
|
const crit = d ? (d.criteria || []) : [];
|
|
@@ -276,10 +234,11 @@
|
|
|
276
234
|
return `<a class="goal-card${blocked ? ' goal-card--blocked' : ''}" href="#/goal/${escapeHtml(String(g.id))}" data-goal-id="${escapeHtml(String(g.id))}"${reorderable ? ' draggable="true"' : ''}>
|
|
277
235
|
<div class="goal-card__head">
|
|
278
236
|
${handle}${priority}
|
|
279
|
-
${d ? ring(sat, crit.length, g.status === 'achieved') : ''}
|
|
237
|
+
${d ? ring(sat, crit.length, g.status === 'achieved', 34) : ''}
|
|
280
238
|
<span class="goal-card__title">${escapeHtml(g.title || 'Untitled goal')}</span>
|
|
281
239
|
</div>
|
|
282
240
|
<div class="goal-card__meta">
|
|
241
|
+
<span class="goal-card__id" title="Goal id — use this to reference the goal (e.g. 'goal ${escapeHtml(String(g.id))}'). Distinct from the priority position on the left.">#${escapeHtml(String(g.id))}</span>
|
|
283
242
|
<span>${escapeHtml(g.version_id || '')}</span>
|
|
284
243
|
${statusBadge(g.status)}
|
|
285
244
|
${blockedPill}
|
|
@@ -291,7 +250,6 @@
|
|
|
291
250
|
</div>
|
|
292
251
|
</a>`;
|
|
293
252
|
}).join('');
|
|
294
|
-
renderGoalsPager(total, state.listPager.page, totalPages, start, pageRows.length);
|
|
295
253
|
}
|
|
296
254
|
|
|
297
255
|
// ---- drag-to-reorder (task 1760, Archon-only, one version at a time) ------
|
|
@@ -331,7 +289,7 @@
|
|
|
331
289
|
// of the goals on other pages. Priority numbers reflect the global
|
|
332
290
|
// position (start + i + 1).
|
|
333
291
|
const filtered = filteredGoals();
|
|
334
|
-
const start = (
|
|
292
|
+
const start = (pagers.list.page - 1) * pagers.list.perPage;
|
|
335
293
|
const cards = [...grid.querySelectorAll('.goal-card')];
|
|
336
294
|
cards.forEach((card, i) => {
|
|
337
295
|
const num = card.querySelector('.goal-card__priority');
|
|
@@ -435,53 +393,12 @@
|
|
|
435
393
|
el.textContent = String(n);
|
|
436
394
|
}
|
|
437
395
|
|
|
438
|
-
// task
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (total <= LIST_MIN_PER_PAGE) { bar.hidden = true; return; }
|
|
445
|
-
bar.hidden = false;
|
|
446
|
-
const range = $(`#${prefix}-range`);
|
|
447
|
-
if (range) range.textContent = total
|
|
448
|
-
? `Showing ${start + 1}–${start + shownCount} of ${total}`
|
|
449
|
-
: 'Showing 0 of 0';
|
|
450
|
-
const ind = $(`#${prefix}-page-ind`);
|
|
451
|
-
if (ind) ind.textContent = `Page ${pager.page} of ${totalPages}`;
|
|
452
|
-
const prev = $(`#${prefix}-prev`);
|
|
453
|
-
if (prev) prev.disabled = pager.page <= 1;
|
|
454
|
-
const next = $(`#${prefix}-next`);
|
|
455
|
-
if (next) next.disabled = pager.page >= totalPages;
|
|
456
|
-
const sel = $(`#${prefix}-per-page`);
|
|
457
|
-
if (sel && sel.value !== String(pager.perPage)) sel.value = String(pager.perPage);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Slice `rows` to a Work table's current page (clamped), paint its bar, return
|
|
461
|
-
// the slice to render.
|
|
462
|
-
function paginateWork(rows, pager, prefix) {
|
|
463
|
-
const total = rows.length;
|
|
464
|
-
const totalPages = Math.max(1, Math.ceil(total / pager.perPage));
|
|
465
|
-
if (pager.page > totalPages) pager.page = totalPages;
|
|
466
|
-
if (pager.page < 1) pager.page = 1;
|
|
467
|
-
const start = (pager.page - 1) * pager.perPage;
|
|
468
|
-
const slice = rows.slice(start, start + pager.perPage);
|
|
469
|
-
renderWorkPager(prefix, pager, total, totalPages, start, slice.length);
|
|
470
|
-
return slice;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// task 1888 — the Work card's To do / Completed tab toggle. Both tables stay
|
|
474
|
-
// in the DOM (already rendered); switching only flips pane visibility.
|
|
475
|
-
function setWorkTab(tab) {
|
|
476
|
-
state.workTab = tab;
|
|
477
|
-
const isTodo = tab === 'todo';
|
|
478
|
-
$('#goal-tab-todo')?.setAttribute('aria-selected', String(isTodo));
|
|
479
|
-
$('#goal-tab-done')?.setAttribute('aria-selected', String(!isTodo));
|
|
480
|
-
const todoPane = $('#goal-todo-pane');
|
|
481
|
-
const donePane = $('#goal-done-pane');
|
|
482
|
-
if (todoPane) todoPane.hidden = !isTodo;
|
|
483
|
-
if (donePane) donePane.hidden = isTodo;
|
|
484
|
-
}
|
|
396
|
+
// task 1888 → kit (2171): the Work card's To do / Completed toggle is an
|
|
397
|
+
// OTBKit tabs instance (built in wireKit below). hash:false is load-bearing —
|
|
398
|
+
// this page owns the #/goal/N sub-route namespace, so the tabs key off
|
|
399
|
+
// in-page state and never touch location.hash.
|
|
400
|
+
let workTabs = null;
|
|
401
|
+
function setWorkTab(tab) { workTabs?.activate(tab); }
|
|
485
402
|
|
|
486
403
|
// task 1828 — sortable column key → header id, per table. Adding a column
|
|
487
404
|
// means an entry here + a matching <th id> in goals.html.
|
|
@@ -546,21 +463,21 @@
|
|
|
546
463
|
const doneBody = document.querySelector('#goal-done tbody');
|
|
547
464
|
const todo = sortWorkRows(state.work.todo, state.workSort.todo);
|
|
548
465
|
const done = sortWorkRows(state.work.done, state.workSort.done);
|
|
549
|
-
// task 1956
|
|
550
|
-
const todoSlice =
|
|
551
|
-
const doneSlice =
|
|
466
|
+
// task 1956 → kit (2171): paginate each table through the shared pager.
|
|
467
|
+
const todoSlice = paginate(todo, pagers.todo);
|
|
468
|
+
const doneSlice = paginate(done, pagers.done);
|
|
552
469
|
if (todoBody) {
|
|
553
470
|
todoBody.innerHTML = todo.length
|
|
554
471
|
? taskRowsTodo(todoSlice)
|
|
555
|
-
: `<tr><td colspan="6" class="ledger__empty"
|
|
472
|
+
: `<tr><td colspan="6" class="ledger__empty">${emptyStateHtml(`Nothing queued under this goal. Members can author bounded tasks here${state.me ? '' : ' once signed in'}.`)}</td></tr>`;
|
|
556
473
|
}
|
|
557
474
|
if (doneBody) {
|
|
558
475
|
doneBody.innerHTML = done.length
|
|
559
476
|
? taskRowsDone(doneSlice)
|
|
560
|
-
:
|
|
477
|
+
: `<tr><td colspan="5" class="ledger__empty">${emptyStateHtml('Nothing completed yet.')}</td></tr>`;
|
|
561
478
|
}
|
|
562
|
-
setCount('
|
|
563
|
-
setCount('
|
|
479
|
+
workTabs?.setCount('todo', state.work.todo.length);
|
|
480
|
+
workTabs?.setCount('done', state.work.done.length);
|
|
564
481
|
reflectSortHeaders('todo');
|
|
565
482
|
reflectSortHeaders('done');
|
|
566
483
|
}
|
|
@@ -648,7 +565,7 @@
|
|
|
648
565
|
function fillConflicts(container, pairs) {
|
|
649
566
|
if (!container) return;
|
|
650
567
|
if (pairs == null) {
|
|
651
|
-
container.innerHTML = '
|
|
568
|
+
container.innerHTML = emptyStateHtml('Conflicts could not be computed.');
|
|
652
569
|
return;
|
|
653
570
|
}
|
|
654
571
|
const note = state.conflictSource === 'client'
|
|
@@ -656,7 +573,7 @@
|
|
|
656
573
|
: '';
|
|
657
574
|
container.innerHTML = (pairs.length
|
|
658
575
|
? pairs.map(conflictHtml).join('')
|
|
659
|
-
: '
|
|
576
|
+
: emptyStateHtml('No conflicts detected among this goal’s open tasks.')) + note;
|
|
660
577
|
}
|
|
661
578
|
|
|
662
579
|
const currentDetail = () => (state.current == null ? null : state.details.get(String(state.current)));
|
|
@@ -719,7 +636,7 @@
|
|
|
719
636
|
// (withDetail=false) instead reveals detail via a per-criterion <details> expand
|
|
720
637
|
// (BV1.R79) so the full text is one click away, not hover-only.
|
|
721
638
|
function critListHtml(list, withDetail) {
|
|
722
|
-
if (!list.length) return '
|
|
639
|
+
if (!list.length) return emptyStateHtml('No done-when criteria yet.');
|
|
723
640
|
return `<ul class="crit-list${withDetail ? ' crit-list--full' : ''}">${list.map((c) => {
|
|
724
641
|
const parts = window.OTB.splitCriterion ? window.OTB.splitCriterion(c.criterion_md) : { label: c.criterion_md, detail: '' };
|
|
725
642
|
const body = withDetail
|
|
@@ -808,7 +725,7 @@
|
|
|
808
725
|
if (list) {
|
|
809
726
|
list.innerHTML = members.length
|
|
810
727
|
? members.slice(0, MEMBER_CAP).map(memberLi).join('')
|
|
811
|
-
: '
|
|
728
|
+
: `<li>${emptyStateHtml('No members yet — be the first to join.')}</li>`;
|
|
812
729
|
}
|
|
813
730
|
renderViewAllFoot('#goal-members-foot', members.length, MEMBER_CAP, 'members', 'members');
|
|
814
731
|
renderMembersFull(d);
|
|
@@ -833,14 +750,14 @@
|
|
|
833
750
|
<span class="mstat"><strong>${fmtNum(earned)}</strong> ${escapeHtml(cur)} earned</span>
|
|
834
751
|
<span class="mstat"><strong>${fmtNum(open)}</strong> open</span>`;
|
|
835
752
|
} else {
|
|
836
|
-
agg.innerHTML = '<span class="mstat
|
|
753
|
+
agg.innerHTML = '<span class="mstat"><span class="kit-skel"></span></span>';
|
|
837
754
|
}
|
|
838
755
|
}
|
|
839
756
|
const full = $('#goal-members-full');
|
|
840
757
|
if (full) {
|
|
841
758
|
full.innerHTML = members.length
|
|
842
759
|
? `<ul class="member-list">${members.map((m) => memberStatLi(m, stats)).join('')}</ul>`
|
|
843
|
-
: '
|
|
760
|
+
: emptyStateHtml('No members yet — be the first to join.');
|
|
844
761
|
}
|
|
845
762
|
const back = $('#goal-members-back');
|
|
846
763
|
if (back && state.current != null) back.setAttribute('href', `#/goal/${state.current}`);
|
|
@@ -857,7 +774,9 @@
|
|
|
857
774
|
const iAmLead = members.some((m) => sameId(m.builder_id, selfId()) && m.role === 'lead');
|
|
858
775
|
|
|
859
776
|
const h = $('#goal-detail-h');
|
|
860
|
-
|
|
777
|
+
// task 2163 — surface the numeric goal id alongside the title (escaped title,
|
|
778
|
+
// so switching from textContent to innerHTML stays injection-safe).
|
|
779
|
+
if (h) h.innerHTML = `<span class="goal-detail__id" title="Goal id">#${escapeHtml(String(g.id))}</span> ${escapeHtml(g.title || 'Untitled goal')}`;
|
|
861
780
|
const desc = $('#goal-detail-desc');
|
|
862
781
|
if (desc) desc.textContent = g.description || '';
|
|
863
782
|
const meta = $('#goal-detail-meta');
|
|
@@ -1040,7 +959,7 @@
|
|
|
1040
959
|
}
|
|
1041
960
|
box.hidden = false;
|
|
1042
961
|
if (!state.inviteOpen) {
|
|
1043
|
-
box.innerHTML = '<button class="btn-ghost" id="goal-invite-open" type="button">+ Invite
|
|
962
|
+
box.innerHTML = '<button class="btn-ghost" id="goal-invite-open" type="button">+ Invite</button>';
|
|
1044
963
|
$('#goal-invite-open')?.addEventListener('click', async () => {
|
|
1045
964
|
state.inviteOpen = true;
|
|
1046
965
|
await loadDirectory();
|
|
@@ -1205,8 +1124,8 @@
|
|
|
1205
1124
|
<span class="pending-row__note">invited — awaiting their response</span>
|
|
1206
1125
|
</li>`;
|
|
1207
1126
|
const parts = [];
|
|
1208
|
-
if (requests.length) parts.push(`<div class="pending-group"><h3 class="pending-group__h">Join requests<span class="
|
|
1209
|
-
if (invites.length) parts.push(`<div class="pending-group"><h3 class="pending-group__h">Pending invitations<span class="
|
|
1127
|
+
if (requests.length) parts.push(`<div class="pending-group"><h3 class="pending-group__h">Join requests<span class="count-pill">${requests.length}</span></h3><ul class="pending-list">${requests.map(requestLi).join('')}</ul></div>`);
|
|
1128
|
+
if (invites.length) parts.push(`<div class="pending-group"><h3 class="pending-group__h">Pending invitations<span class="count-pill">${invites.length}</span></h3><ul class="pending-list">${invites.map(inviteLi).join('')}</ul></div>`);
|
|
1210
1129
|
return parts.join('');
|
|
1211
1130
|
}
|
|
1212
1131
|
function renderPendingRequests() {
|
|
@@ -1265,10 +1184,9 @@
|
|
|
1265
1184
|
// task 1888 — a freshly opened goal starts from the compact defaults: To do
|
|
1266
1185
|
// tab selected, capped lists, counts hidden until its own data lands.
|
|
1267
1186
|
function resetWorkUi() {
|
|
1268
|
-
state.workTab = 'todo';
|
|
1269
1187
|
// task 1956 — a freshly opened goal starts both Work tables on page 1 (per-page persists).
|
|
1270
|
-
|
|
1271
|
-
|
|
1188
|
+
pagers.todo.page = 1;
|
|
1189
|
+
pagers.done.page = 1;
|
|
1272
1190
|
state.workSort = { todo: { by: null, dir: 'desc' }, done: { by: null, dir: 'desc' } };
|
|
1273
1191
|
state.work = { todo: [], done: [] };
|
|
1274
1192
|
state.workLoaded = false;
|
|
@@ -1282,7 +1200,9 @@
|
|
|
1282
1200
|
const pending = $('#goal-pending');
|
|
1283
1201
|
if (pending) { pending.hidden = true; pending.innerHTML = ''; }
|
|
1284
1202
|
setWorkTab('todo');
|
|
1285
|
-
|
|
1203
|
+
workTabs?.setCount('todo', null);
|
|
1204
|
+
workTabs?.setCount('done', null);
|
|
1205
|
+
['#goal-conflicts-count', '#goal-conflicts-view-count',
|
|
1286
1206
|
'#goal-members-count', '#goal-members-view-count', '#goal-completion-count'].forEach((sel) => {
|
|
1287
1207
|
const el = $(sel);
|
|
1288
1208
|
if (el) { el.hidden = true; el.textContent = ''; }
|
|
@@ -1294,14 +1214,15 @@
|
|
|
1294
1214
|
['#goal-conflicts-card', '#goal-conflicts-view-card'].forEach((sel) => $(sel)?.classList.remove('goal-conflicts-card--live'));
|
|
1295
1215
|
reflectSortHeaders('todo');
|
|
1296
1216
|
reflectSortHeaders('done');
|
|
1217
|
+
// Kit shimmer placeholders while the goal's tasks load (was "…" cells).
|
|
1297
1218
|
const todoBody = document.querySelector('#goal-todo tbody');
|
|
1298
1219
|
const doneBody = document.querySelector('#goal-done tbody');
|
|
1299
|
-
if (todoBody) todoBody.innerHTML =
|
|
1300
|
-
if (doneBody) doneBody.innerHTML =
|
|
1220
|
+
if (todoBody) todoBody.innerHTML = skeletonRows(3, 6);
|
|
1221
|
+
if (doneBody) doneBody.innerHTML = skeletonRows(3, 5);
|
|
1301
1222
|
const conflicts = $('#goal-conflicts');
|
|
1302
|
-
if (conflicts) conflicts.innerHTML = '<
|
|
1223
|
+
if (conflicts) conflicts.innerHTML = '<span class="kit-skel"></span>';
|
|
1303
1224
|
const conflictsFull = $('#goal-conflicts-full');
|
|
1304
|
-
if (conflictsFull) conflictsFull.innerHTML = '<
|
|
1225
|
+
if (conflictsFull) conflictsFull.innerHTML = '<span class="kit-skel"></span>';
|
|
1305
1226
|
}
|
|
1306
1227
|
|
|
1307
1228
|
// task 1828 — views sharing this page: the list, a goal's detail, and the
|
|
@@ -1394,6 +1315,12 @@
|
|
|
1394
1315
|
}
|
|
1395
1316
|
|
|
1396
1317
|
async function boot() {
|
|
1318
|
+
// Kit shimmer placeholders while the goal list loads (was one "…" div).
|
|
1319
|
+
const grid = $('#goal-grid');
|
|
1320
|
+
if (grid) {
|
|
1321
|
+
grid.innerHTML = Array.from({ length: 8 }, () =>
|
|
1322
|
+
'<div class="goal-card goal-card--skel" aria-hidden="true"><span class="kit-skel"></span><span class="kit-skel"></span></div>').join('');
|
|
1323
|
+
}
|
|
1397
1324
|
try {
|
|
1398
1325
|
state.me = await getJSON(`${API}/me`).catch(() => null);
|
|
1399
1326
|
// task 1828 — "My goals" needs an identity; disable it when signed out.
|
|
@@ -1412,30 +1339,37 @@
|
|
|
1412
1339
|
const sub = $('#goals-sub');
|
|
1413
1340
|
if (sub) sub.textContent = 'Goals could not be loaded. Refresh to try again.';
|
|
1414
1341
|
const grid = $('#goal-grid');
|
|
1415
|
-
if (grid) grid.innerHTML = '
|
|
1342
|
+
if (grid) grid.innerHTML = emptyStateHtml('Goals could not be loaded.');
|
|
1416
1343
|
}
|
|
1417
1344
|
}
|
|
1418
1345
|
|
|
1419
1346
|
// task 1932 — any list filter change re-paginates the grid from page 1.
|
|
1420
|
-
const onListFilterChange = () => {
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
//
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
$('#goals-prev')?.addEventListener('click', () => {
|
|
1434
|
-
if (state.listPager.page > 1) { state.listPager.page -= 1; renderList(); }
|
|
1435
|
-
});
|
|
1436
|
-
$('#goals-next')?.addEventListener('click', () => {
|
|
1437
|
-
state.listPager.page += 1; renderList(); // renderList clamps to the last page
|
|
1347
|
+
const onListFilterChange = () => { pagers.list.page = 1; renderList(); };
|
|
1348
|
+
// task 2171 — the list filter bar is a kit filterBar: Version / Status /
|
|
1349
|
+
// Membership, all primary (no overflow needed). Control ids are unchanged
|
|
1350
|
+
// (f-goal-version, f-goal-status, f-goal-mine) so filteredGoals' reads +
|
|
1351
|
+
// populateVersionFilter's dynamic option fill keep working untouched.
|
|
1352
|
+
makeFilterBar({
|
|
1353
|
+
mount: $('#goals-filters'),
|
|
1354
|
+
primary: [
|
|
1355
|
+
{ id: 'f-goal-version', label: 'Version', options: [{ value: '', label: '— any —' }] },
|
|
1356
|
+
{ id: 'f-goal-status', label: 'Status', value: 'open', options: [{ value: '', label: '— any —' }, 'open', 'achieved', 'archived'] },
|
|
1357
|
+
{ id: 'f-goal-mine', label: 'Membership', options: [{ value: '', label: 'All goals' }, { value: 'mine', label: 'My goals' }] },
|
|
1358
|
+
],
|
|
1359
|
+
onChange: onListFilterChange,
|
|
1438
1360
|
});
|
|
1361
|
+
// task 2171 — stamp + wire the kit pager bars (grid + both Work tables); the
|
|
1362
|
+
// bar ids and localStorage prefs are exactly what the bespoke pagers used.
|
|
1363
|
+
const pagerMounts = [
|
|
1364
|
+
['#goals-pager-mount', pagers.list, 'Goals', renderList],
|
|
1365
|
+
['#goal-todo-pager-mount', pagers.todo, 'To-do tasks', renderWork],
|
|
1366
|
+
['#goal-done-pager-mount', pagers.done, 'Completed tasks', renderWork],
|
|
1367
|
+
];
|
|
1368
|
+
for (const [sel, pager, label, rerender] of pagerMounts) {
|
|
1369
|
+
const mount = $(sel);
|
|
1370
|
+
if (mount) mount.innerHTML = pagerBarHtml(pager.prefix, { label });
|
|
1371
|
+
wirePager(pager, rerender);
|
|
1372
|
+
}
|
|
1439
1373
|
$('#goal-back')?.addEventListener('click', (e) => { e.preventDefault(); window.location.hash = ''; });
|
|
1440
1374
|
// task 1828 — "← Back to goal" from any of the full views.
|
|
1441
1375
|
['#goal-conflicts-back', '#goal-members-back', '#goal-completion-back'].forEach((sel) => {
|
|
@@ -1444,25 +1378,16 @@
|
|
|
1444
1378
|
if (state.current != null) window.location.hash = `#/goal/${state.current}`;
|
|
1445
1379
|
});
|
|
1446
1380
|
});
|
|
1447
|
-
// task 1888
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
try { window.localStorage.setItem(WORK_PER_PAGE_KEY[key], String(v)); } catch (_) { /* private mode → in-memory only */ }
|
|
1458
|
-
renderWork();
|
|
1459
|
-
});
|
|
1460
|
-
$(`#${prefix}-prev`)?.addEventListener('click', () => {
|
|
1461
|
-
if (state.workPager[key].page > 1) { state.workPager[key].page -= 1; renderWork(); }
|
|
1462
|
-
});
|
|
1463
|
-
$(`#${prefix}-next`)?.addEventListener('click', () => {
|
|
1464
|
-
state.workPager[key].page += 1; renderWork(); // renderWork clamps to the last page
|
|
1465
|
-
});
|
|
1381
|
+
// task 1888 → kit (2171): the Work card's tab toggle. hash:false is
|
|
1382
|
+
// load-bearing — this page owns the #/goal/N sub-route namespace, so the
|
|
1383
|
+
// tabs key off in-page state and never touch location.hash.
|
|
1384
|
+
workTabs = makeTabs({
|
|
1385
|
+
mount: $('#goal-work-tabs'),
|
|
1386
|
+
hash: false,
|
|
1387
|
+
tabs: [
|
|
1388
|
+
{ id: 'todo', label: 'To do', panel: 'goal-todo-pane' },
|
|
1389
|
+
{ id: 'done', label: 'Completed', panel: 'goal-done-pane' },
|
|
1390
|
+
],
|
|
1466
1391
|
});
|
|
1467
1392
|
// task 1828 — click / keyboard sort on the Work-table column headers.
|
|
1468
1393
|
Object.entries(WORK_SORT_HEADERS).forEach(([table, cols]) => {
|