create-pmos 0.185.0 → 0.187.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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/scripts/workflow-state.js +2 -1
  6. package/payload/state/discoveries.md +1 -1
  7. package/workspace/c3-ib-triage-backlog-test.js +20 -13
  8. package/workspace/c3-ini-life-test.js +167 -0
  9. package/workspace/c3-initiatives-test.js +6 -6
  10. package/workspace/c3-phone-test.js +67 -16
  11. package/workspace/c3-quiet-test.js +2 -1
  12. package/workspace/c3-report-setup-test.js +1 -0
  13. package/workspace/guide/initiative-boards.md +37 -2
  14. package/workspace/guide/phone-companion.md +19 -4
  15. package/workspace/guide/reports-set-up.md +4 -2
  16. package/workspace/lib/engine/boards.js +73 -18
  17. package/workspace/lib/engine/workflow-state.js +2 -1
  18. package/workspace/lib/initiative-rows.js +2 -2
  19. package/workspace/lib/routes-boards.js +119 -6
  20. package/workspace/lib/routes-meeting-focus.js +19 -7
  21. package/workspace/lib/routes-report-setups.js +37 -4
  22. package/workspace/lib/vendor/scripts/workflow-state.js +2 -1
  23. package/workspace/package-lock.json +2 -2
  24. package/workspace/package.json +1 -1
  25. package/workspace/public/pm/c3-demo-boards.js +82 -17
  26. package/workspace/public/pm/c3-demo-focus.js +39 -1
  27. package/workspace/public/pm/c3-demo-rs.js +3 -1
  28. package/workspace/public/pm/c3-focus.css +53 -0
  29. package/workspace/public/pm/c3-focus.js +188 -44
  30. package/workspace/public/pm/c3-ib-initiatives.js +127 -60
  31. package/workspace/public/pm/c3-ib-items.js +2 -2
  32. package/workspace/public/pm/c3-ini-life.css +32 -0
  33. package/workspace/public/pm/c3-ini-life.js +379 -0
  34. package/workspace/public/pm/c3-ini-ui.js +12 -7
  35. package/workspace/public/pm/c3-initiatives.js +14 -4
  36. package/workspace/public/pm/c3-phone.css +3 -1
  37. package/workspace/public/pm/c3-phone.js +28 -8
  38. package/workspace/public/pm/c3-report-setup.js +8 -3
  39. package/workspace/public/pm/c3-report-store.js +14 -1
  40. package/workspace/server.js +7 -2
  41. package/workspace/test-lists/c3-ini-life.json +9 -0
@@ -1,96 +1,144 @@
1
- /* pm/c3-ib-initiatives.js — the Initiatives tab of a board (mockup v5, 34-initiatives.js tabInitiatives / iniToolbar / iniRow).
1
+ /* pm/c3-ib-initiatives.js — the Initiatives tab of a board (mockup v5, 34-initiatives.js tabInitiatives / iniToolbar / iniRow; its header and
2
+ * lifecycle from mockup v2 of c3-initiative-lifecycle, approved 2026-10-13).
2
3
  * registerBoardTab('initiatives', (host, ctx) => ...) ctx is documented in the header of c3-initiatives.js.
3
- * What it draws, all of it paged on the server (GET /api/boards/backlog; q, view, cat, stage, status, owner, horizon, sort, offset, limit):
4
- * - a search box (debounced), the saved views as pills (Now and next, Mine, At risk, No owner, All) and New initiative;
5
- * - the selects Category, Stage, Status, Owner and Horizon, the Sort select and a direction toggle (sort=-key is descending). "Not done"
6
- * and "At risk or late" are comma lists (status=on track,at risk,late), which the server reads as any of;
7
- * - one console3 row per initiative: id, its workflow template, the stage dots of its own workflow, status, owner, progress, dates,
8
- * warnings; Move (a menu of its own stages, ctx.moveTo) and Open;
9
- * - a numbered pager of 25 ("26–50 of 312", Previous, 1 2 … 13, Next).
4
+ * What it draws, all of it paged on the server (GET /api/boards/backlog; q, view, cat, stage, status, owner, horizon, sort, offset, limit, archived):
5
+ * - one row of controls: a search box with an icon (debounced), the view menu (Now and next, Mine, At risk, No owner, All), a filter
6
+ * icon whose menu holds Status (grouped Active, Paused, Closed), Stage, Category, Owner and Horizon and whose badge counts the
7
+ * filters on, a sort icon whose menu holds the sort keys and the direction, and New initiative. "All active" and "At risk or late"
8
+ * are comma lists (status=on track,at risk,late), which the server reads as any of;
9
+ * - the filters on, as removable chips in the list header, and Clear;
10
+ * - one console3 row per initiative: id, its workflow template, the stage dots of its own workflow, status, then owner, progress,
11
+ * dates and warnings (or, when it is on hold or cancelled, since when, the check-back day and the reason), "Merged from", the
12
+ * duplicate hint; a "…" menu (Move to a stage, Status, Tidy up: c3-ini-life.js rowMenu) and Open;
13
+ * - a footer: the numbered pager of 25, "N cancelled hidden" (opens All with Status: Cancelled) and "N archived" (opens the archive);
14
+ * - the archive: a search box and Back to initiatives, each archived initiative with its reason or "Merged into <id>", Restore or
15
+ * Undo merge.
10
16
  * ctx.filter (from the URL or a Summary tile) opens the tab on that view and those filters. One request is in flight at a time; a change
11
- * made meanwhile is sent when it returns and the older answer is dropped. host.dataset.ibbState is loading | ready | error and
12
- * host.dataset.ibbSeq counts the answers drawn (the browser suite polls both). */
17
+ * made meanwhile is sent when it returns and the older answer is dropped. After a hold, cancel or resume the list reads again until the
18
+ * row shows it (the engine derives a lifecycle change in a child process). host.dataset.ibbState is loading | ready | error and
19
+ * host.dataset.ibbSeq counts the answers drawn (the browser suites poll both). */
13
20
  import { el } from './dom.js';
