@bongos/core 1.17.3 → 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 +161 -91
- 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/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 +4 -345
- package/docs/api-reference.md +5 -17
- 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 +37 -0
- package/modules/hall-ui/public/goals-page.js +98 -176
- package/modules/hall-ui/public/goals.css +183 -0
- package/modules/hall-ui/public/goals.html +35 -261
- package/modules/hall-ui/public/goals.js +7 -2
- package/modules/hall-ui/public/hall-widgets.js +12 -4
- 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 +31 -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/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/fixtures/access-requests.json +20 -1
- package/scripts/hall-preview/fixtures/audit-log.json +68 -1
- 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 -1
- package/scripts/hall-preview/fixtures/builders__roster.json +48 -1
- package/scripts/hall-preview/fixtures/grades__by-builder.json +32 -1
- package/scripts/hall-preview/fixtures/inbox__detail.json +15 -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__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 -1
- package/scripts/hall-preview/fixtures/security__docs.json +10 -1
- package/scripts/hall-preview/fixtures/security__reports.json +31 -1
- package/scripts/hall-preview/fixtures/sessions__search.json +9 -1
- package/scripts/hall-preview/server.js +18 -1
- package/scripts/migrate.sh +0 -0
- 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/module-api.js +20 -1
- package/tests/provision.mjs +69 -0
|
@@ -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,7 +234,7 @@
|
|
|
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">
|
|
@@ -292,7 +250,6 @@
|
|
|
292
250
|
</div>
|
|
293
251
|
</a>`;
|
|
294
252
|
}).join('');
|
|
295
|
-
renderGoalsPager(total, state.listPager.page, totalPages, start, pageRows.length);
|
|
296
253
|
}
|
|
297
254
|
|
|
298
255
|
// ---- drag-to-reorder (task 1760, Archon-only, one version at a time) ------
|
|
@@ -332,7 +289,7 @@
|
|
|
332
289
|
// of the goals on other pages. Priority numbers reflect the global
|
|
333
290
|
// position (start + i + 1).
|
|
334
291
|
const filtered = filteredGoals();
|
|
335
|
-
const start = (
|
|
292
|
+
const start = (pagers.list.page - 1) * pagers.list.perPage;
|
|
336
293
|
const cards = [...grid.querySelectorAll('.goal-card')];
|
|
337
294
|
cards.forEach((card, i) => {
|
|
338
295
|
const num = card.querySelector('.goal-card__priority');
|
|
@@ -436,53 +393,12 @@
|
|
|
436
393
|
el.textContent = String(n);
|
|
437
394
|
}
|
|
438
395
|
|
|
439
|
-
// task
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if (total <= LIST_MIN_PER_PAGE) { bar.hidden = true; return; }
|
|
446
|
-
bar.hidden = false;
|
|
447
|
-
const range = $(`#${prefix}-range`);
|
|
448
|
-
if (range) range.textContent = total
|
|
449
|
-
? `Showing ${start + 1}–${start + shownCount} of ${total}`
|
|
450
|
-
: 'Showing 0 of 0';
|
|
451
|
-
const ind = $(`#${prefix}-page-ind`);
|
|
452
|
-
if (ind) ind.textContent = `Page ${pager.page} of ${totalPages}`;
|
|
453
|
-
const prev = $(`#${prefix}-prev`);
|
|
454
|
-
if (prev) prev.disabled = pager.page <= 1;
|
|
455
|
-
const next = $(`#${prefix}-next`);
|
|
456
|
-
if (next) next.disabled = pager.page >= totalPages;
|
|
457
|
-
const sel = $(`#${prefix}-per-page`);
|
|
458
|
-
if (sel && sel.value !== String(pager.perPage)) sel.value = String(pager.perPage);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Slice `rows` to a Work table's current page (clamped), paint its bar, return
|
|
462
|
-
// the slice to render.
|
|
463
|
-
function paginateWork(rows, pager, prefix) {
|
|
464
|
-
const total = rows.length;
|
|
465
|
-
const totalPages = Math.max(1, Math.ceil(total / pager.perPage));
|
|
466
|
-
if (pager.page > totalPages) pager.page = totalPages;
|
|
467
|
-
if (pager.page < 1) pager.page = 1;
|
|
468
|
-
const start = (pager.page - 1) * pager.perPage;
|
|
469
|
-
const slice = rows.slice(start, start + pager.perPage);
|
|
470
|
-
renderWorkPager(prefix, pager, total, totalPages, start, slice.length);
|
|
471
|
-
return slice;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// task 1888 — the Work card's To do / Completed tab toggle. Both tables stay
|
|
475
|
-
// in the DOM (already rendered); switching only flips pane visibility.
|
|
476
|
-
function setWorkTab(tab) {
|
|
477
|
-
state.workTab = tab;
|
|
478
|
-
const isTodo = tab === 'todo';
|
|
479
|
-
$('#goal-tab-todo')?.setAttribute('aria-selected', String(isTodo));
|
|
480
|
-
$('#goal-tab-done')?.setAttribute('aria-selected', String(!isTodo));
|
|
481
|
-
const todoPane = $('#goal-todo-pane');
|
|
482
|
-
const donePane = $('#goal-done-pane');
|
|
483
|
-
if (todoPane) todoPane.hidden = !isTodo;
|
|
484
|
-
if (donePane) donePane.hidden = isTodo;
|
|
485
|
-
}
|
|
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); }
|
|
486
402
|
|
|
487
403
|
// task 1828 — sortable column key → header id, per table. Adding a column
|
|
488
404
|
// means an entry here + a matching <th id> in goals.html.
|
|
@@ -547,21 +463,21 @@
|
|
|
547
463
|
const doneBody = document.querySelector('#goal-done tbody');
|
|
548
464
|
const todo = sortWorkRows(state.work.todo, state.workSort.todo);
|
|
549
465
|
const done = sortWorkRows(state.work.done, state.workSort.done);
|
|
550
|
-
// task 1956
|
|
551
|
-
const todoSlice =
|
|
552
|
-
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);
|
|
553
469
|
if (todoBody) {
|
|
554
470
|
todoBody.innerHTML = todo.length
|
|
555
471
|
? taskRowsTodo(todoSlice)
|
|
556
|
-
: `<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>`;
|
|
557
473
|
}
|
|
558
474
|
if (doneBody) {
|
|
559
475
|
doneBody.innerHTML = done.length
|
|
560
476
|
? taskRowsDone(doneSlice)
|
|
561
|
-
:
|
|
477
|
+
: `<tr><td colspan="5" class="ledger__empty">${emptyStateHtml('Nothing completed yet.')}</td></tr>`;
|
|
562
478
|
}
|
|
563
|
-
setCount('
|
|
564
|
-
setCount('
|
|
479
|
+
workTabs?.setCount('todo', state.work.todo.length);
|
|
480
|
+
workTabs?.setCount('done', state.work.done.length);
|
|
565
481
|
reflectSortHeaders('todo');
|
|
566
482
|
reflectSortHeaders('done');
|
|
567
483
|
}
|
|
@@ -649,7 +565,7 @@
|
|
|
649
565
|
function fillConflicts(container, pairs) {
|
|
650
566
|
if (!container) return;
|
|
651
567
|
if (pairs == null) {
|
|
652
|
-
container.innerHTML = '
|
|
568
|
+
container.innerHTML = emptyStateHtml('Conflicts could not be computed.');
|
|
653
569
|
return;
|
|
654
570
|
}
|
|
655
571
|
const note = state.conflictSource === 'client'
|
|
@@ -657,7 +573,7 @@
|
|
|
657
573
|
: '';
|
|
658
574
|
container.innerHTML = (pairs.length
|
|
659
575
|
? pairs.map(conflictHtml).join('')
|
|
660
|
-
: '
|
|
576
|
+
: emptyStateHtml('No conflicts detected among this goal’s open tasks.')) + note;
|
|
661
577
|
}
|
|
662
578
|
|
|
663
579
|
const currentDetail = () => (state.current == null ? null : state.details.get(String(state.current)));
|
|
@@ -720,7 +636,7 @@
|
|
|
720
636
|
// (withDetail=false) instead reveals detail via a per-criterion <details> expand
|
|
721
637
|
// (BV1.R79) so the full text is one click away, not hover-only.
|
|
722
638
|
function critListHtml(list, withDetail) {
|
|
723
|
-
if (!list.length) return '
|
|
639
|
+
if (!list.length) return emptyStateHtml('No done-when criteria yet.');
|
|
724
640
|
return `<ul class="crit-list${withDetail ? ' crit-list--full' : ''}">${list.map((c) => {
|
|
725
641
|
const parts = window.OTB.splitCriterion ? window.OTB.splitCriterion(c.criterion_md) : { label: c.criterion_md, detail: '' };
|
|
726
642
|
const body = withDetail
|
|
@@ -809,7 +725,7 @@
|
|
|
809
725
|
if (list) {
|
|
810
726
|
list.innerHTML = members.length
|
|
811
727
|
? members.slice(0, MEMBER_CAP).map(memberLi).join('')
|
|
812
|
-
: '
|
|
728
|
+
: `<li>${emptyStateHtml('No members yet — be the first to join.')}</li>`;
|
|
813
729
|
}
|
|
814
730
|
renderViewAllFoot('#goal-members-foot', members.length, MEMBER_CAP, 'members', 'members');
|
|
815
731
|
renderMembersFull(d);
|
|
@@ -834,14 +750,14 @@
|
|
|
834
750
|
<span class="mstat"><strong>${fmtNum(earned)}</strong> ${escapeHtml(cur)} earned</span>
|
|
835
751
|
<span class="mstat"><strong>${fmtNum(open)}</strong> open</span>`;
|
|
836
752
|
} else {
|
|
837
|
-
agg.innerHTML = '<span class="mstat
|
|
753
|
+
agg.innerHTML = '<span class="mstat"><span class="kit-skel"></span></span>';
|
|
838
754
|
}
|
|
839
755
|
}
|
|
840
756
|
const full = $('#goal-members-full');
|
|
841
757
|
if (full) {
|
|
842
758
|
full.innerHTML = members.length
|
|
843
759
|
? `<ul class="member-list">${members.map((m) => memberStatLi(m, stats)).join('')}</ul>`
|
|
844
|
-
: '
|
|
760
|
+
: emptyStateHtml('No members yet — be the first to join.');
|
|
845
761
|
}
|
|
846
762
|
const back = $('#goal-members-back');
|
|
847
763
|
if (back && state.current != null) back.setAttribute('href', `#/goal/${state.current}`);
|
|
@@ -1043,7 +959,7 @@
|
|
|
1043
959
|
}
|
|
1044
960
|
box.hidden = false;
|
|
1045
961
|
if (!state.inviteOpen) {
|
|
1046
|
-
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>';
|
|
1047
963
|
$('#goal-invite-open')?.addEventListener('click', async () => {
|
|
1048
964
|
state.inviteOpen = true;
|
|
1049
965
|
await loadDirectory();
|
|
@@ -1208,8 +1124,8 @@
|
|
|
1208
1124
|
<span class="pending-row__note">invited — awaiting their response</span>
|
|
1209
1125
|
</li>`;
|
|
1210
1126
|
const parts = [];
|
|
1211
|
-
if (requests.length) parts.push(`<div class="pending-group"><h3 class="pending-group__h">Join requests<span class="
|
|
1212
|
-
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>`);
|
|
1213
1129
|
return parts.join('');
|
|
1214
1130
|
}
|
|
1215
1131
|
function renderPendingRequests() {
|
|
@@ -1268,10 +1184,9 @@
|
|
|
1268
1184
|
// task 1888 — a freshly opened goal starts from the compact defaults: To do
|
|
1269
1185
|
// tab selected, capped lists, counts hidden until its own data lands.
|
|
1270
1186
|
function resetWorkUi() {
|
|
1271
|
-
state.workTab = 'todo';
|
|
1272
1187
|
// task 1956 — a freshly opened goal starts both Work tables on page 1 (per-page persists).
|
|
1273
|
-
|
|
1274
|
-
|
|
1188
|
+
pagers.todo.page = 1;
|
|
1189
|
+
pagers.done.page = 1;
|
|
1275
1190
|
state.workSort = { todo: { by: null, dir: 'desc' }, done: { by: null, dir: 'desc' } };
|
|
1276
1191
|
state.work = { todo: [], done: [] };
|
|
1277
1192
|
state.workLoaded = false;
|
|
@@ -1285,7 +1200,9 @@
|
|
|
1285
1200
|
const pending = $('#goal-pending');
|
|
1286
1201
|
if (pending) { pending.hidden = true; pending.innerHTML = ''; }
|
|
1287
1202
|
setWorkTab('todo');
|
|
1288
|
-
|
|
1203
|
+
workTabs?.setCount('todo', null);
|
|
1204
|
+
workTabs?.setCount('done', null);
|
|
1205
|
+
['#goal-conflicts-count', '#goal-conflicts-view-count',
|
|
1289
1206
|
'#goal-members-count', '#goal-members-view-count', '#goal-completion-count'].forEach((sel) => {
|
|
1290
1207
|
const el = $(sel);
|
|
1291
1208
|
if (el) { el.hidden = true; el.textContent = ''; }
|
|
@@ -1297,14 +1214,15 @@
|
|
|
1297
1214
|
['#goal-conflicts-card', '#goal-conflicts-view-card'].forEach((sel) => $(sel)?.classList.remove('goal-conflicts-card--live'));
|
|
1298
1215
|
reflectSortHeaders('todo');
|
|
1299
1216
|
reflectSortHeaders('done');
|
|
1217
|
+
// Kit shimmer placeholders while the goal's tasks load (was "…" cells).
|
|
1300
1218
|
const todoBody = document.querySelector('#goal-todo tbody');
|
|
1301
1219
|
const doneBody = document.querySelector('#goal-done tbody');
|
|
1302
|
-
if (todoBody) todoBody.innerHTML =
|
|
1303
|
-
if (doneBody) doneBody.innerHTML =
|
|
1220
|
+
if (todoBody) todoBody.innerHTML = skeletonRows(3, 6);
|
|
1221
|
+
if (doneBody) doneBody.innerHTML = skeletonRows(3, 5);
|
|
1304
1222
|
const conflicts = $('#goal-conflicts');
|
|
1305
|
-
if (conflicts) conflicts.innerHTML = '<
|
|
1223
|
+
if (conflicts) conflicts.innerHTML = '<span class="kit-skel"></span>';
|
|
1306
1224
|
const conflictsFull = $('#goal-conflicts-full');
|
|
1307
|
-
if (conflictsFull) conflictsFull.innerHTML = '<
|
|
1225
|
+
if (conflictsFull) conflictsFull.innerHTML = '<span class="kit-skel"></span>';
|
|
1308
1226
|
}
|
|
1309
1227
|
|
|
1310
1228
|
// task 1828 — views sharing this page: the list, a goal's detail, and the
|
|
@@ -1397,6 +1315,12 @@
|
|
|
1397
1315
|
}
|
|
1398
1316
|
|
|
1399
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
|
+
}
|
|
1400
1324
|
try {
|
|
1401
1325
|
state.me = await getJSON(`${API}/me`).catch(() => null);
|
|
1402
1326
|
// task 1828 — "My goals" needs an identity; disable it when signed out.
|
|
@@ -1415,30 +1339,37 @@
|
|
|
1415
1339
|
const sub = $('#goals-sub');
|
|
1416
1340
|
if (sub) sub.textContent = 'Goals could not be loaded. Refresh to try again.';
|
|
1417
1341
|
const grid = $('#goal-grid');
|
|
1418
|
-
if (grid) grid.innerHTML = '
|
|
1342
|
+
if (grid) grid.innerHTML = emptyStateHtml('Goals could not be loaded.');
|
|
1419
1343
|
}
|
|
1420
1344
|
}
|
|
1421
1345
|
|
|
1422
1346
|
// task 1932 — any list filter change re-paginates the grid from page 1.
|
|
1423
|
-
const onListFilterChange = () => {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
//
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
$('#goals-prev')?.addEventListener('click', () => {
|
|
1437
|
-
if (state.listPager.page > 1) { state.listPager.page -= 1; renderList(); }
|
|
1438
|
-
});
|
|
1439
|
-
$('#goals-next')?.addEventListener('click', () => {
|
|
1440
|
-
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,
|
|
1441
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
|
+
}
|
|
1442
1373
|
$('#goal-back')?.addEventListener('click', (e) => { e.preventDefault(); window.location.hash = ''; });
|
|
1443
1374
|
// task 1828 — "← Back to goal" from any of the full views.
|
|
1444
1375
|
['#goal-conflicts-back', '#goal-members-back', '#goal-completion-back'].forEach((sel) => {
|
|
@@ -1447,25 +1378,16 @@
|
|
|
1447
1378
|
if (state.current != null) window.location.hash = `#/goal/${state.current}`;
|
|
1448
1379
|
});
|
|
1449
1380
|
});
|
|
1450
|
-
// task 1888
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
try { window.localStorage.setItem(WORK_PER_PAGE_KEY[key], String(v)); } catch (_) { /* private mode → in-memory only */ }
|
|
1461
|
-
renderWork();
|
|
1462
|
-
});
|
|
1463
|
-
$(`#${prefix}-prev`)?.addEventListener('click', () => {
|
|
1464
|
-
if (state.workPager[key].page > 1) { state.workPager[key].page -= 1; renderWork(); }
|
|
1465
|
-
});
|
|
1466
|
-
$(`#${prefix}-next`)?.addEventListener('click', () => {
|
|
1467
|
-
state.workPager[key].page += 1; renderWork(); // renderWork clamps to the last page
|
|
1468
|
-
});
|
|
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
|
+
],
|
|
1469
1391
|
});
|
|
1470
1392
|
// task 1828 — click / keyboard sort on the Work-table column headers.
|
|
1471
1393
|
Object.entries(WORK_SORT_HEADERS).forEach(([table, cols]) => {
|