create-pmos 0.134.0 → 0.136.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 (211) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/payload/state/log.md +1 -1
  7. package/workspace/bin/notes-replay.js +235 -0
  8. package/workspace/c3-briefing-test.js +130 -0
  9. package/workspace/c3-foundation-test.js +159 -0
  10. package/workspace/c3-ib-items-test.js +105 -0
  11. package/workspace/c3-ib-more-test.js +120 -0
  12. package/workspace/c3-ib-triage-backlog-test.js +119 -0
  13. package/workspace/c3-ini-harness.js +66 -0
  14. package/workspace/c3-initiatives-test.js +140 -0
  15. package/workspace/c3-one-home-test.js +14 -1
  16. package/workspace/c3-reports-test.js +57 -18
  17. package/workspace/c3-settings-test.js +139 -0
  18. package/workspace/c3-work-test.js +42 -10
  19. package/workspace/fixtures/one-home-baseline.json +1 -9
  20. package/workspace/guide/briefing-cards.md +15 -3
  21. package/workspace/guide/briefing.md +12 -8
  22. package/workspace/guide/calendar-popup.md +2 -2
  23. package/workspace/guide/console-calendar.md +39 -10
  24. package/workspace/guide/index.md +1 -0
  25. package/workspace/guide/initiative-boards.md +65 -0
  26. package/workspace/guide/jira.md +3 -1
  27. package/workspace/guide/layout.md +2 -0
  28. package/workspace/guide/my-work.md +5 -3
  29. package/workspace/guide/notes.md +16 -1
  30. package/workspace/guide/reports-dashboards.md +29 -5
  31. package/workspace/guide/settings.md +68 -0
  32. package/workspace/guide/welcome.md +10 -10
  33. package/workspace/lib/calendar-cli.js +3 -2
  34. package/workspace/lib/calendar-layers.js +21 -10
  35. package/workspace/lib/calendar-tidy.js +230 -0
  36. package/workspace/lib/engine/boards.js +542 -0
  37. package/workspace/lib/engine/breakdown-ops.js +180 -0
  38. package/workspace/lib/engine/calendar-import.js +84 -0
  39. package/workspace/lib/engine/calendar-model.js +185 -0
  40. package/workspace/lib/engine/card-state.js +92 -0
  41. package/workspace/lib/engine/derived/derive.js +92 -0
  42. package/workspace/lib/engine/derived/plan.js +3 -0
  43. package/workspace/lib/engine/derived/trust.js +3 -0
  44. package/workspace/lib/engine/doc-health.js +142 -0
  45. package/workspace/lib/engine/doc-sheet.js +75 -0
  46. package/workspace/lib/engine/intake.js +78 -0
  47. package/workspace/lib/engine/outbox.js +86 -0
  48. package/workspace/lib/engine/plan-forecast.js +342 -0
  49. package/workspace/lib/engine/promises.js +29 -0
  50. package/workspace/lib/engine/records/index.js +114 -0
  51. package/workspace/lib/engine/releases.js +96 -0
  52. package/workspace/lib/engine/replies.js +99 -0
  53. package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
  54. package/workspace/lib/engine/report-families.js +175 -0
  55. package/workspace/lib/engine/roadmap-ops.js +150 -0
  56. package/workspace/lib/engine/sessions-list.js +27 -0
  57. package/workspace/lib/engine/signal-rules.js +95 -0
  58. package/workspace/lib/engine/signals/cards.js +57 -0
  59. package/workspace/lib/engine/signals/detectors.js +370 -0
  60. package/workspace/lib/engine/signals/facts-cli.js +8 -0
  61. package/workspace/lib/engine/signals/facts.js +197 -0
  62. package/workspace/lib/engine/signals/index.js +33 -0
  63. package/workspace/lib/engine/signals/kinds.js +110 -0
  64. package/workspace/lib/engine/signals/layout-paths.js +44 -0
  65. package/workspace/lib/engine/signals/options.js +24 -0
  66. package/workspace/lib/engine/signals/score.js +72 -0
  67. package/workspace/lib/engine/signals/select.js +47 -0
  68. package/workspace/lib/engine/step-planner.js +65 -0
  69. package/workspace/lib/engine/trust/claim-rules.json +93 -0
  70. package/workspace/lib/engine/trust/import-calendar.js +21 -0
  71. package/workspace/lib/engine/trust/import-guard.js +36 -0
  72. package/workspace/lib/engine/trust/ledger.js +53 -0
  73. package/workspace/lib/engine/trust/outbox.js +45 -0
  74. package/workspace/lib/engine/trust/plan.js +5 -0
  75. package/workspace/lib/engine/trust/trust.js +21 -0
  76. package/workspace/lib/engine/truth-table.js +57 -0
  77. package/workspace/lib/engine/workspace-lib.js +24 -0
  78. package/workspace/lib/factory-join.js +1 -1
  79. package/workspace/lib/facts-async.js +4 -4
  80. package/workspace/lib/headless.js +95 -0
  81. package/workspace/lib/import-overlay.js +2 -1
  82. package/workspace/lib/jira-status.js +30 -3
  83. package/workspace/lib/notes-capture.js +9 -1
  84. package/workspace/lib/notes-final.js +38 -13
  85. package/workspace/lib/notes-replay.js +125 -0
  86. package/workspace/lib/notes-screen.js +180 -0
  87. package/workspace/lib/notes-whisper.js +10 -3
  88. package/workspace/lib/outbox-store.js +10 -6
  89. package/workspace/lib/pmos-verbs.js +3 -1
  90. package/workspace/lib/proactive-cli.js +7 -0
  91. package/workspace/lib/proactive.js +15 -2
  92. package/workspace/lib/records-dir.js +29 -0
  93. package/workspace/lib/repo-script.js +36 -0
  94. package/workspace/lib/report-dashboards.js +5 -2
  95. package/workspace/lib/report-frame.js +122 -0
  96. package/workspace/lib/report-schedules.js +113 -0
  97. package/workspace/lib/route-modules.js +9 -2
  98. package/workspace/lib/routes-boards.js +734 -0
  99. package/workspace/lib/routes-brief-page.js +13 -4
  100. package/workspace/lib/routes-briefing.js +22 -11
  101. package/workspace/lib/routes-c3-init.js +9 -8
  102. package/workspace/lib/routes-c3-life.js +2 -1
  103. package/workspace/lib/routes-c3-reports.js +99 -15
  104. package/workspace/lib/routes-c3-strategy.js +7 -5
  105. package/workspace/lib/routes-c3-timeline.js +3 -2
  106. package/workspace/lib/routes-calendar-import.js +3 -2
  107. package/workspace/lib/routes-calendar-views.js +9 -4
  108. package/workspace/lib/routes-doc-health.js +2 -1
  109. package/workspace/lib/routes-doc-sheet.js +2 -1
  110. package/workspace/lib/routes-forecast.js +2 -1
  111. package/workspace/lib/routes-hooks.js +3 -2
  112. package/workspace/lib/routes-initiatives.js +2 -1
  113. package/workspace/lib/routes-intake.js +2 -1
  114. package/workspace/lib/routes-notes.js +55 -9
  115. package/workspace/lib/routes-outbox.js +23 -3
  116. package/workspace/lib/routes-pm-items.js +19 -6
  117. package/workspace/lib/routes-records.js +10 -7
  118. package/workspace/lib/routes-releases.js +2 -1
  119. package/workspace/lib/routes-report-facts.js +4 -3
  120. package/workspace/lib/routes-report-families.js +2 -1
  121. package/workspace/lib/routes-roadmap-ops.js +17 -14
  122. package/workspace/lib/routes-runs.js +6 -5
  123. package/workspace/lib/routes-settings.js +51 -0
  124. package/workspace/lib/routes-signals.js +3 -2
  125. package/workspace/lib/routes-stage-tools.js +220 -0
  126. package/workspace/lib/run-answers.js +2 -1
  127. package/workspace/lib/settings-store.js +226 -0
  128. package/workspace/lib/stage-tools.js +208 -0
  129. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  130. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  131. package/workspace/lib/work-evidence.js +73 -0
  132. package/workspace/package-lock.json +2 -2
  133. package/workspace/package.json +1 -1
  134. package/workspace/public/pm/briefing-deck.js +63 -17
  135. package/workspace/public/pm/briefing-model.js +36 -7
  136. package/workspace/public/pm/c3-ambient.js +100 -0
  137. package/workspace/public/pm/c3-brief-needs.js +26 -6
  138. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  139. package/workspace/public/pm/c3-brief-today.js +56 -21
  140. package/workspace/public/pm/c3-brief.js +2 -1
  141. package/workspace/public/pm/c3-calendar.js +161 -42
  142. package/workspace/public/pm/c3-calpop.js +5 -5
  143. package/workspace/public/pm/c3-demo-boards.js +649 -0
  144. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  145. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  146. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  147. package/workspace/public/pm/c3-demo-data.js +17 -27
  148. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  149. package/workspace/public/pm/c3-demo.js +2 -1
  150. package/workspace/public/pm/c3-dock.js +1 -1
  151. package/workspace/public/pm/c3-ib-docs.js +80 -0
  152. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  153. package/workspace/public/pm/c3-ib-items.js +201 -0
  154. package/workspace/public/pm/c3-ib-releases.js +51 -0
  155. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  156. package/workspace/public/pm/c3-ib-triage.js +178 -0
  157. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  158. package/workspace/public/pm/c3-ini-records.js +197 -0
  159. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  160. package/workspace/public/pm/c3-ini-ui.js +229 -0
  161. package/workspace/public/pm/c3-ini-work.js +139 -0
  162. package/workspace/public/pm/c3-initiatives.css +218 -0
  163. package/workspace/public/pm/c3-initiatives.js +741 -0
  164. package/workspace/public/pm/c3-plan.js +2 -2
  165. package/workspace/public/pm/c3-reports.css +47 -47
  166. package/workspace/public/pm/c3-reports.js +223 -185
  167. package/workspace/public/pm/c3-settings.css +74 -0
  168. package/workspace/public/pm/c3-settings.js +703 -0
  169. package/workspace/public/pm/c3-shell.js +102 -36
  170. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  171. package/workspace/public/pm/c3-terminal.js +2 -2
  172. package/workspace/public/pm/calendar-views.js +1 -1
  173. package/workspace/public/pm/components.css +319 -213
  174. package/workspace/public/pm/console3.css +99 -23
  175. package/workspace/public/pm/dest-notes.js +26 -4
  176. package/workspace/public/pm/notes-screen-capture.js +165 -0
  177. package/workspace/public/pm/notes-screen-view.js +98 -0
  178. package/workspace/public/pm/panel-trees.js +5 -2
  179. package/workspace/public/pm/pm-work-view.js +233 -146
  180. package/workspace/public/pm/shell-bell.js +3 -2
  181. package/workspace/public/pm/shell-recording.js +29 -8
  182. package/workspace/public/pm/tokens.css +2 -2
  183. package/workspace/public/pm/view-notes.js +46 -9
  184. package/workspace/public/pm/welcome.js +197 -89
  185. package/workspace/public/welcome.html +6 -4
  186. package/workspace/server.js +105 -6
  187. package/workspace/skills/day-check.skill +1 -1
  188. package/workspace/skills/jira-sync.skill +6 -4
  189. package/workspace/skills/morning-brief.skill +1 -1
  190. package/workspace/test-files.json +12 -3
  191. package/workspace/test-lists/c3-ib-items.json +9 -0
  192. package/workspace/test-lists/c3-ib-more.json +9 -0
  193. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  194. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  195. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  196. package/workspace/test-lists/console3-briefing.json +9 -0
  197. package/workspace/test-lists/console3-foundation.json +9 -0
  198. package/workspace/test-lists/console3-reports.json +3 -3
  199. package/workspace/test-lists/meeting-notes.json +10 -4
  200. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  201. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  202. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  203. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  204. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  205. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  206. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  207. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  208. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  209. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  210. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  211. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -0,0 +1,201 @@