14
21
  import { registerBoardTab } from './c3-initiatives.js';
15
- import { icon, pill, lk, ib, pbtn, chip, st, note, lbl, sect, list, empty, nrow, statusChip, ownerChip, stepsMini, progChip, dateChip, daysBetween, plural, openMenu, sw, CATS, put } from './c3-ini-ui.js';
22
+ import { icon, lk, ib, pbtn, pill, chip, st, note, list, empty, nrow, sect, statusChip, ownerChip, stepsMini, progChip, dateChip, daysBetween, plural, openMenu, CATS, put } from './c3-ini-ui.js';
16
23
  import { wfOf, catOf, tplName } from './c3-ini-work.js';
24
+ import { rowMenu, lifeChips, archivedChips, dupChip, mergedFromChip, restoreBtn, rowIcon, ensureLifeCss } from './c3-ini-life.js';
17
25
 
18
26
  const PAGE = 25;
19
27
  const VIEWS = [['now', 'Now and next'], ['mine', 'Mine'], ['risk', 'At risk'], ['noowner', 'No owner'], ['all', 'All']];
20
- const STATUS = [['', 'Any'], ['on track,at risk,late', 'Not done'], ['on track', 'On track'], ['at risk,late', 'At risk or late'], ['at risk', 'At risk'], ['late', 'Late'], ['done', 'Done']];
28
+ const STATUS_GROUPS = [['', [['', 'Any']]],
29
+ ['Active', [['on track,at risk,late', 'All active'], ['on track', 'On track'], ['at risk,late', 'At risk or late'], ['at risk', 'At risk'], ['late', 'Late']]],
30
+ ['Paused', [['on hold', 'On hold']]],
31
+ ['Closed', [['done,cancelled', 'Done or cancelled'], ['done', 'Done'], ['cancelled', 'Cancelled']]]];
32
+ const STATUS = STATUS_GROUPS.flatMap((g) => g[1]);
21
33
  const HORIZON = [['', 'Any'], ['now,next', 'Now and next'], ['now', 'Now'], ['next', 'Next'], ['later', 'Later']];
22
34
  const SORTS = [['horizon', 'Horizon'], ['end', 'Ends'], ['title', 'Title'], ['stage', 'Stage'], ['progress', 'Progress'], ['status', 'Status'], ['owner', 'Owner'], ['id', 'Key']];
23
- const FKEYS = ['cat', 'stage', 'status', 'owner', 'horizon'];
35
+ const FKEYS = ['status', 'stage', 'cat', 'owner', 'horizon'];
36
+ const FNAME = { status: 'Status', stage: 'Stage', cat: 'Category', owner: 'Owner', horizon: 'Horizon' };
24
37
  const toneOf = (s) => (s === 'late' ? 'bad' : s === 'at risk' ? 'warn' : 'ok');
25
38
  const warnChip = (w) => st('warn', 'alert', w, { title: 'An order rule, shown as a warning. It never blocks.', 'data-ini-warning': w });
39
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
26
40
 
27
41
  registerBoardTab('initiatives', (host, ctx) => {
42
+ ensureLifeCss();
28
43
  const b = ctx.board;
29
44
  const f0 = ctx.filter || {};
30
45
  const sort0 = String(f0.sort || 'horizon');
31
46
  const Q = { q: String(f0.q || ''), view: VIEWS.some((v) => v[0] === f0.view) ? f0.view : 'now', cat: CATS.includes(f0.cat) ? f0.cat : '', stage: f0.stage || '', status: f0.status || '', owner: f0.owner || '', horizon: f0.horizon || '',
32
- sort: SORTS.some((s) => s[0] === sort0.replace(/^-/, '')) ? sort0.replace(/^-/, '') : 'horizon', dir: sort0.startsWith('-') ? -1 : 1, offset: Math.max(0, Number(f0.offset) || 0) };
47
+ sort: SORTS.some((s) => s[0] === sort0.replace(/^-/, '')) ? sort0.replace(/^-/, '') : 'horizon', dir: sort0.startsWith('-') ? -1 : 1, offset: Math.max(0, Number(f0.offset) || 0), archived: f0.archived === '1' };
33
48
  const S = { data: null, flying: false, again: false, dead: false, seq: 0, err: '' };
34
49
 
35
- const search = el('input', { type: 'text', 'data-ibb': 'q', 'aria-label': 'Search initiatives', placeholder: 'Search ' + b.name + ' initiatives', value: Q.q, autocomplete: 'off' });
50
+ const search = el('input', { type: 'text', 'data-ibb': 'q', 'aria-label': 'Search initiatives', value: Q.q, autocomplete: 'off' });
36
51
  let deb = null;
37
52
  search.addEventListener('input', () => { clearTimeout(deb); deb = setTimeout(() => { Q.q = search.value.trim(); Q.offset = 0; request(); }, 250); });
38
53
  search.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); clearTimeout(deb); Q.q = search.value.trim(); Q.offset = 0; request(); } });
39
- const views = el('div', { class: 'chips2', role: 'group', 'aria-label': 'Saved views', 'data-ibb': 'views' });
40
- const top = el('div', { class: 'calbar' }, el('div', { class: 'form ini-grow' }, search), views,
41
- pbtn('primary', 'New initiative', 'plus', { 'data-ini': 'new-initiative', 'aria-haspopup': 'dialog', onclick: () => ctx.newInitiative() }));
42
- const filters = el('div', { class: 'chips2', 'data-ibb': 'filters' });
43
- const count = el('div', { class: 'dim ini-note', 'data-ibb': 'count', 'aria-live': 'polite' });
54
+ const top = el('div', { class: 'calbar il-bar', 'data-ibb': 'toolbar' });
44
55
  const err = el('div', { class: 'fold w', role: 'alert', 'data-ibb': 'error', hidden: '' });
45
56
  const body = el('div', { 'data-ibb': 'body' });
46
- const pager = el('div', { class: 'chips2 ini-pager', 'data-ibb': 'pager' });
47
- put(host, el('div', { class: 'calbody', 'data-ibb': 'initiatives' }, top, filters, count, err, body, pager));
57
+ const foot = el('div', { class: 'il-listfoot', 'data-ibb': 'foot' });
58
+ put(host, el('div', { class: 'calbody', 'data-ibb': 'initiatives' }, top, err, body, foot));
48
59
  host.dataset.ibbState = 'loading'; host.dataset.ibbSeq = '0';
49
60
 