1
+ /* pm/c3-ib-items.js — the Backlog, Board and Timeline tabs of a board (mockup v5, 34-initiatives.js tabItems / backlogBody / kanbanBody /
2
+ * timelineBody). The three show the same work items, Jira items and PMOS tasks mixed (GET /api/boards/items through loadItems), under ONE
3
+ * filter set per board that the three tabs share: search, Type, Hide done, Initiative and Source. ctx.filter.ini (a link "this initiative
4
+ * only" from an initiative page) sets the Initiative filter on mount. Source shows only when the board has Build and deliver on: with it
5
+ * off there is no Jira, and every item is a PMOS task.
6
+ * registerBoardTab('backlog' | 'board' | 'timeline', (host, ctx) => ...)
7
+ * Backlog an "Add a PMOS task" form (POST /api/boards/tasks/add: title, initiative), then a section per initiative: its epics, each
8
+ * followed by its items, then the items with no epic. A row opens the item sheet (c3-ini-work.js openWorkSheet).
9
+ * Board four columns, one per item status (To do, In progress, In review, Done), 8 cards each with "Show more". A PMOS task is
10
+ * dragged to another column (HTML drag and drop) or moved with its Move menu: POST /api/boards/tasks/state. A Jira item has
11
+ * a lock: a drop says its status lives in Jira. Epics are on Backlog and Timeline, not here.
12
+ * Timeline initiative > epic > item on the HTML timeline (c3-ini-timeline.js): a PMOS bar drags to reschedule (POST
13
+ * /api/boards/tasks/reschedule, with Undo), a Jira bar is locked; release dates (GET /api/releases) are markers.
14
+ * Every control is a button; the drag handlers are drag events on the cards and columns (pointer handlers stay on the document). */
15
+ import { el } from './dom.js';
16
+ import { registerBoardTab } from './c3-initiatives.js';
17
+ import { icon, pill, lk, ib, pbtn, note, lbl, sect, card, list, empty, stepsMini, plural, trunc, openMenu, mono } from './c3-ini-ui.js';
18
+ import { STATUSES, wfOf, tplName, loadItems, dropItems, workRow, workTlRows, iniTlRow, rescheduleTask, lockedToast } from './c3-ini-work.js';
19
+ import { timeline, tlInit, tlRange, tlStill } from './c3-ini-timeline.js';
20
+
21
+ const COL_CAP = 8;
22
+ const TL_CAP = 14;
23
+ const KINDS = [['', 'Any type'], ['Epic', 'Epic'], ['Story', 'Story'], ['Task', 'Task'], ['Bug', 'Bug']];
24
+ const STATE = new Map(); // ws|board -> the shared filters
25
+ const stateOf = (ctx) => {
26
+ const k = ctx.ws + '|' + ctx.board.id;
27
+ if (!STATE.has(k)) STATE.set(k, { q: '', ini: '', src: '', kind: '', hideDone: false, more: {}, tlAll: false });
28
+ return STATE.get(k);
29
+ };
30
+ const matches = (st, w) => {
31
+ const q = st.q.trim().toLowerCase();
32
+ return (!st.ini || w.ini === st.ini) && (!st.src || w.src === st.src) && (!st.kind || w.kind === st.kind) && (!st.hideDone || w.status !== 'Done')
33
+ && (!q || ((w.key || '') + ' ' + w.title + ' ' + (w.owner || '') + ' ' + w.ini).toLowerCase().includes(q));
34
+ };
35
+
36
+ function mountItems(tab) {
37
+ return async (host, ctx) => {
38
+ const b = ctx.board;
39
+ const st = stateOf(ctx);
40
+ if (ctx.filter && ctx.filter.ini) st.ini = ctx.filter.ini;
41
+ if (!b.build && st.src === 'jira') st.src = '';
42
+ let dead = false; let R = null;
43
+ host.replaceChildren(note('Reading the work items'));
44
+ async function reload(force) {
45
+ R = await loadItems(ctx, force);
46
+ if (!dead) draw();
47
+ }
48
+ const changed = () => { dropItems(ctx); reload(true); };
49
+
50
+ function toolbar(all, shown) {
51
+ const byId = new Map(R.initiatives.map((i) => [i.id, i]));
52
+ const withWork = [...new Set(all.map((w) => w.ini))].map((id) => byId.get(id)).filter(Boolean).sort((a, c) => ((c.featured ? 1 : 0) - (a.featured ? 1 : 0)) || a.id.localeCompare(c.id));
53
+ if (st.ini && !withWork.some((i) => i.id === st.ini) && byId.get(st.ini)) withWork.push(byId.get(st.ini));
54
+ const others = R.initiatives.length - withWork.length;
55
+ const n = shown.filter((w) => w.kind !== 'Epic').length; const ne = shown.filter((w) => w.kind === 'Epic').length;
56
+ const search = el('input', { type: 'text', value: st.q, placeholder: 'Search ' + b.name + ' work items', 'aria-label': 'Search work items', 'data-wk': 'q', autocomplete: 'off' });
57
+ let deb = null;
58
+ search.addEventListener('input', () => { clearTimeout(deb); deb = setTimeout(() => { st.q = search.value; draw('[data-wk="q"]'); }, 200); });
59
+ const any = st.q || st.ini || st.src || st.kind || st.hideDone;
60
+ return [
61
+ el('div', { class: 'calbar' }, el('div', { class: 'form ini-grow' }, search),
62
+ el('label', { class: 'chips2' }, lbl('Type'), el('select', { 'aria-label': 'Type', 'data-wk': 'kind', onchange: (e) => { st.kind = e.target.value; draw('[data-wk="kind"]'); } }, ...KINDS.map(([v, l]) => el('option', { value: v, selected: v === st.kind }, l)))),
63
+ pill({ 'data-wk': 'done', 'aria-pressed': String(st.hideDone), onclick: () => { st.hideDone = !st.hideDone; draw('[data-wk="done"]'); } }, 'Hide done')),
64
+ el('div', { class: 'chips2', role: 'group', 'aria-label': 'Initiative filter', 'data-wk': 'inis' }, lbl('INITIATIVE'),
65
+ pill({ 'data-wk-ini': '', 'aria-pressed': String(!st.ini), onclick: () => { st.ini = ''; draw('[data-wk-ini=""]'); } }, 'All initiatives'),
66
+ ...withWork.map((i) => pill({ 'data-wk-ini': i.id, 'aria-pressed': String(st.ini === i.id), title: i.id + ' ' + i.title, onclick: () => { st.ini = i.id; draw('[data-wk-ini="' + i.id + '"]'); } }, el('span', { class: 'mono' }, i.id), ' ' + trunc(i.title, 30))),
67
+ others > 0 ? el('span', { class: 'dim ini-note' }, plural(others, 'more initiative has', 'more initiatives have') + ' no epics or tasks yet') : null),
68
+ b.build
69
+ ? el('div', { class: 'chips2', role: 'group', 'aria-label': 'Source filter', 'data-wk': 'srcs' }, lbl('SOURCE'),
70
+ ...[['', 'All sources'], ['jira', [icon('jira', 'sm'), 'Jira ' + (b.jira || '')]], ['pmos', 'PMOS tasks']].map(([v, l]) => pill({ 'data-wk-src': v, 'aria-pressed': String(st.src === v), onclick: () => { st.src = v; draw('[data-wk-src="' + v + '"]'); } }, ...[].concat(l))))
71
+ : el('div', { class: 'dim ini-note', 'data-wk': 'no-jira' }, icon('info', 'sm'), ' Build and deliver is off for ' + b.name + ', so there is no Jira here: every item is a PMOS task. ', lk({ onclick: () => ctx.openBoardSettings() }, 'Board settings')),
72
+ el('div', { class: 'dim ini-note', 'data-wk': 'count' }, plural(n, 'item') + (ne ? ' in ' + plural(ne, 'epic') : '') + (st.ini ? ' · ' + st.ini + ' only' : '') + (st.src ? ' · ' + (st.src === 'jira' ? 'Jira only' : 'PMOS only') : '') + ' · ',
73
+ any ? lk({ 'data-wk': 'clear', onclick: () => { Object.assign(st, { q: '', ini: '', src: '', kind: '', hideDone: false }); draw(); } }, 'Clear filters') : null, any ? ' · ' : null, 'same filters on Backlog, Board and Timeline'),
74
+ ];
75
+ }
76
+
77
+ /* ── Backlog ── */
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);
80
+ const F = { ini: st.ini && inis.some((i) => i.id === st.ini) ? st.ini : (inis[0] || {}).id || '', busy: false };
81
+ const input = el('input', { type: 'text', placeholder: 'Add a PMOS task', 'aria-label': 'Add a PMOS task', 'data-wk': 'add-title' });
82
+ const msg = el('div', { class: 'fold w', role: 'alert', hidden: '', 'data-wk': 'add-error' });
83
+ async function add() {
84
+ const title = input.value.trim();
85
+ if (!title) { msg.textContent = 'Write the task first.'; msg.hidden = false; input.focus(); return; }
86
+ if (!F.ini || F.busy) return;
87
+ F.busy = true;
88
+ const r = await ctx.api.post('/api/boards/tasks/add', { board: b.id, initiative: F.ini, title });
89
+ F.busy = false;
90
+ if (!r || !r.ok) { msg.textContent = 'The task was not added: ' + ((r && r.error) || 'no answer') + '.'; msg.hidden = false; return; }
91
+ ctx.toast('PMOS task added to ' + F.ini + '. Drag it on the Timeline to set its dates.');
92
+ changed();
93
+ }
94
+ input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); add(); } });
95
+ return card({ ic: 'plus', title: 'Add a PMOS task', sub: b.build ? 'Jira items are created in Jira' : 'this board has no Jira', attrs: { 'data-wk': 'add' } },
96
+ inis.length ? el('div', { class: 'form' }, input,
97
+ el('select', { 'aria-label': 'Initiative for the task', 'data-wk': 'add-ini', onchange: (e) => { F.ini = e.target.value; } }, ...inis.map((i) => el('option', { value: i.id, selected: i.id === F.ini }, i.id + ' ' + trunc(i.title, 28)))),
98
+ pbtn('outline', 'Add', 'plus', { 'data-wk': 'add-go', onclick: add })) : note('No open initiative to add a task to.'), msg);
99
+ }
100
+ function backlogBody(shown, all) {
101
+ const byId = new Map(R.initiatives.map((i) => [i.id, i]));
102
+ const groups = [...new Set(shown.map((w) => w.ini))].map((id) => byId.get(id)).filter(Boolean).sort((a, c) => ((c.featured ? 1 : 0) - (a.featured ? 1 : 0)) || String(a.start || '9').localeCompare(String(c.start || '9')));
103
+ const secs = groups.map((i) => {
104
+ const its = shown.filter((w) => w.ini === i.id); const epics = its.filter((w) => w.kind === 'Epic');
105
+ const inEpic = (w) => w.kind !== 'Epic' && w.epic && epics.some((e) => e.uid === w.epic);
106
+ const ordered = [...epics.flatMap((e) => [e, ...its.filter((w) => w.epic === e.uid)]), ...its.filter((w) => w.kind !== 'Epic' && !inEpic(w))];
107
+ return sect({ ic: 'init', title: i.id + ' ' + trunc(i.title, 48), count: its.length, attrs: { 'data-wk-group': i.id },
108
+ right: el('span', { class: 'chips2' }, stepsMini(wfOf(i, b), i.stage, tplName(i, b)), lk({ onclick: () => ctx.openInitiative(i.id, 'breakdown') }, 'Open initiative')) },
109
+ list(ordered.map((w) => workRow(ctx, w, { all, sub: inEpic(w), onChanged: changed }))));
110
+ });
111
+ return [addForm(), ...(secs.length ? secs : [empty('No work items match. Clear the filters, or add a PMOS task.', { 'data-wk': 'empty' })])];
112
+ }
113
+
114
+ /* ── Board (kanban) ── */
115
+ async function setState(w, s) {
116
+ if (w.src === 'jira') { lockedToast(ctx, w.key); return; }
117
+ if (w.status === s) return;
118
+ const r = await ctx.api.post('/api/boards/tasks/state', { board: b.id, task: w.ref, state: s });
119
+ if (!r || !r.ok) { ctx.toast({ text: 'The status did not change: ' + ((r && r.error) || 'no answer') + '.', tone: 'error' }); return; }
120
+ ctx.toast('"' + trunc(w.title, 40) + '" moved to ' + s + '.');
121
+ changed();
122
+ }
123
+ function kbCard(w, all) {
124
+ const li = workRow(ctx, w, { all, epic: true, onChanged: changed,
125
+ right: w.src === 'pmos' ? [ib('Move ' + w.title + ' to another status', 'more', { 'data-wk-move': w.ref, 'aria-haspopup': 'menu', 'aria-expanded': 'false',
126
+ onclick: (e) => openMenu(e.currentTarget, { label: 'Move to a status', items: [{ group: 'Move to' }, ...STATUSES.map((s) => ({ label: s, current: s === w.status, disabled: s === w.status, attrs: { 'data-wk-state': s }, onPick: () => setState(w, s) }))] }) })] : [] });
127
+ li.setAttribute('draggable', 'true'); li.setAttribute('data-ini-card', w.uid);
128
+ li.addEventListener('dragstart', (e) => { try { e.dataTransfer.setData('text/plain', w.uid); e.dataTransfer.effectAllowed = 'move'; } catch { /* no data transfer */ } li.classList.add('ini-dragging'); });
129
+ li.addEventListener('dragend', () => li.classList.remove('ini-dragging'));
130
+ return li;
131
+ }
132
+ function kanbanBody(shown, all) {
133
+ const items = shown.filter((w) => w.kind !== 'Epic');
134
+ const cols = STATUSES.map((s) => {
135
+ const cards = items.filter((w) => w.status === s).sort((a, c) => ((c.blocked ? 1 : 0) - (a.blocked ? 1 : 0)) || String(a.end || '9').localeCompare(String(c.end || '9')));
136
+ const cap = st.more[s] ? cards.length : COL_CAP;
137
+ const col = sect({ ic: s === 'Done' ? 'check' : s === 'To do' ? 'todo' : 'bolt', title: s, count: cards.length, attrs: { 'data-ini-col': s, 'aria-label': s } },
138
+ cards.length ? list(cards.slice(0, cap).map((w) => kbCard(w, all))) : empty('Drop a PMOS task here'),
139
+ cards.length > cap ? el('div', { class: 'chips2' }, pill({ 'data-wk-more': s, onclick: () => { st.more[s] = true; draw(); } }, 'Show ' + (cards.length - cap) + ' more')) : null);
140
+ col.addEventListener('dragover', (e) => { e.preventDefault(); col.classList.add('over'); });
141
+ col.addEventListener('dragleave', (e) => { if (!col.contains(e.relatedTarget)) col.classList.remove('over'); });
142
+ col.addEventListener('drop', (e) => {
143
+ e.preventDefault(); col.classList.remove('over');
144
+ let uid = ''; try { uid = e.dataTransfer.getData('text/plain'); } catch { uid = ''; }
145
+ const w = items.find((x) => x.uid === uid); if (w) setState(w, s);
146
+ });
147
+ return col;
148
+ });
149
+ return [note('Columns are item statuses. Drag a PMOS task to move it, or use its menu. Jira items carry a lock: their status changes in Jira and shows here after the next sync. Epics are in Backlog and Timeline.'),
150
+ el('div', { class: 'brief sbb' }, el('div', { class: 'ini-kb', 'data-wk': 'kanban' }, ...cols))];
151
+ }
152
+
153
+ /* ── Timeline ── */
154
+ let markers = null;
155
+ async function loadMarkers() {
156
+ if (!b.build) { markers = []; return; }
157
+ const r = await ctx.api.get('/api/releases', { board: b.id });
158
+ markers = r && r.ok ? (r.releases || []).filter((x) => !x.board || x.board === b.id).map((x) => ({ id: x.id, at: String(x.release_at || x.deploy_at || x.date || '').slice(0, 10), label: x.name || x.id, kind: 'release' })).filter((m) => m.at) : [];
159
+ }
160
+ function timelineBody(shown) {
161
+ const withItems = new Set(shown.map((w) => w.ini));
162
+ let inis = R.initiatives.filter((i) => (st.ini ? i.id === st.ini : true));
163
+ let hidden = 0;
164
+ if (!st.ini) {
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)));
167
+ const cap = st.tlAll ? 1e9 : TL_CAP; inis = [...pick, ...rest].slice(0, Math.max(cap, pick.length));
168
+ hidden = pick.length + rest.length - inis.length;
169
+ }
170
+ const rows = inis.map((i) => iniTlRow(ctx, i, workTlRows(ctx, shown.filter((w) => w.ini === i.id), changed), b));
171
+ const first = inis.find((i) => withItems.has(i.id));
172
+ const { from, to } = tlRange(ctx.today);
173
+ const key = 'b:' + ctx.ws + ':' + b.id;
174
+ const node = timeline({ key, rows, from, to, today: ctx.today, markers: markers || [], head: 'Initiative > epic > item',
175
+ defOpen: st.ini ? [st.ini] : first ? [first.id, ...shown.filter((w) => w.ini === first.id && w.kind === 'Epic').map((e) => e.uid)] : [],
176
+ onMove: (task, days) => rescheduleTask(ctx, task, days, () => { tlStill(key); changed(); }),
177
+ onLocked: (k) => lockedToast(ctx, k) });
178
+ requestAnimationFrame(() => { if (!dead) tlInit(host); });
179
+ return [sect({ ic: 'cal', title: 'Timeline', count: rows.length, right: mono('press a row to open its epics') }, node,
180
+ hidden > 0 ? el('div', { class: 'chips2' }, el('span', { class: 'dim ini-note' }, plural(hidden, 'more Now initiative', 'more Now initiatives') + ' with no epics yet'), pill({ 'data-wk': 'tl-all', onclick: () => { st.tlAll = true; draw(); } }, 'Show all')) : null,
181
+ b.build ? note('Release dates come from Jira ' + (b.jira || '') + ' fix versions. PMOS bars drag to reschedule; Jira bars are locked.') : note('Every bar here is a PMOS task: drag it to reschedule.'))];
182
+ }
183
+
184
+ function draw(focusSel) {
185
+ if (dead) return;
186
+ if (!R.ok) { host.replaceChildren(el('div', { class: 'fold w', role: 'alert', 'data-ini': 'error' }, icon('alert', 'sm'), 'The work items could not be read: ' + R.error + '.')); return; }
187
+ const all = R.items; const shown = all.filter((w) => matches(st, w));
188
+ const body = tab === 'backlog' ? backlogBody(shown, all) : tab === 'board' ? kanbanBody(shown, all) : timelineBody(shown);
189
+ host.replaceChildren(el('div', { class: 'calbody', 'data-wk-tab': tab }, ...toolbar(all, shown), ...body));
190
+ if (focusSel) { const f = host.querySelector(focusSel); if (f) { f.focus(); if (f.setSelectionRange && f.value) f.setSelectionRange(f.value.length, f.value.length); } }
191
+ }
192
+
193
+ if (tab === 'timeline') await loadMarkers();
194
+ await reload(false);
195
+ return () => { dead = true; };
196
+ };
197
+ }
198
+
199
+ registerBoardTab('backlog', mountItems('backlog'));
200
+ registerBoardTab('board', mountItems('board'));
201
+ registerBoardTab('timeline', mountItems('timeline'));
@@ -0,0 +1,51 @@
1
+ /* pm/c3-ib-releases.js — the Releases tab of a board with Build and deliver on (mockup v5, 34-initiatives.js tabReleases).
2
+ * registerBoardTab('releases', (host, ctx) => ...)
3
+ * Reads GET /api/releases (lib/routes-releases.js: id, name?, initiative_ids, deploy_at, release_at, accepted_at, gapFlags, and `board`
4
+ * and `status` when a release carries them) and keeps the releases whose initiative_ids meet the board's initiatives (loadItems). A
5
+ * release that names another board in `board` is left out. Read-only: releases mirror Jira fix versions; their dates live on the Calendar.
6
+ * Left: Unreleased (the nearest first), each row with its status, date and days to go, the share of its scope done, and in place its
7
+ * scope as links; then Released. Right: where releases live, and Open the Calendar. Nothing is invented: a release with no name shows
8
+ * its id, one with no date says "No date yet". */
9
+ import { el } from './dom.js';
10
+ import { registerBoardTab } from './c3-initiatives.js';
11
+ import { icon, pbtn, lk, chip, tag, st, note, sect, card, list, empty, nrow, cols, hbs, hbr, statusChip, plural, day, daysBetween } from './c3-ini-ui.js';
12
+ import { loadItems } from './c3-ini-work.js';
13
+
14
+ const whenOf = (r) => String(r.release_at || r.deploy_at || r.date || '').slice(0, 10);
15
+ const relSt = (s) => (/at risk/i.test(s) ? st('warn', 'alert', 'At risk') : /ready/i.test(s) ? st('ok', 'check', 'Ready') : /released|accepted/i.test(s) ? st('ok', 'check', s) : tag(s));
16
+
17
+ registerBoardTab('releases', async (host, ctx) => {
18
+ let dead = false;
19
+ const b = ctx.board;
20
+ host.replaceChildren(note('Reading the releases'));
21
+ const [items, rel] = await Promise.all([loadItems(ctx), ctx.api.get('/api/releases', { board: b.id })]);
22
+ if (dead) return () => {};
23
+ const fail = (t) => { host.replaceChildren(el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), t)); return () => { dead = true; }; };
24
+ if (!items.ok) return fail('The initiatives of this board could not be read: ' + items.error + '.');
25
+ if (!rel || !rel.ok) return fail('The releases could not be read: ' + ((rel && rel.error) || 'no answer') + '.');
26
+ const byId = new Map(items.initiatives.map((i) => [i.id, i]));
27
+ const mine = (rel.releases || []).filter((r) => (r.board ? r.board === b.id : Array.isArray(r.initiative_ids) && r.initiative_ids.some((id) => byId.has(id))));
28
+ const statusOf = (r, past) => r.status || (r.accepted_at ? 'Released' : past ? 'Past' : 'Planned');
29
+ const up = mine.filter((r) => !whenOf(r) || whenOf(r) >= ctx.today).sort((a, c) => (!whenOf(a)) - (!whenOf(c)) || whenOf(a).localeCompare(whenOf(c)));
30
+ const past = mine.filter((r) => whenOf(r) && whenOf(r) < ctx.today).sort((a, c) => whenOf(c).localeCompare(whenOf(a)));
31
+ const row = (r, isPast) => {
32
+ const scope = (r.initiative_ids || []).map((id) => byId.get(id) || { id, title: '', progress: null, status: '' });
33
+ const known = scope.filter((i) => i.progress != null);
34
+ const prog = known.length ? known.reduce((n, i) => n + (Number(i.progress) || 0), 0) / known.length : 0;
35
+ const status = statusOf(r, isPast); const w = whenOf(r); const gaps = Array.isArray(r.gapFlags) ? r.gapFlags.length : 0;
36
+ return nrow({ ic: isPast ? 'check' : 'out', hot: /at risk/i.test(status), tone: isPast ? 'ok' : '', title: r.name || r.id, titleAttrs: { 'data-inb-relopen': r.id },
37
+ chips: [relSt(status), chip(icon('cal', 'sm'), w ? day(w) + (isPast ? '' : ' · ' + plural(daysBetween(ctx.today, w), 'day')) : 'No date yet'),
38
+ known.length ? chip(Math.round(prog * 100) + '% of scope done') : null, gaps ? st('warn', 'alert', plural(gaps, 'gap')) : null, tag(icon('jira', 'sm'), 'fix version')],
39
+ xp: [hbs([hbr('Scope done', Math.round(prog * 100) + '%', prog, /at risk/i.test(status) ? 'warn' : 'ok')]),
40
+ el('div', { class: 'chips', 'data-inb': 'release-scope' }, el('span', { class: 'dim ini-note' }, 'Scope:'),
41
+ ...(scope.length ? scope.map((i) => el('span', { class: 'chips2' }, byId.has(i.id) ? lk({ onclick: () => ctx.openInitiative(i.id), 'data-inb-scope': i.id }, i.id + (i.title ? ' ' + i.title : '')) : chip(i.id), i.status ? statusChip(i.status) : null))
42
+ : [el('span', { class: 'dim' }, 'No initiatives in scope')]))],
43
+ attrs: { 'data-inb-release': r.id, 'data-inb-when': isPast ? 'past' : r === up[0] ? 'next' : 'later' } });
44
+ };
45
+ host.replaceChildren(cols(
46
+ [sect({ ic: 'out', title: 'Unreleased', count: up.length, attrs: { 'data-inb': 'release-next' } }, up.length ? list(up.map((r) => row(r, false))) : empty('No release planned for ' + b.name + '.', { 'data-inb': 'release-none-next' })),
47
+ past.length ? sect({ ic: 'check', title: 'Released', count: past.length, attrs: { 'data-inb': 'release-past' } }, list(past.map((r) => row(r, true)))) : null],
48
+ [card({ ic: 'out', title: 'Where releases live' }, note('Releases mirror Jira ' + (b.jira || '') + ' fix versions, because this board builds and delivers. Release dates are references: their home is the Calendar.'),
49
+ el('div', { class: 'chips2' }, pbtn('outline', 'Open the Calendar', 'cal', { 'data-inb': 'open-calendar', onclick: () => ctx.showTab('cal') })))]));
50
+ return () => { dead = true; };
51
+ });
@@ -0,0 +1,55 @@
1
+ /* pm/c3-ib-sprints.js — the Sprints tab of a board with Build and deliver on (mockup v5, 34-initiatives.js tabSprints).
2
+ * registerBoardTab('sprints', (host, ctx) => ...)
3
+ * Reads GET /api/boards/sprints { source: jira|none, readAt, sprints: [{ name, state: active|future|closed, issues: [{ key, summary, status,
4
+ * category: todo|doing|done, initiative }], counts }] }. READ-ONLY: the sprints mirror Jira, so there is no create, edit or move control.
5
+ * Left: the source line (mirrored from Jira, read at), the active sprint's items as rows (a linked initiative opens its page), then the
6
+ * future and closed sprints in place. Right: the active sprint's counts, and its burndown from GET /api/stage-tools/burndown (built by
7
+ * the stage-records change; when that route is not on this server yet, the card says so quietly). source 'none' says there is no tracker. */
8
+ import { el } from './dom.js';
9
+ import { registerBoardTab } from './c3-initiatives.js';
10
+ import { icon, pbtn, lk, chip, tag, st, note, sect, card, list, empty, nrow, cols, hbs, hbr, kv, plural, day } from './c3-ini-ui.js';
11
+ import { loadItems } from './c3-ini-work.js';
12
+ import { recordCards } from './c3-ini-records.js';
13
+
14
+ const CAT = { todo: 'To do', doing: 'In progress', done: 'Done' };
15
+ const readTime = (s) => { const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(String(s || '')); return m ? day(m[1]) + ', ' + m[2] : ''; };
16
+ const countsOf = (sp) => { const c = sp.counts || {}; const f = (k) => (c[k] != null ? Number(c[k]) : (sp.issues || []).filter((i) => i.category === k).length); return { todo: f('todo'), doing: f('doing'), done: f('done') }; };
17
+
18
+ registerBoardTab('sprints', async (host, ctx) => {
19
+ let dead = false;
20
+ const b = ctx.board;
21
+ host.replaceChildren(note('Reading the sprints'));
22
+ const [r, items] = await Promise.all([ctx.api.get('/api/boards/sprints', { board: b.id }), loadItems(ctx)]);
23
+ if (dead) return () => {};
24
+ if (!r || !r.ok) { host.replaceChildren(el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), 'The sprints could not be read: ' + ((r && r.error) || 'no answer') + '.')); return () => { dead = true; }; }
25
+ if (r.source === 'none') { host.replaceChildren(empty('Waiting for the first sync from Jira ' + (b.jira || '') + '. Sprints will be mirrored here, read-only.', { 'data-inb': 'sprints-none' })); return () => { dead = true; }; }
26
+ const sprints = r.sprints || [];
27
+ const active = sprints.find((s) => s.state === 'active');
28
+ const others = sprints.filter((s) => s !== active);
29
+ const issueRow = (i) => nrow({ ic: i.category === 'done' ? 'check' : i.category === 'doing' ? 'bolt' : 'todo', hot: i.category === 'doing', title: i.summary || i.key,
30
+ chips: [el('span', { class: 'chip2 mono' }, i.key), tag(icon('jira', 'sm'), 'Jira'), i.category === 'done' ? st('ok', 'check', i.status || 'Done') : chip(i.status || CAT[i.category] || ''),
31
+ i.initiative ? lk({ onclick: () => ctx.openInitiative(i.initiative), 'data-inb-issue-ini': i.initiative }, icon('init', 'sm'), i.initiative) : el('span', { class: 'dim' }, 'No initiative')],
32
+ right: [el('span', { class: 'chip2', title: 'Status lives in Jira' }, icon('lock', 'sm'))], attrs: { 'data-inb-issue': i.key } });
33
+ const t = readTime(r.readAt);
34
+ const left = [
35
+ el('div', { class: 'fold', 'data-inb': 'sprints-source' }, icon('lock', 'sm'), el('b', null, 'Mirrored from Jira, read-only.'), ' ' + (t ? 'Read ' + t + ' from Jira ' + (b.jira || '') + '.' : 'Not read yet.') + ' Change sprints in Jira.'),
36
+ active ? sect({ ic: 'check', title: active.name + ' items', count: (active.issues || []).length, right: tag('read-only'), attrs: { 'data-inb-sprint': active.name, 'data-inb-state': 'active' } },
37
+ (active.issues || []).length ? list(active.issues.map(issueRow)) : empty('No items in this sprint yet.', { 'data-inb': 'sprint-no-issues' }))
38
+ : empty(sprints.length ? 'No sprint is active.' : 'Jira has no sprints for this board.', { 'data-inb': sprints.length ? 'sprints-no-active' : 'sprints-empty' }),
39
+ ...others.map((s) => { const c = countsOf(s); return sect({ ic: s.state === 'closed' ? 'check' : 'clock', title: s.name, count: (s.issues || []).length, right: tag(s.state || 'sprint'), attrs: { 'data-inb-sprint': s.name, 'data-inb-state': s.state } },
40
+ nrow({ ic: 'clock', title: c.done + ' done, ' + c.doing + ' in progress, ' + c.todo + ' to do', xp: (s.issues || []).length ? [list(s.issues.map(issueRow))] : [note('No items.')] })); }),
41
+ ];
42
+ const right = [];
43
+ if (active) {
44
+ const c = countsOf(active); const tot = Math.max(1, c.todo + c.doing + c.done);
45
+ right.push(card({ ic: 'clock', title: active.name, attrs: { 'data-inb': 'sprint-counts' } }, kv([['Items', String(c.todo + c.doing + c.done)], ['Done', String(c.done)], ['In progress', String(c.doing)], ['To do', String(c.todo)]]),
46
+ hbs([hbr('Done', c.done, c.done / tot, 'ok'), hbr('In progress', c.doing, c.doing / tot, 'acc'), hbr('To do', c.todo, c.todo / tot, 'pm')])));
47
+ const ids = items.ok ? items.initiatives.map((i) => i.id) : [];
48
+ const pseudo = { ...ctx, row: { id: ids.join(','), title: '' } };
49
+ right.push(...await recordCards(pseudo, ['burndown']));
50
+ }
51
+ right.push(card({ ic: 'out', title: 'In Jira' }, note('Sprints, their dates and their items are Jira\'s. PMOS mirrors them after each sync.'), el('div', { class: 'chips2' }, pbtn('outline', 'Open the Board', 'cols', { onclick: () => ctx.openBoard(b.id, 'board') }))));
52
+ if (dead) return () => {};
53
+ host.replaceChildren(cols(left, right));
54
+ return () => { dead = true; };
55
+ });
@@ -0,0 +1,178 @@
1
+ /* pm/c3-ib-triage.js — the Triage tab of a board: the home of new asks (mockup v5, 34-initiatives.js tabTriage / askRow).
2
+ * registerBoardTab('triage', (host, ctx) => ...) ctx is documented in the header of c3-initiatives.js.
3
+ * Left: "Add an ask" (one line, POST /api/boards/asks/add; its intake draft shows on the new row), the new asks (GET /api/boards/asks
4
+ * state=open, newest first) and the decided ones (accepted and declined). Each ask row opens in place: its draft (the lane guess with its
5
+ * confidence, the anchor suggestion, any prior rejection) and Accept, Ask for more, Decline.
6
+ * Accept a sheet with title, lane and anchor prefilled from the draft; the anchor is required and the sheet says why when it is
7
+ * empty. POST /api/boards/asks/accept starts an initiative with the board's default workflow, in its first stage; a toast
8
+ * opens it.
9
+ * Ask for more a sheet for the question to the asker (POST /api/boards/asks/more). The ask stays open, marked "asked for more".
10
+ * Decline a sheet that asks for the reason (POST /api/boards/asks/decline).
11
+ * Right: how triage works, naming the default workflow and its first stage.
12
+ * Contract fields relied on: an ask is { id, line, source?, at?, by?, needMore?, moreNote?, draft?: { title?, lane, confidence,
13
+ * anchor_suggestion, prior_rejections: [{ id, title, why }] }, initiative? (accepted), why? (declined) }. Nothing is invented. */
14
+ import { el } from './dom.js';
15
+ import { registerBoardTab } from './c3-initiatives.js';
16
+ import { icon, pbtn, lk, chip, tag, st, note, noteA, lbl, sect, card, list, empty, nrow, cols, chgl, ago } from './c3-ini-ui.js';
17
+ import { TEMPLATES } from './c3-ini-work.js';
18
+
19
+ const LANES = ['feature', 'discovery', 'chore', 'incident', 'content', 'prototype'];
20
+ const ANCHOR_WHY = 'An initiative needs an anchor: a key result or a health budget. PMOS refuses to create one without it.';
21
+ const pct = (c) => (typeof c === 'number' && c >= 0 && c <= 1 ? Math.round(c * 100) + '%' : '');
22
+ const errText = (r, what) => what + ': ' + ((r && r.error) || 'no answer') + '.';
23
+ const errLine = (attrs) => el('div', { class: 'fold w', role: 'alert', hidden: '', ...attrs });
24
+
25
+ registerBoardTab('triage', async (host, ctx) => {
26
+ const b = ctx.board;
27
+ const def = TEMPLATES[b.defTpl] || TEMPLATES[b.template] || TEMPLATES.product;
28
+ const first = (TEMPLATES[b.defTpl] ? def.stages[0] : (b.workflow || [])[0]) || def.stages[0];
29
+ const S = { open: [], other: [], busy: false, dead: false, err: '', justAdded: '' };
30
+
31
+ const input = el('input', { type: 'text', 'data-ibt': 'line', 'aria-label': 'Add an ask', placeholder: 'Add an ask: who wants what, and why', maxlength: '300', autocomplete: 'off' });
32
+ input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); add(); } });
33
+ const addBtn = pbtn('outline', 'Add', 'plus', { 'data-ibt': 'add', onclick: () => add() });
34
+ const msg = errLine({ 'data-ibt': 'error' });
35
+ const left = el('div', { class: 'ini-col' });
36
+ const right = card({ ic: 'inbox', title: 'How triage works', attrs: { 'data-ibt': 'intro' } },
37
+ note('New asks land here from Slack, email, Jira Service Desk and meeting notes. Each gets a sharpness check and a prior-ask check. Accepting starts an initiative with this board\'s default workflow, ' + def.name + ', in ' + first.name + '. You can switch the initiative to another workflow afterwards.'),
38
+ chgl([el('li', null, icon('check', 'sm'), el('span', null, 'Accept: starts an initiative (' + def.name + ', ' + first.name + ')'), el('span')),
39
+ el('li', null, icon('reply', 'sm'), el('span', null, 'Ask for more: the asker is prompted for detail'), el('span')),
40
+ el('li', null, icon('x', 'sm'), el('span', null, 'Decline: the asker is told why'), el('span'))]));
41
+ host.replaceChildren(cols(left, right));
42
+ const setErr = (t) => { msg.textContent = t || ''; msg.hidden = !t; };
43
+
44
+ function draftNodes(a) {
45
+ const d = a.draft;
46
+ if (!d) return [noteA({ 'data-ibt': 'no-draft' }, 'No intake draft for this ask.')];
47
+ const c = pct(d.confidence);
48
+ return [el('div', { class: 'chips', 'data-ibt': 'draft' },
49
+ tag('Lane guess: ' + (d.lane || 'none') + (c ? ', ' + c + ' sure' : '')),
50
+ d.anchor_suggestion ? chip(icon('target', 'sm'), 'Anchor: ' + d.anchor_suggestion) : st('warn', 'alert', 'No anchor suggested', { 'data-ibt': 'anchor-sug' }),
51
+ ...(Array.isArray(d.prior_rejections) ? d.prior_rejections : []).map((p) => st('bad', 'x', 'Rejected before: ' + (p.id ? p.id + ' ' : '') + (p.title || '') + (p.why ? ' (' + p.why + ')' : ''), { title: p.why || '', 'data-ibt': 'prior' })))];
52
+ }
53
+ function row(a, state) {
54
+ const open = state === 'open';
55
+ const ini = a.initiative && (a.initiative.id || a.initiative);
56
+ const sharp = a.draft && typeof a.draft.confidence === 'number' ? (a.draft.confidence >= 0.7 ? 'sharp' : a.draft.confidence < 0.5 ? 'needs detail' : 'readable') : 'no draft';
57
+ return nrow({ ic: state === 'declined' ? 'x' : state === 'accepted' ? 'check' : 'bolt', hot: open, title: a.line, key: 'ask:' + a.id,
58
+ chips: [a.source ? chip(a.source) : null, a.by ? chip(icon('user', 'sm'), a.by) : null, a.at ? chip(ago(a.at, ctx.today)) : null,
59
+ open && a.needMore ? st('warn', 'alert', 'asked for more', { 'data-ibt': 'need-more', title: a.moreNote || '' }) : null,
60
+ state === 'accepted' ? st('ok', 'check', 'Accepted') : null, state === 'accepted' && ini ? lk({ 'data-ibt-open': ini, onclick: () => ctx.openInitiative(ini) }, ini) : null,
61
+ state === 'declined' ? tag('declined') : null],
62
+ right: open ? [tag(sharp)] : [],
63
+ xp: open ? [...draftNodes(a), a.needMore && a.moreNote ? note('You asked: ' + a.moreNote) : null,
64
+ el('div', { class: 'chips2' }, pbtn('primary', 'Accept', 'check', { 'data-ibt-accept': a.id, onclick: () => openAccept(a) }),
65
+ pbtn('outline', 'Ask for more', 'reply', { 'data-ibt-more': a.id, onclick: () => openMore(a) }),
66
+ pbtn('ghost', 'Decline', 'x', { 'data-ibt-decline': a.id, onclick: () => openDecline(a) }))].filter(Boolean)
67
+ : state === 'declined' && a.why ? [note('Declined: ' + a.why)] : null,
68
+ attrs: { 'data-ibt-ask': a.id, 'data-state': state } });
69
+ }
70
+ function draw() {
71
+ left.replaceChildren(
72
+ card({ ic: 'plus', title: 'Add an ask' }, el('div', { class: 'form', 'data-ibt': 'add-form' }, input, addBtn), msg),
73
+ sect({ ic: 'inbox', title: 'New asks', count: S.open.length }, S.open.length ? list(S.open.map((a) => row(a, 'open')), { 'data-ibt': 'asks', 'aria-label': 'Open asks' }) : empty('Nothing new to triage on this board.', { 'data-ibt': 'empty' })),
74
+ S.other.length ? sect({ ic: 'check', title: 'Decided', count: S.other.length, attrs: { 'data-ibt': 'closed' } }, list(S.other.map((x) => row(x, x.state)))) : null);
75
+ if (S.justAdded) {
76
+ const li = left.querySelector('[data-ibt-ask="' + CSS.escape(S.justAdded) + '"] .tt'); S.justAdded = '';
77
+ if (li) li.click();
78
+ }
79
+ }
80
+ async function reload() {
81
+ const [op, ac, de] = await Promise.all(['open', 'accepted', 'declined'].map((state) => ctx.api.get('/api/boards/asks', { board: b.id, state })));
82
+ if (S.dead) return;
83
+ if (!op || !op.ok) { setErr(errText(op, 'The asks could not be read')); return; }
84
+ const by = (x, y) => String(y.at || '').localeCompare(String(x.at || ''));
85
+ S.open = (op.asks || []).slice().sort(by);
86
+ S.other = [...((ac && ac.ok && ac.asks) || []).map((x) => ({ ...x, state: 'accepted' })), ...((de && de.ok && de.asks) || []).map((x) => ({ ...x, state: 'declined' }))].sort(by);
87
+ }
88
+
89
+ async function add() {
90
+ const line = input.value.trim();
91
+ if (!line) { setErr('Write the ask in one line first.'); input.focus(); return; }
92
+ if (S.busy) return;
93
+ S.busy = true; addBtn.disabled = true; setErr('');
94
+ const r = await ctx.api.post('/api/boards/asks/add', { board: b.id, line });
95
+ S.busy = false; addBtn.disabled = false;
96
+ if (S.dead) return;
97
+ if (!r || !r.ok) { setErr(errText(r, 'The ask was not added')); input.focus(); return; }
98
+ const ask = { ...(r.ask || { id: 'ask-' + Date.now(), line }), draft: r.draft || (r.ask && r.ask.draft) || null };
99
+ S.open = [ask, ...S.open.filter((x) => x.id !== ask.id)];
100
+ input.value = ''; S.justAdded = ask.id;
101
+ draw();
102
+ ctx.toast('Ask added to ' + b.name + ' triage. Its intake draft is on the row.');
103
+ }
104
+ const fieldOf = (label, node) => el('label', { class: 'ini-field' }, lbl(label), el('div', { class: 'form' }, node));
105
+
106
+ function openAccept(a) {
107
+ const d = a.draft || {};
108
+ const F = { title: d.title || a.line, lane: LANES.includes(d.lane) ? d.lane : 'feature', anchor: d.anchor_suggestion || '' };
109
+ const body = el('div', { class: 'ini-form', 'data-ibt': 'accept-form' });
110
+ const sheet = ctx.openSheet({ title: 'Accept ask', icon: 'check', body });
111
+ const title = el('input', { type: 'text', 'data-ibt': 'f-title', 'aria-label': 'Title', value: F.title, oninput: (e) => { F.title = e.target.value; sync(); } });
112
+ const lane = el('select', { 'data-ibt': 'f-lane', 'aria-label': 'Lane', onchange: (e) => { F.lane = e.target.value; } }, ...LANES.map((l) => el('option', { value: l, selected: l === F.lane }, l)));
113
+ const anchor = el('input', { type: 'text', 'data-ibt': 'f-anchor', 'aria-label': 'Anchor', 'aria-describedby': 'ibt-anchor-why', placeholder: 'For example: KR 2.1 Activation to 45%', value: F.anchor, oninput: (e) => { F.anchor = e.target.value; sync(); } });
114
+ const why = el('div', { class: 'dim ini-note', id: 'ibt-anchor-why', 'data-ibt': 'anchor-why' });
115
+ const fail = errLine({ 'data-ibt': 'accept-error' });
116
+ const go = pbtn('primary', 'Accept into ' + first.name, null, { 'data-ibt': 'accept-go', onclick: submit });
117
+ function sync() {
118
+ const none = !F.anchor.trim();
119
+ why.textContent = none ? (d.anchor_suggestion ? '' : 'The intake draft has no anchor suggestion. ') + ANCHOR_WHY + ' Type one to go on.'
120
+ : 'It starts with the ' + def.name + ' workflow, in ' + first.name + '.';
121
+ go.disabled = none || !F.title.trim();
122
+ }
123
+ async function submit() {
124
+ if (!F.anchor.trim() || !F.title.trim()) { sync(); return; }
125
+ go.disabled = true; fail.hidden = true;
126
+ const r = await ctx.api.post('/api/boards/asks/accept', { board: b.id, ask: a.id, title: F.title.trim(), lane: F.lane, anchor: F.anchor.trim() });
127
+ if (!r || !r.ok) { fail.textContent = errText(r, 'The ask was not accepted'); fail.hidden = false; go.disabled = false; return; }
128
+ sheet.close();
129
+ const id = (r.initiative && (r.initiative.id || r.initiative)) || '';
130
+ ctx.toast({ text: 'Accepted as ' + id + ' in ' + (r.stage || first.name) + ', with the ' + def.name + ' workflow.', actionLabel: 'Open ' + id, onAction: () => ctx.openInitiative(id), timeout: 9000 });
131
+ ctx.refresh();
132
+ }
133
+ body.replaceChildren(note('"' + a.line + '"'), fieldOf('TITLE', title), fieldOf('LANE', lane), fieldOf('ANCHOR', anchor), why, fail,
134
+ el('div', { class: 'chips2' }, go, pbtn('ghost', 'Cancel', null, { onclick: () => sheet.close() })));
135
+ sync();
136
+ title.focus();
137
+ }
138
+ function openMore(a) {
139
+ const F = { note: '' };
140
+ const body = el('div', { class: 'ini-form', 'data-ibt': 'more-form' });
141
+ const sheet = ctx.openSheet({ title: 'Ask for more', icon: 'reply', body });
142
+ const q = el('input', { type: 'text', 'data-ibt': 'f-more', 'aria-label': 'What do you need to know', placeholder: 'For example: who asked, and how often does it happen?', maxlength: '500', oninput: (e) => { F.note = e.target.value; } });
143
+ const fail = errLine({ 'data-ibt': 'more-error' });
144
+ const go = pbtn('primary', 'Ask for more', null, { 'data-ibt': 'more-go', onclick: async () => {
145
+ go.disabled = true; fail.hidden = true;
146
+ const r = await ctx.api.post('/api/boards/asks/more', { board: b.id, ask: a.id, note: F.note.trim() });
147
+ if (!r || !r.ok) { fail.textContent = errText(r, 'The question was not sent'); fail.hidden = false; go.disabled = false; return; }
148
+ sheet.close();
149
+ ctx.toast('The asker is prompted for more detail. The ask stays in triage.');
150
+ if (!S.dead) { a.needMore = true; a.moreNote = F.note.trim(); draw(); }
151
+ } });
152
+ body.replaceChildren(note('"' + a.line + '"' + (a.source ? ' · ' + a.source : '')), fieldOf('QUESTION', q), fail, el('div', { class: 'chips2' }, go, pbtn('ghost', 'Cancel', null, { onclick: () => sheet.close() })));
153
+ q.focus();
154
+ }
155
+ function openDecline(a) {
156
+ const F = { why: '' };
157
+ const body = el('div', { class: 'ini-form', 'data-ibt': 'decline-form' });
158
+ const sheet = ctx.openSheet({ title: 'Decline ask', icon: 'x', body });
159
+ const why = el('textarea', { rows: '3', 'data-ibt': 'f-why', 'aria-label': 'Reason', placeholder: 'Why not? The reason is kept and shows if the same ask comes again.', oninput: (e) => { F.why = e.target.value; go.disabled = !F.why.trim(); } });
160
+ const fail = errLine({ 'data-ibt': 'decline-error' });
161
+ const go = pbtn('primary', 'Decline', null, { 'data-ibt': 'decline-go', disabled: true, onclick: async () => {
162
+ if (!F.why.trim()) return;
163
+ go.disabled = true; fail.hidden = true;
164
+ const r = await ctx.api.post('/api/boards/asks/decline', { board: b.id, ask: a.id, why: F.why.trim() });
165
+ if (!r || !r.ok) { fail.textContent = errText(r, 'The ask was not declined'); fail.hidden = false; go.disabled = false; return; }
166
+ sheet.close();
167
+ ctx.toast('Ask declined. The asker is told why.');
168
+ ctx.refresh();
169
+ } });
170
+ body.replaceChildren(note('"' + a.line + '"'), el('label', { class: 'ini-field' }, lbl('REASON'), el('div', { class: 'form ini-text' }, why)), fail,
171
+ el('div', { class: 'chips2' }, go, pbtn('ghost', 'Cancel', null, { onclick: () => sheet.close() })));
172
+ why.focus();
173
+ }
174
+
175
+ await reload();
176
+ if (!S.dead) draw();
177
+ return () => { S.dead = true; };
178
+ });