50
- const sel = (k, label, opts) => el('label', { class: 'chips2' }, lbl(label),
51
- el('select', { 'data-ibb-filter': k, 'aria-label': label, onchange: (e) => { Q[k] = e.target.value; Q.offset = 0; request(); } }, ...opts.map(([v, l]) => el('option', { value: v, selected: v === Q[k] }, l))));
61
+ const on = () => FKEYS.filter((k) => Q[k]);
62
+ const labelOf = (k, v) => (k === 'status' ? (STATUS.find((x) => x[0] === v) || [null, v])[1] : k === 'horizon' ? (HORIZON.find((x) => x[0] === v) || [null, v])[1] : k === 'owner' && v === 'none' ? 'No owner' : v);
63
+ const set = (patch) => { Object.assign(Q, patch, { offset: 0 }); request(); };
64
+ function clearFilters() { for (const k of FKEYS) Q[k] = ''; Q.q = ''; search.value = ''; Q.offset = 0; request(); }
65
+
52
66
  function drawControls() {
53
- put(views, ...VIEWS.map(([k, l]) => pill({ 'data-ibb-view': k, 'aria-pressed': String(Q.view === k), onclick: () => { Q.view = k; Q.offset = 0; request(); } }, l)));
67
+ search.placeholder = Q.archived ? 'Search the ' + b.name + ' archive' : 'Search ' + b.name + ' initiatives';
68
+ search.setAttribute('aria-label', Q.archived ? 'Search the archive' : 'Search initiatives');
69
+ const box = el('div', { class: 'form il-search' }, icon('search', 'sm'), search);
70
+ if (Q.archived) {
71
+ put(top, box, pbtn('outline', [el('span', { class: 'il-back' }, icon('next', 'sm')), 'Back to initiatives'], null, { 'data-ibb': 'back', onclick: () => set({ archived: false }) }));
72
+ return;
73
+ }
74
+ const n = on().length;
75
+ const view = (VIEWS.find((v) => v[0] === Q.view) || VIEWS[0])[1];
76
+ const sl = (SORTS.find((s) => s[0] === Q.sort) || SORTS[0])[1];
77
+ put(top, box,
78
+ pbtn('ghost', [view, el('span', { class: 'il-chev' }, icon('next', 'sm'))], null, { 'data-ibb': 'views', 'aria-haspopup': 'menu', 'aria-expanded': 'false', title: 'Saved views', onclick: (e) => viewsMenu(e.currentTarget) }),
79
+ ib(n ? 'Filters, ' + n + ' on' : 'Filters', [icon('filter', 'sm'), n ? el('span', { class: 'bdg' }, String(n)) : null], { class: 'il-ib', 'data-ibb': 'filters', 'aria-haspopup': 'dialog', 'aria-expanded': 'false', title: 'Filters', onclick: (e) => filterMenu(e.currentTarget) }),
80
+ ib('Sort: ' + sl + ', ' + (Q.dir > 0 ? 'ascending' : 'descending'), el('span', { class: Q.dir > 0 ? 'il-up' : 'il-down' }, icon('next', 'sm')), { 'data-ibb': 'sort', 'aria-haspopup': 'menu', 'aria-expanded': 'false', onclick: (e) => sortMenu(e.currentTarget) }),
81
+ pbtn('primary', 'New initiative', 'plus', { 'data-ini': 'new-initiative', 'aria-haspopup': 'dialog', onclick: () => ctx.newInitiative() }));
82
+ }
83
+ const tick = () => el('span', { class: 'il-tick' }, icon('check', 'sm'));
84
+ function viewsMenu(anchor) {
85
+ openMenu(anchor, { label: 'Saved views', items: [{ group: 'Views' }, ...VIEWS.map(([k, l]) => ({ kids: [tick(), l], checked: Q.view === k, attrs: { 'data-ibb-view': k }, onPick: () => set({ view: k }) }))] });
86
+ }
87
+ function sortMenu(anchor) {
88
+ openMenu(anchor, { label: 'Sort', items: [{ group: 'Sort by' }, ...SORTS.map(([k, l]) => ({ kids: [tick(), l], checked: Q.sort === k, attrs: { 'data-ibb-sort': k }, onPick: () => set({ sort: k }) })),
89
+ { group: 'Order' }, { kids: [el('span', { class: Q.dir > 0 ? 'il-up' : 'il-down' }, icon('next', 'sm')), Q.dir > 0 ? 'Ascending, press to flip' : 'Descending, press to flip'], attrs: { 'data-ibb': 'dir' }, onPick: () => set({ dir: -Q.dir }) }] });
90
+ }
91
+ function filterMenu(anchor) {
54
92
  const fa = (S.data && S.data.facets) || {};
55
93
  const keep = (k, keys) => (Q[k] && !keys.includes(Q[k]) ? [Q[k], ...keys] : keys);
56
94
  const order = (b.workflow || []).map((s) => s.name);
57
95
  const stages = keep('stage', Object.keys(fa.stage || {}).sort((x, y) => ((order.indexOf(x) + 1 || 99) - (order.indexOf(y) + 1 || 99)) || x.localeCompare(y)));
58
96
  const owners = keep('owner', Object.keys(fa.owner || {}).filter((o) => o !== 'none').sort((x, y) => (x === 'You' ? -1 : y === 'You' ? 1 : x.localeCompare(y))));
59
97
  const n = (k, v) => (fa[k] && fa[k][v] != null ? ' (' + fa[k][v] + ')' : '');
60
- put(filters,
61
- sel('cat', 'Category', [['', 'Any'], ...CATS.map((c) => [c, c + n('cat', c)])]),
62
- sel('stage', 'Stage', [['', 'Any'], ...stages.map((s) => [s, s + n('stage', s)])]),
63
- sel('status', 'Status', STATUS),
64
- sel('owner', 'Owner', [['', 'Anyone'], ['none', 'No owner' + n('owner', 'none')], ...owners.map((o) => [o, o + n('owner', o)])]),
65
- sel('horizon', 'Horizon', HORIZON),
66
- el('label', { class: 'chips2' }, lbl('Sort'), el('select', { 'data-ibb': 'sort', 'aria-label': 'Sort by', onchange: (e) => { Q.sort = e.target.value; Q.offset = 0; request(); } }, ...SORTS.map(([v, l]) => el('option', { value: v, selected: v === Q.sort }, l)))),
67
- ib(Q.dir > 0 ? 'Ascending, press to flip' : 'Descending, press to flip', el('span', { class: Q.dir > 0 ? 'ini-up' : 'ini-down' }, icon('next', 'sm')), { 'data-ibb': 'dir', onclick: () => { Q.dir = -Q.dir; Q.offset = 0; request(); } }));
98
+ const opt = (k, [v, l]) => el('option', { value: v, selected: v === Q[k] }, l);
99
+ const sel = (k, label, kids) => el('label', { class: 'il-f' }, el('span', null, label), el('select', { 'data-ibb-filter': k, 'aria-label': label, onchange: (e) => { Q[k] = e.target.value; Q.offset = 0; request(); } }, ...kids));
100
+ const panel = el('div', { 'data-ibb': 'filter-panel' }, el('div', { class: 'ini-mlbl' }, 'Filter initiatives'),
101
+ sel('status', 'Status', STATUS_GROUPS.map(([g, opts]) => (g ? el('optgroup', { label: g }, ...opts.map((o) => opt('status', o))) : opts.map((o) => opt('status', o))))),
102
+ sel('stage', 'Stage', [['', 'Any'], ...stages.map((s) => [s, s + n('stage', s)])].map((o) => opt('stage', o))),
103
+ sel('cat', 'Category', [['', 'Any'], ...CATS.map((c) => [c, c + n('cat', c)])].map((o) => opt('cat', o))),
104
+ sel('owner', 'Owner', [['', 'Anyone'], ['none', 'No owner' + n('owner', 'none')], ...owners.map((o) => [o, o + n('owner', o)])].map((o) => opt('owner', o))),
105
+ sel('horizon', 'Horizon', HORIZON.map((o) => opt('horizon', o))),
106
+ el('div', { class: 'chips2 il-fend' }, lk({ 'data-ibb': 'clear-in-menu', onclick: () => { clearFilters(); anchor.focus(); } }, 'Clear filters')));
107
+ openMenu(anchor, { label: 'Filters', body: panel });
68
108
  }
69
- const active = () => FKEYS.some((k) => Q[k]) || Q.q;
70
- function clearFilters() { for (const k of FKEYS) Q[k] = ''; Q.q = ''; search.value = ''; Q.offset = 0; request(); }
71
-
72
- function moveMenu(anchor, r) {
73
- const wf = wfOf(r, b);
74
- openMenu(anchor, { label: 'Move ' + r.id + ' to a stage', items: [{ group: 'Move ' + r.id + ' to' }, ...wf.map((s) => ({ kids: [sw(s.cat), s.name], current: s.name === r.stage, disabled: s.name === r.stage, attrs: { 'data-ini-moveto': s.name },
75
- onPick: async () => { const m = await ctx.moveTo(r.id, s.name); if (m && m.ok) request(); } }))] });
109
+ function chipsOf() {
110
+ const ks = on();
111
+ if (!ks.length) return null;
112
+ return el('div', { class: 'chips2 il-chips', 'data-ibb': 'chips' },
113
+ ...ks.map((k) => lk({ 'data-ibb-unset': k, title: 'Remove this filter', onclick: () => set({ [k]: '' }) }, FNAME[k] + ': ' + labelOf(k, Q[k]) + ' ', icon('x', 'sm'))),
114
+ lk({ 'data-ibb': 'clear', onclick: clearFilters }, 'Clear'));
76
115
  }
116
+
117
+ const L = { ctx, done: (change) => settle(change) };
77
118
  function rowOf(r) {
119
+ if (r.archived) {
120
+ return nrow({ ic: rowIcon(r), title: r.title || r.id, onOpen: () => ctx.openInitiative(r.id), openLabel: 'Open ' + r.id + ' ' + (r.title || ''), titleAttrs: { 'data-ibb-open': r.id },
121
+ chips: archivedChips(r).flat(), right: [restoreBtn(L, r)], attrs: { 'data-ibb-row': r.id, 'data-il-archived': r.archived.into ? 'merged' : 'archived' } });
122
+ }
78
123
  const wf = wfOf(r, b);
79
- const late = r.status !== 'done' && r.end && daysBetween(ctx.today, r.end) < 0;
80
- return nrow({ ic: r.status === 'late' || r.status === 'at risk' ? 'alert' : r.status === 'done' ? 'check' : 'init', hot: r.status === 'late' || r.status === 'at risk', title: r.title || r.id,
124
+ const stopped = r.status === 'on hold' || r.status === 'cancelled';
125
+ const late = !stopped && r.status !== 'done' && r.end && daysBetween(ctx.today, r.end) < 0;
126
+ return nrow({ ic: rowIcon(r), hot: r.status === 'late' || r.status === 'at risk', title: r.title || r.id,
81
127
  onOpen: () => ctx.openInitiative(r.id), openLabel: 'Open ' + r.id + ' ' + (r.title || ''), titleAttrs: { 'data-ibb-open': r.id },
82
- chips: [el('span', { class: 'chip2 mono' }, r.id), el('span', { class: 'tag', title: 'Workflow' }, tplName(r, b)), stepsMini(wf, r.stage, tplName(r, b)), statusChip(r.status), ownerChip(r.owner),
83
- progChip(r.progress || 0, toneOf(r.status)), dateChip(r.start, r.end, late), r.anchor ? null : st('warn', 'alert', 'No anchor'), ...(r.warnings || []).map(warnChip)],
84
- right: [ib('Move ' + r.id + ' to another stage', 'more', { 'data-ibb-move': r.id, 'aria-haspopup': 'menu', 'aria-expanded': 'false', disabled: !!b.self, onclick: (e) => moveMenu(e.currentTarget, r) }),
85
- ib('Open ' + r.id, 'next', { 'data-ini-open': r.id, onclick: () => ctx.openInitiative(r.id) })],
86
- attrs: { 'data-ibb-row': r.id, 'data-cat': r.cat || catOf(wf, r.stage) } });
128
+ chips: [el('span', { class: 'chip2 mono' }, r.id), el('span', { class: 'tag', title: 'Workflow' }, tplName(r, b)), stepsMini(wf, r.stage, tplName(r, b)), statusChip(r.status),
129
+ ...(stopped ? lifeChips(r) : [ownerChip(r.owner), progChip(r.progress || 0, toneOf(r.status)), dateChip(r.start, r.end, late), b.build ? chip(icon('bolt', 'sm'), plural((r.epics || []).length, 'epic')) : null, r.anchor ? null : st('warn', 'alert', 'No anchor'), ...(r.warnings || []).map(warnChip)]),
130
+ mergedFromChip(r), dupChip(L, r)],
131
+ right: [ib('Actions for ' + r.id, 'more', { 'data-ibb-move': r.id, 'data-ibb-menu': r.id, 'aria-haspopup': 'menu', 'aria-expanded': 'false', title: 'Move, pause, merge or archive',
132
+ onclick: (e) => rowMenu(e.currentTarget, L, r, b.self ? null : async (stage) => { const m = await ctx.moveTo(r.id, stage); if (m && m.ok) request(); }) }),
133
+ ib('Open ' + r.id, 'next', { 'data-ini-open': r.id, onclick: () => ctx.openInitiative(r.id) })],
134
+ attrs: { 'data-ibb-row': r.id, 'data-cat': r.cat || catOf(wf, r.stage), 'data-status': r.status } });
87
135
  }
88
- function drawPager(d) {
136
+ function pagerOf(d) {
89
137
  const pages = Math.max(1, Math.ceil(d.total / PAGE)); const cur = Math.floor(d.offset / PAGE);
90
- if (pages < 2) { pager.replaceChildren(); return; }
138
+ if (pages < 2) return null;
91
139
  const nums = []; for (let p = 0; p < pages; p++) if (p < 2 || p >= pages - 1 || Math.abs(p - cur) <= 1) nums.push(p); else if (nums[nums.length - 1] !== '…') nums.push('…');
92
140
  const to = (p) => { Q.offset = p * PAGE; request(true); };
93
- put(pager, el('span', { class: 'dim mono ini-meta' }, (d.offset + 1) + '–' + Math.min(d.total, d.offset + d.rows.length) + ' of ' + d.total),
141
+ return el('div', { class: 'chips2 ini-pager', 'data-ibb': 'pager' }, el('span', { class: 'dim mono ini-meta' }, (d.offset + 1) + '–' + Math.min(d.total, d.offset + d.rows.length) + ' of ' + d.total),
94
142
  pill({ 'data-ibb': 'prev', disabled: cur === 0, onclick: () => to(cur - 1) }, 'Previous'),
95
143
  ...nums.map((p) => (p === '…' ? el('span', { class: 'dim' }, '…') : pill({ 'data-ibb-page': String(p + 1), 'aria-pressed': String(p === cur), 'aria-label': 'Page ' + (p + 1), onclick: () => to(p) }, String(p + 1)))),
96
144
  pill({ 'data-ibb': 'next', disabled: cur >= pages - 1, onclick: () => to(cur + 1) }, 'Next'));
@@ -100,11 +148,18 @@ registerBoardTab('initiatives', (host, ctx) => {
100
148
  drawControls();
101
149
  const d = S.data;
102
150
  if (!d) return;
103
- const label = (VIEWS.find((v) => v[0] === Q.view) || [])[1];
104
- put(count, plural(d.total, 'initiative') + ' · ' + label + (active() ? ' · ' : ''), active() ? lk({ 'data-ibb': 'clear', onclick: clearFilters }, 'Clear filters') : '', ' · each row shows its own workflow and stage');
105
- put(body, sect({ ic: 'init', title: 'Initiatives', count: d.total },
106
- d.rows.length ? list(d.rows.map(rowOf), { 'data-ibb': 'rows' }) : empty(active() || Q.view !== 'all' ? 'No initiatives match. Clear the filters or pick All.' : b.name + ' has no initiatives yet. Press New initiative, or accept an ask in Triage.', { 'data-ibb': 'empty' })));
107
- drawPager(d);
151
+ if (Q.archived) {
152
+ put(body, sect({ ic: 'clip', title: 'Archive', count: d.total, right: el('span', { class: 'dim il-hint' }, 'Kept whole. Restore puts it back as it was.'), attrs: { 'data-ibb': 'archive' } },
153
+ d.rows.length ? list(d.rows.map(rowOf), { 'data-ibb': 'rows' }) : empty(Q.q ? 'Nothing in the archive matches.' : 'Nothing archived on this board.', { 'data-ibb': 'empty' })));
154
+ put(foot, pagerOf(d));
155
+ } else {
156
+ const filtered = on().length || Q.q;
157
+ put(body, sect({ ic: 'init', title: 'Initiatives', count: d.total, right: chipsOf() },
158
+ d.rows.length ? list(d.rows.map(rowOf), { 'data-ibb': 'rows' }) : empty(filtered || Q.view !== 'all' ? 'No initiatives match. Clear the filters or pick All.' : b.name + ' has no initiatives yet. Press New initiative, or accept an ask in Triage.', { 'data-ibb': 'empty' })));
159
+ put(foot, pagerOf(d), d.cancelledHidden || d.archived ? el('span', { class: 'chips2' },
160
+ d.cancelledHidden ? lk({ 'data-ibb': 'show-cancelled', title: 'Cancelled initiatives are hidden from every view', onclick: () => set({ view: 'all', status: 'cancelled' }) }, icon('x', 'sm'), d.cancelledHidden + ' cancelled hidden') : null,
161
+ d.archived ? lk({ 'data-ibb': 'show-archive', onclick: () => set({ archived: true }) }, icon('clip', 'sm'), d.archived + ' archived') : null) : null);
162
+ }
108
163
  host.dataset.ibbSeq = String(++S.seq);
109
164
  }
110
165
 
@@ -116,16 +171,28 @@ registerBoardTab('initiatives', (host, ctx) => {
116
171
  for (;;) {
117
172
  S.again = false;
118
173
  const q = { ...Q };
119
- r = await ctx.api.get('/api/boards/backlog', { board: b.id, q: q.q, view: q.view, cat: q.cat, stage: q.stage, status: q.status, owner: q.owner, horizon: q.horizon, sort: (q.dir < 0 ? '-' : '') + q.sort, offset: q.offset, limit: PAGE });
120
- if (S.dead) return;
174
+ r = await ctx.api.get('/api/boards/backlog', { board: b.id, q: q.q, view: q.view, cat: q.cat, stage: q.stage, status: q.status, owner: q.owner, horizon: q.horizon, sort: (q.dir < 0 ? '-' : '') + q.sort, offset: q.offset, limit: PAGE, archived: q.archived ? '1' : '' });
175
+ if (S.dead) return null;
121
176
  if (!S.again) break;
122
177
  }
123
178
  S.flying = false; host.removeAttribute('aria-busy');
124
- if (!r || !r.ok || r.pending) { S.err = r && r.pending ? 'The repository is still being read. Try again in a moment.' : 'The initiatives could not be read: ' + ((r && r.error) || 'no answer') + '.'; host.dataset.ibbState = 'error'; draw(); return; }
125
- S.err = ''; S.data = { total: r.total || 0, offset: r.offset || 0, limit: r.limit || PAGE, rows: r.rows || [], facets: r.facets || {} };
179
+ if (!r || !r.ok || r.pending) { S.err = r && r.pending ? 'The repository is still being read. Try again in a moment.' : 'The initiatives could not be read: ' + ((r && r.error) || 'no answer') + '.'; host.dataset.ibbState = 'error'; draw(); return null; }
180
+ S.err = ''; S.data = { total: r.total || 0, offset: r.offset || 0, limit: r.limit || PAGE, rows: r.rows || [], facets: r.facets || {}, cancelledHidden: r.cancelledHidden || 0, archived: r.archived || 0 };
126
181
  Q.offset = S.data.offset;
127
182
  draw(); host.dataset.ibbState = 'ready';
128
- if (keepFocus) { const f = pager.querySelector('[aria-pressed="true"]'); if (f) f.focus(); }
183
+ if (keepFocus) { const f = foot.querySelector('[aria-pressed="true"]'); if (f) f.focus(); }
184
+ return S.data;
185
+ }
186
+ /* After a write: read again. A lifecycle line is derived in a child process, so the list may show the old status for a moment;
187
+ * read again until the row shows the new one, or it left the list (a paused initiative leaves Now and next), up to ten times. */
188
+ async function settle(change) {
189
+ for (let k = 0; k < 10 && !S.dead; k++) {
190
+ const d = await request();
191
+ if (!d || !change || change.kind !== 'status') return;
192
+ const row = d.rows.find((x) => x.id === change.id);
193
+ if (change.status ? (!row || row.status === change.status) : (!row || (row.status !== 'on hold' && row.status !== 'cancelled'))) return;
194
+ await sleep(800);
195
+ }
129
196
  }
130
197
 
131
198
  drawControls();
@@ -76,7 +76,7 @@ function mountItems(tab) {
76
76
 
77
77
  /* ── Backlog ── */
78
78
  function addForm() {
79
- const inis = R.initiatives.filter((i) => i.status !== 'done').sort((a, c) => ((c.featured ? 1 : 0) - (a.featured ? 1 : 0)) || a.id.localeCompare(c.id)).slice(0, 60);
79
+ const inis = R.initiatives.filter((i) => i.status !== 'done' && i.status !== 'cancelled').sort((a, c) => ((c.featured ? 1 : 0) - (a.featured ? 1 : 0)) || a.id.localeCompare(c.id)).slice(0, 60);
80
80
  const F = { ini: st.ini && inis.some((i) => i.id === st.ini) ? st.ini : (inis[0] || {}).id || '', busy: false };
81
81
  const input = el('input', { type: 'text', placeholder: 'Add a PMOS task', 'aria-label': 'Add a PMOS task', 'data-wk': 'add-title' });
82
82
  const msg = el('div', { class: 'fold w', role: 'alert', hidden: '', 'data-wk': 'add-error' });
@@ -163,7 +163,7 @@ function mountItems(tab) {
163
163
  let hidden = 0;
164
164
  if (!st.ini) {
165
165
  const pick = inis.filter((i) => withItems.has(i.id)).sort((a, c) => ((c.featured ? 1 : 0) - (a.featured ? 1 : 0)) || String(a.start || '9').localeCompare(String(c.start || '9')));
166
- const rest = (st.src || st.q || st.kind) ? [] : inis.filter((i) => !withItems.has(i.id) && i.status !== 'done' && i.start && i.end && (i.horizon === 'now' || i.featured)).sort((a, c) => String(a.start).localeCompare(String(c.start)));
166
+ const rest = (st.src || st.q || st.kind) ? [] : inis.filter((i) => !withItems.has(i.id) && ['on track', 'at risk', 'late'].includes(i.status) && i.start && i.end && (i.horizon === 'now' || i.featured)).sort((a, c) => String(a.start).localeCompare(String(c.start)));
167
167
  const cap = st.tlAll ? 1e9 : TL_CAP; inis = [...pick, ...rest].slice(0, Math.max(cap, pick.length));
168
168
  hidden = pick.length + rest.length - inis.length;
169
169
  }
@@ -0,0 +1,32 @@
1
+ /* pm/c3-ini-life.css — the board Initiatives header in two rows and the lifecycle sheets (c3-initiative-lifecycle; mockup v2,
2
+ * 34-initiatives.css "lifecycle + quieter Initiatives header"). Linked once by c3-ini-life.js. Every rule is under an `il-` class. */
3
+ .c3 .il-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
4
+ .c3 .il-search{position:relative;flex:1 1 260px}
5
+ .c3 .il-search > .ic{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--muted-foreground);pointer-events:none}
6
+ .c3 .il-search input[type="text"]{padding-left:30px}
7
+ .c3 .il-chev{display:inline-flex;transform:rotate(90deg);margin-left:2px;color:var(--muted-foreground)}
8
+ .c3 .il-back{display:inline-flex;transform:rotate(180deg)}
9
+ .c3 .il-ib{position:relative}
10
+ .c3 .il-ib .bdg{position:absolute;top:-6px;right:-6px}
11
+ .c3 .il-up,.c3 .il-down{display:inline-flex}
12
+ .c3 .il-up{transform:rotate(-90deg)}
13
+ .c3 .il-down{transform:rotate(90deg)}
14
+ .c3 .il-chips{justify-content:flex-end}
15
+ .c3 .il-foot{justify-content:flex-end;margin-top:4px}
16
+ .c3 .il-listfoot{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}
17
+ .c3 .il-listfoot > :only-child{margin-left:auto}
18
+ .c3 .il-hint{font-size:var(--fs-2)}
19
+ .c3 .il-sheet{display:grid;gap:14px}
20
+ .c3 .il-sheet textarea{width:100%;resize:vertical}
21
+ .c3 .il-move{gap:8px;align-items:flex-start}
22
+ .c3 .il-banner{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
23
+ .c3 .il-banner > span:first-of-type{flex:1 1 240px}
24
+ .c3 .ini-mpanel .il-f{display:grid;grid-template-columns:76px 1fr;align-items:center;gap:8px;padding:4px 8px;font-size:var(--fs-2);color:var(--muted-foreground)}
25
+ .c3 .ini-mpanel .il-f select{width:100%}
26
+ .c3 .ini-mpanel .il-fend{justify-content:flex-end;padding:6px 8px 2px}
27
+ .c3 .ini-mi .il-hint{color:var(--muted-foreground)}
28
+ .c3 .ini-mi[aria-checked="false"] .il-tick{visibility:hidden}
29
+ /* The board's Initiatives list shows every chip of a row, as the mockup draws it (the quiet pages keep two elsewhere). */
30
+ .c3 [data-ibb="rows"] .nrow .nb > .chips > :nth-child(n+3){display:inline-flex !important}
31
+ .c3 [data-ibb="rows"] .nrow .nb > .chips > .qt-hide{display:none !important}
32
+ .c3 .il-note{font-size:var(--fs-2);line-height:1.45}