create-pmos 0.124.0 → 0.126.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 (129) 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/scripts/nine-stages.js +81 -0
  6. package/payload/scripts/workflow-state.js +2 -0
  7. package/payload/state/discoveries.md +1 -1
  8. package/payload/state/log.md +1 -1
  9. package/workspace/c3-one-home-test.js +131 -0
  10. package/workspace/fixtures/initiatives-api/list.json +171 -3
  11. package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
  12. package/workspace/fixtures/initiatives-api/sheet.json +57 -1
  13. package/workspace/fixtures/one-home-baseline.json +83 -0
  14. package/workspace/guide/briefing-cards.md +40 -0
  15. package/workspace/guide/index.md +1 -0
  16. package/workspace/guide/layout.md +2 -0
  17. package/workspace/lib/engine/nine-stages.js +81 -0
  18. package/workspace/lib/engine/workflow-state.js +2 -0
  19. package/workspace/lib/facts-async.js +51 -0
  20. package/workspace/lib/item-steps.js +52 -0
  21. package/workspace/lib/jira-status.js +8 -3
  22. package/workspace/lib/jobs-to-be-done.js +52 -0
  23. package/workspace/lib/needs-overdue.js +41 -0
  24. package/workspace/lib/outbox-store.js +87 -0
  25. package/workspace/lib/pm-items.js +115 -0
  26. package/workspace/lib/route-modules.js +21 -0
  27. package/workspace/lib/routes-briefing.js +109 -0
  28. package/workspace/lib/routes-c3-init.js +53 -0
  29. package/workspace/lib/routes-c3-life.js +119 -0
  30. package/workspace/lib/routes-c3-strategy.js +169 -0
  31. package/workspace/lib/routes-c3-timeline.js +191 -0
  32. package/workspace/lib/routes-calendar-import.js +60 -0
  33. package/workspace/lib/routes-calendar-views.js +61 -0
  34. package/workspace/lib/routes-doc-health.js +79 -0
  35. package/workspace/lib/routes-doc-sheet.js +169 -0
  36. package/workspace/lib/routes-forecast.js +85 -0
  37. package/workspace/lib/routes-hooks.js +121 -0
  38. package/workspace/lib/routes-initiatives.js +3 -0
  39. package/workspace/lib/routes-intake.js +24 -0
  40. package/workspace/lib/routes-outbox.js +97 -0
  41. package/workspace/lib/routes-pm-items.js +216 -0
  42. package/workspace/lib/routes-records.js +61 -0
  43. package/workspace/lib/routes-releases.js +129 -0
  44. package/workspace/lib/routes-report-facts.js +84 -0
  45. package/workspace/lib/routes-report-families.js +32 -0
  46. package/workspace/lib/routes-roadmap-ops.js +141 -0
  47. package/workspace/lib/routes-runs.js +119 -0
  48. package/workspace/lib/routes-signals.js +50 -0
  49. package/workspace/lib/run-answers.js +69 -0
  50. package/workspace/package-lock.json +2 -2
  51. package/workspace/package.json +1 -1
  52. package/workspace/public/c3.html +26 -0
  53. package/workspace/public/pm/briefing-deck.js +413 -0
  54. package/workspace/public/pm/briefing-model.js +211 -0
  55. package/workspace/public/pm/c3-breakdown.js +240 -0
  56. package/workspace/public/pm/c3-brief.js +278 -0
  57. package/workspace/public/pm/c3-cal-strategy.js +528 -0
  58. package/workspace/public/pm/c3-case.js +185 -0
  59. package/workspace/public/pm/c3-daypart.js +176 -0
  60. package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
  61. package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
  62. package/workspace/public/pm/c3-demo-cal2.js +66 -0
  63. package/workspace/public/pm/c3-demo-cal3.js +173 -0
  64. package/workspace/public/pm/c3-demo-data.js +560 -0
  65. package/workspace/public/pm/c3-demo-init.js +373 -0
  66. package/workspace/public/pm/c3-demo-life.js +77 -0
  67. package/workspace/public/pm/c3-demo-misc2.js +46 -0
  68. package/workspace/public/pm/c3-demo-plan2.js +30 -0
  69. package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
  70. package/workspace/public/pm/c3-demo-rep2.js +92 -0
  71. package/workspace/public/pm/c3-demo-term.js +153 -0
  72. package/workspace/public/pm/c3-demo-time.js +49 -0
  73. package/workspace/public/pm/c3-demo-work-ix.js +80 -0
  74. package/workspace/public/pm/c3-demo-work2.js +13 -0
  75. package/workspace/public/pm/c3-demo.js +113 -0
  76. package/workspace/public/pm/c3-doc-sheet.js +314 -0
  77. package/workspace/public/pm/c3-dock.js +180 -0
  78. package/workspace/public/pm/c3-docs.js +290 -0
  79. package/workspace/public/pm/c3-expand.js +96 -0
  80. package/workspace/public/pm/c3-init-extras.js +170 -0
  81. package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
  82. package/workspace/public/pm/c3-misc-panels.js +89 -0
  83. package/workspace/public/pm/c3-plan-panels.js +163 -0
  84. package/workspace/public/pm/c3-plan.js +192 -0
  85. package/workspace/public/pm/c3-roadmap.js +249 -0
  86. package/workspace/public/pm/c3-sheet.js +107 -0
  87. package/workspace/public/pm/c3-shell.js +674 -0
  88. package/workspace/public/pm/c3-signals.js +196 -0
  89. package/workspace/public/pm/c3-strategy-panels.js +45 -0
  90. package/workspace/public/pm/c3-swipe.js +230 -0
  91. package/workspace/public/pm/c3-terminal-data.js +26 -0
  92. package/workspace/public/pm/c3-terminal-edge.js +91 -0
  93. package/workspace/public/pm/c3-terminal-icons.js +41 -0
  94. package/workspace/public/pm/c3-terminal-results.js +141 -0
  95. package/workspace/public/pm/c3-terminal.js +52 -0
  96. package/workspace/public/pm/c3-tickets.js +163 -0
  97. package/workspace/public/pm/c3-timeline.js +201 -0
  98. package/workspace/public/pm/c3-toast.js +16 -0
  99. package/workspace/public/pm/calendar-views.js +1014 -0
  100. package/workspace/public/pm/console3-lift.md +220 -0
  101. package/workspace/public/pm/console3.css +1547 -0
  102. package/workspace/public/pm/dest-initiatives.js +16 -3
  103. package/workspace/public/pm/dest-mywork.js +8 -1
  104. package/workspace/public/pm/dest-reports.js +47 -1
  105. package/workspace/public/pm/dest-today.js +55 -1
  106. package/workspace/public/pm/doc-health-panel.js +110 -0
  107. package/workspace/public/pm/doc-rail.js +9 -3
  108. package/workspace/public/pm/lifecycle-lens-model.js +228 -0
  109. package/workspace/public/pm/lifecycle-lens.js +261 -0
  110. package/workspace/public/pm/new-initiative-sheet.js +116 -21
  111. package/workspace/public/pm/outbox-panel.js +125 -0
  112. package/workspace/public/pm/pm-work-view.js +895 -0
  113. package/workspace/public/pm/releases-view.js +248 -0
  114. package/workspace/public/pm/report-families-view.js +503 -0
  115. package/workspace/public/pm/run-card.js +244 -0
  116. package/workspace/public/pm/sessions-panel.js +149 -0
  117. package/workspace/public/pm/shell-bell.js +162 -0
  118. package/workspace/public/pm/shell-model.js +83 -0
  119. package/workspace/public/pm/shell-phone.js +124 -0
  120. package/workspace/public/pm/shell-readaloud.js +53 -0
  121. package/workspace/public/pm/shell-recording.js +110 -0
  122. package/workspace/public/pm/shell-ticker.js +90 -0
  123. package/workspace/public/pm/slide-confirm.js +163 -0
  124. package/workspace/public/pm/timeline-forecast-view.js +164 -0
  125. package/workspace/public/pm/today-day.js +8 -2
  126. package/workspace/public/pm/tokens.css +1 -0
  127. package/workspace/server.js +5 -2
  128. package/workspace/test-files.json +21 -3
  129. package/workspace/test-lists/browser-suites-green.json +5 -0
@@ -0,0 +1,89 @@
1
+ /* pm/c3-misc-panels.js — three panels of the console3 mockup that the Briefing, Work and Reports views lacked (content audit, planning/research/console3-content-parity.md).
2
+ * stopCard(rows) Briefing, Reflect lens: "What I could stop" (console3.html vBrief, line 1475): the agent's proposals, an icon and a line each.
3
+ * It is a read-only list, as in the mockup: nothing here acts, so nothing outward can be sent from it.
4
+ * forecastChecksCard(opts) Reports, Project family: "Forecast at each check, <initiative>" with the chart "Forecast date at each check against the promise"
5
+ * (console3.html driftSVG, line 3964). The points are the CHECKS of the plan from GET /api/c3-timeline?initiative=<id>
6
+ * (workspace/lib/routes-c3-timeline.js, which folds the plan ledger with scripts/plan-forecast.js checkFrames): per check the forecast end
7
+ * and the promised date. opts = { id, title }. With no id, the initiative with the largest slip among the Calendar's quarter rows is used.
8
+ * With fewer than two checks that carry a forecast, the card says so and draws nothing.
9
+ * Nothing here writes or sends. Built with el(): data never goes through innerHTML. */
10
+ import { el } from './dom.js';
11
+ import { pmGet } from './http.js';
12
+ import { c3ic } from './pm-work-view.js';
13
+ import { readQuarter } from './c3-plan.js';
14
+
15
+ /* ── What I could stop ── */
16
+ export function stopCard(rows) {
17
+ const list = (Array.isArray(rows) ? rows : []).filter((r) => r && r.text);
18
+ if (!list.length) return null;
19
+ return el('section', { class: 'card sect', 'data-c3pg': 'stop', 'aria-label': 'What I could stop' },
20
+ el('div', { class: 'sh2' }, el('h2', null, c3ic('moon', 'sm'), 'What I could stop')),
21
+ el('ul', { class: 'list', role: 'list' }, ...list.map((r) => el('li', { class: 'nrow', style: 'grid-template-columns:auto minmax(0,1fr)', 'data-stop-row': '' },
22
+ el('span', { class: 'ki' }, c3ic(r.icon || 'check')), el('div', { class: 'nb' }, r.text)))));
23
+ }
24
+
25
+ /* ── Forecast at each check ── */
26
+ const NSV = 'http://www.w3.org/2000/svg';
27
+ const sv = (tag, attrs, ...kids) => { const n = document.createElementNS(NSV, tag); for (const k of Object.keys(attrs || {})) n.setAttribute(k, attrs[k]); for (const c of kids) if (c) n.appendChild(c); return n; };
28
+ const svt = (x, y, t, a) => { const n = sv('text', { x, y, ...(a || {}) }); n.textContent = t; return n; };
29
+ const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
30
+ const dayNum = (iso) => Math.round(Date.parse(iso + 'T00:00:00Z') / 864e5);
31
+ const dlab = (n) => { const d = new Date(n * 864e5); return d.getUTCDate() + ' ' + MON[d.getUTCMonth()]; };
32
+
33
+ /** The chart, from [{ date: 'YYYY-MM-DD', forecast_end, promised }] (the frames that carry a forecast). */
34
+ export function forecastChart(frames) {
35
+ const W = 560; const H = 160; const x0 = 58; const x1 = W - 20; const y0 = 16; const y1 = H - 30;
36
+ const vals = frames.map((f) => dayNum(f.forecast_end)); const pr = dayNum(frames[0].promised || frames[0].forecast_end);
37
+ const mn = Math.min(pr, ...vals) - 2; const mx = Math.max(pr, ...vals) + 2; const n = frames.length;
38
+ const X = (i) => x0 + i * (x1 - x0) / (n - 1); const Y = (v) => y1 - (v - mn) / (mx - mn) * (y1 - y0);
39
+ const s = sv('svg', { class: 'mat', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Forecast date at each check against the promise', 'data-rp-chart': 'forecast-checks' });
40
+ [pr, Math.round((mn + mx) / 2), mx - 1].forEach((v) => s.append(sv('line', { x1: x0, x2: x1, y1: Y(v), y2: Y(v), stroke: 'var(--border)' }), svt(x0 - 5, Y(v) + 4, dlab(v), { 'text-anchor': 'end' })));
41
+ s.append(sv('line', { x1: x0, x2: x1, y1: Y(pr), y2: Y(pr), stroke: 'var(--success)', 'stroke-dasharray': '5 4', 'stroke-width': 2 }),
42
+ sv('polyline', { points: vals.map((v, i) => X(i).toFixed(1) + ',' + Y(v).toFixed(1)).join(' '), fill: 'none', stroke: 'var(--destructive)', 'stroke-width': 2.5, 'stroke-linejoin': 'round' }));
43
+ vals.forEach((v, i) => {
44
+ const c = sv('circle', { cx: X(i), cy: Y(v), r: i === n - 1 ? 6 : 4, fill: v > pr ? 'var(--destructive)' : 'var(--success)', stroke: 'var(--card)', 'stroke-width': 2 });
45
+ const t = sv('title', {}); t.textContent = dlab(dayNum(frames[i].date)) + ': forecast ' + dlab(v) + (v > pr ? ', ' + (v - pr) + ' days after the promise' : ', on the promise');
46
+ c.appendChild(t); s.append(c, svt(X(i), H - 12, dlab(dayNum(frames[i].date)), { 'text-anchor': 'middle' }));
47
+ });
48
+ s.append(svt(x1, Y(pr) - 6, 'promised ' + dlab(pr), { 'text-anchor': 'end', fill: 'var(--success)' }));
49
+ return s;
50
+ }
51
+
52
+ const done = new Map(); // initiative id → the frames that carry a forecast, or { error }
53
+ const pending = new Map();
54
+ function readChecks(id) {
55
+ if (done.has(id)) return Promise.resolve(done.get(id));
56
+ if (!pending.has(id)) {
57
+ pending.set(id, pmGet('/api/c3-timeline?initiative=' + encodeURIComponent(id)).then((j) => {
58
+ const r = j && j.ok !== false && Array.isArray(j.frames) ? j.frames.filter((f) => f.forecast_end && f.date) : { error: (j && j.error) || 'unreadable' };
59
+ done.set(id, r); pending.delete(id); return r;
60
+ }, (e) => { pending.delete(id); return { error: String((e && e.message) || e) }; }));
61
+ }
62
+ return pending.get(id);
63
+ }
64
+ /** With no initiative named: the one with the largest slip among the quarter rows that have a promise. */
65
+ async function pickInitiative() {
66
+ const j = await readQuarter().catch(() => null);
67
+ const rows = ((j && j.gantt) || []).filter((g) => g.status !== 'no plan' && g.promised);
68
+ rows.sort((a, b) => (b.slip_days || 0) - (a.slip_days || 0));
69
+ return rows[0] ? { id: rows[0].id, title: rows[0].title || rows[0].id } : null;
70
+ }
71
+
72
+ export function forecastChecksCard(opts = {}) {
73
+ const head = (t) => el('h3', null, c3ic('clock'), 'Forecast at each check' + (t ? ', ' + t : ''));
74
+ const box = el('section', { class: 'card', 'data-rp-section': 'forecast-checks' });
75
+ const paint = (t, r) => {
76
+ if (r && r.error) box.replaceChildren(head(t), el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status' }, 'The checks could not be read: ' + r.error));
77
+ else if (!r || r.length < 2) box.replaceChildren(head(t), el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status', 'data-rp-empty': '' }, 'Fewer than two checks have a forecast date yet, so there is no history to draw.'));
78
+ else box.replaceChildren(head(t), forecastChart(r));
79
+ };
80
+ const run = async () => {
81
+ const who = opts.id ? { id: opts.id, title: opts.title } : await pickInitiative();
82
+ if (!who) { box.replaceChildren(head(''), el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status', 'data-rp-empty': '' }, 'No initiative has a promised date yet, so there is nothing to check a forecast against.')); return; }
83
+ if (done.has(who.id)) { paint(who.title, done.get(who.id)); return; }
84
+ box.replaceChildren(head(who.title), el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status' }, 'Reading the checks.'));
85
+ paint(who.title, await readChecks(who.id));
86
+ };
87
+ run();
88
+ return box;
89
+ }
@@ -0,0 +1,163 @@
1
+ /* pm/c3-plan-panels.js — the commitments panels of the console3 mockup, built for the Initiatives Timeline (All scope) and the Lifecycle Plan and Monitor stages.
2
+ * Ported from console3.html: progSVG (2716), commits (2704), the Timeline All scope (3841), Why it slips and Dependencies (3850), Path and dates
3
+ * and Progress and timing (3597, 3598).
4
+ *
5
+ * ensureCommits(V) reads the commitments once; V.commits = { state, rows, range, today }. V.render() runs when they land.
6
+ * progressCard(V, title, sub, o) the "Progress of commitments: on time, at risk, delayed and overdue" chart in a card (o.cap rows, o.onOpen(id, title)).
7
+ * whySlipsCard(V, causes) bars of days past plan per cause. causes: { head, rows: [[label, fraction, color, text]], note } | null
8
+ * dependenciesCard(list) rows { cls, text }
9
+ * slipFacts(frames) derives the causes and the dependencies from the whole-plan frames of GET /api/c3-timeline
10
+ *
11
+ * DATA. Real mode: initiatives come from GET /api/calendar-views (view=quarter, the `gantt` rows the Calendar Quarter draws: promised date, forecast,
12
+ * progress, status); the PM's own dated tasks come from GET /api/pm-items (an open task is on time until its due date passes, then overdue). Nothing is
13
+ * invented: an initiative with no promised date is not drawn, a task with no due date is not drawn, and a bar fills only where progress is recorded.
14
+ * Demo (/c3.html?demo=1): window.__c3planfx (c3-demo-plan2.js) holds the mockup's rows in the same row shape. Built with el(); every string from data is a text node. */
15
+ import { el } from './dom.js';
16
+ import { pmGet } from './http.js';
17
+ import { c3ic } from './pm-work-view.js';
18
+ import { card } from './c3-init-extras.js';
19
+
20
+ const NS = 'http://www.w3.org/2000/svg';
21
+ const DAY = 864e5;
22
+ const MN = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
23
+ const OK = 'var(--success)'; const WARN = 'var(--warning)'; const BAD = 'var(--destructive)'; const MFG = 'var(--muted-foreground)';
24
+ const ST = { done: { l: 'Done', c: OK }, ok: { l: 'On time', c: OK }, risk: { l: 'At risk', c: WARN }, late: { l: 'Delayed', c: BAD }, over: { l: 'Overdue', c: BAD }, park: { l: 'Parked', c: MFG } };
25
+ const SEVR = { over: 0, late: 1, risk: 2, ok: 3, park: 4, done: 5 };
26
+ const STKEY = { 'on time': 'ok', 'at risk': 'risk', delayed: 'late', overdue: 'over' };
27
+ const utc = (s) => Date.parse(String(s).slice(0, 10) + 'T00:00:00Z');
28
+ const ymd = (t) => new Date(t).toISOString().slice(0, 10);
29
+ const localYmd = () => { const d = new Date(); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); };
30
+ const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
31
+ const isPhone = () => typeof matchMedia === 'function' && matchMedia('(max-width:640px)').matches;
32
+ const sv = (tag, attrs, ...kids) => { const n = document.createElementNS(NS, tag); for (const k of Object.keys(attrs || {})) if (attrs[k] != null) n.setAttribute(k, attrs[k]); for (const c of kids) if (c) n.appendChild(c); return n; };
33
+ const tx = (x, y, s, attrs) => { const n = sv('text', { x, y, ...(attrs || {}) }); n.textContent = s; return n; };
34
+ const dlab = (iso) => { const t = utc(iso); const d = new Date(t); return d.getUTCDate() + ' ' + MN[d.getUTCMonth()]; };
35
+
36
+ /* ── the reads ── */
37
+ /** The range the chart draws: the first of this month and 61 days on (the mockup's 1 to 61). */
38
+ function rangeOf(today) { const d = new Date(utc(today)); const s = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1); return { start: ymd(s), end: ymd(s + 60 * DAY) }; }
39
+
40
+ async function readReal(V, today) {
41
+ const ws = V.ctx.ws ? '&ws=' + encodeURIComponent(V.ctx.ws) : '';
42
+ const range = rangeOf(today);
43
+ const [cal, items] = await Promise.all([pmGet('/api/calendar-views?view=quarter&date=' + today + ws).catch(() => null), pmGet('/api/pm-items').catch(() => null)]);
44
+ const rows = []; const day = (s) => s;
45
+ const gantt = cal && cal.ok !== false && Array.isArray(cal.gantt) ? cal.gantt : [];
46
+ gantt.forEach((g) => {
47
+ const st = STKEY[g.status]; if (!st || !g.promised) return;
48
+ const f = g.forecast && (g.forecast.end || g.forecast.start); const p = g.planned && g.planned.start;
49
+ rows.push({ id: g.id, n: g.title || g.id, kind: 'init', start: day(p || g.promised), due: day(g.promised), fc: f ? day(f) : day(g.promised), pct: typeof g.progress === 'number' ? g.progress : null, st,
50
+ why: (g.title || g.id) + ': ' + g.status + (g.slip_days > 0 ? ', ' + g.slip_days + ' days past the plan' : ''), go: g.id });
51
+ });
52
+ const seen = new Set();
53
+ if (items && items.ok !== false && items.lists) Object.values(items.lists).forEach((l) => (l || []).forEach((i) => {
54
+ if (!i || i.done || seen.has(i.id) || !/^\d{4}-\d{2}-\d{2}/.test(String(i.due || ''))) return; seen.add(i.id);
55
+ const d = String(i.due).slice(0, 10); const over = d < today;
56
+ rows.push({ id: 'item-' + i.id, n: i.title, kind: 'task', due: d, st: over ? 'over' : 'ok', why: i.title + (over ? ', overdue' : ', due ' + d), late: over ? Math.round((utc(today) - utc(d)) / DAY) : 0 });
57
+ }));
58
+ return { rows, range, today, real: true, error: cal && cal.ok === false ? cal.error || null : null };
59
+ }
60
+
61
+ /** Start the reads once. V.commits.state: 'loading' | 'ready'. */
62
+ export function ensureCommits(V) {
63
+ if (V.commits) return V.commits;
64
+ const C = V.commits = { state: 'loading', rows: [], range: null, today: localYmd() };
65
+ const fx = typeof window !== 'undefined' && window.__c3planfx;
66
+ const done = (d) => { if (V.disposed) return; Object.assign(C, d, { state: 'ready' }); if (V.render) V.render(); };
67
+ if (window.__c3demo && fx) { Promise.resolve().then(() => done({ rows: fx.rows, range: rangeOf(C.today), real: false })); return C; }
68
+ readReal(V, C.today).then(done).catch(() => done({ rows: [], range: rangeOf(C.today), real: true, error: 'The commitments could not be read.' }));
69
+ return C;
70
+ }
71
+
72
+ /* ── progSVG ── */
73
+ function progSvg(all, range, today, o) {
74
+ const a = 1; const b = Math.round((utc(range.end) - utc(range.start)) / DAY) + 1;
75
+ const day = (s) => Math.round((utc(s) - utc(range.start)) / DAY) + 1;
76
+ const TD = day(today); const cap = o.cap || 9;
77
+ const R = all.map((c) => ({ ...c, d: day(c.due), s: c.start ? day(c.start) : null, f: c.fc ? day(c.fc) : null }))
78
+ .filter((c) => { const lo = c.kind === 'init' ? c.s : c.d; const hi = Math.max(c.d, c.f || c.d); return (c.st === 'over' && TD >= a && TD <= b) || (hi >= a && lo <= b); })
79
+ .sort((x, y) => SEVR[x.st] - SEVR[y.st] || x.d - y.d).slice(0, cap);
80
+ const W = 720; const LW = 150; const SW = 104; const CW = W - LW - SW; const N = b - a + 1; const dw = CW / N; const rh = 30; const top = 28; const H = top + R.length * rh + 8;
81
+ const Xl = (n) => LW + (n - a) * dw; const Xc = (n) => Xl(n) + dw / 2;
82
+ const dl = (n) => dlab(ymd(utc(range.start) + (n - 1) * DAY));
83
+ const cid = 'pgc' + (o.uid || 'x');
84
+ const s = sv('svg', { class: 'pg', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Progress of commitments: on time, at risk, delayed and overdue', 'data-plan-chart': o.uid || '' }, sv('defs', {}, sv('clipPath', { id: cid }, sv('rect', { x: LW, y: 18, width: CW, height: H - 18 }))));
85
+ for (let n = a; n <= b; n++) { const d = new Date(utc(range.start) + (n - 1) * DAY); if (d.getUTCDay() === 1) s.append(sv('line', { x1: Xl(n), x2: Xl(n), y1: 18, y2: H - 4, stroke: 'var(--border)' }), tx(Xl(n) + 3, 14, String(d.getUTCDate()))); }
86
+ if (TD >= a && TD <= b) { const now = new Date(); const fr = clamp((now.getHours() + now.getMinutes() / 60 - 8) / 10, 0, 1); const tX = Xl(TD) + dw * fr; s.append(sv('rect', { x: Xl(TD), y: 20, width: dw, height: H - 24, fill: 'var(--accw)', rx: 6 }), sv('line', { x1: tX, x2: tX, y1: 20, y2: H - 4, stroke: 'var(--foreground)', 'stroke-width': 2 })); }
87
+ const stat = (c) => (c.st === 'over' ? 'Overdue ' + (c.late || Math.max(1, TD - c.d)) + ' d' : c.st === 'late' ? 'Delayed +' + (c.f - c.d) + ' d' : ST[c.st].l);
88
+ const phone = isPhone();
89
+ R.forEach((c, i) => {
90
+ const y = top + i * rh; const cy = y + rh / 2; const S = ST[c.st]; const nm = c.n.length > 17 ? c.n.slice(0, 16) + '…' : c.n; const dim = c.st === 'done' || c.st === 'park';
91
+ const g = sv('g', { opacity: dim ? 0.6 : 1, 'data-plan-row': c.id, 'data-status': c.st, ...(c.go && o.onOpen && !phone ? { role: 'button', tabindex: '0', 'data-a': 'goinit', 'data-k': String(c.go), 'aria-label': 'Open ' + c.n + ': ' + stat(c), style: 'cursor:pointer' } : {}) });
92
+ g.append(sv('rect', { x: 0, y, width: W, height: rh, fill: 'transparent' }), tx(0, cy + 4, nm, { class: 't' }));
93
+ if (c.kind === 'init') {
94
+ const xs = Xl(c.s); const xe = Xc(c.d); const has = c.pct != null; const xf = xs + (xe - xs) * (c.pct || 0); const ex = clamp((TD - c.s) / Math.max(1, c.d - c.s), 0, 1); const xt = xs + (xe - xs) * ex;
95
+ const bars = sv('g', { 'clip-path': `url(#${cid})` }, sv('rect', { x: xs, y: cy - 5, width: Math.max(2, xe - xs), height: 10, rx: 5, fill: S.c, 'fill-opacity': 0.2 }));
96
+ if (has) bars.append(sv('rect', { x: xs, y: cy - 5, width: Math.max(2, xf - xs), height: 10, rx: 5, fill: S.c }));
97
+ if (c.st !== 'park') bars.append(sv('line', { x1: xt, x2: xt, y1: cy - 9, y2: cy + 9, stroke: 'var(--foreground)', 'stroke-width': 2 }));
98
+ if (c.f > c.d) bars.append(sv('rect', { x: xe, y: cy - 2.5, width: Xc(c.f) - xe, height: 5, rx: 2.5, fill: BAD, 'fill-opacity': 0.6 }), sv('circle', { cx: Xc(c.f), cy, r: 6, fill: BAD, stroke: 'var(--card)', 'stroke-width': 2 }));
99
+ bars.append(sv('path', { d: `M${xe} ${cy - 9} l8 9 -8 9 -8 -9z`, fill: 'var(--card)', stroke: S.c, 'stroke-width': 2 }));
100
+ g.append(bars);
101
+ if (c.d > b) g.append(tx(Xl(b + 1) - 4, cy - 12, 'due ' + dl(c.d) + (c.f > c.d ? ' → ' + dl(c.f) : ''), { 'text-anchor': 'end', style: 'font-size:var(--fs-1)' }));
102
+ } else {
103
+ if (c.st === 'over') g.append(sv('line', { x1: Xc(Math.max(a, c.d)), x2: Xc(Math.min(b, TD)), y1: cy, y2: cy, stroke: BAD, 'stroke-width': 3, 'stroke-dasharray': '5 4', 'stroke-linecap': 'round' }));
104
+ if (c.d >= a && c.d <= b) { if (c.st === 'done') g.append(sv('circle', { cx: Xc(c.d), cy, r: 8, fill: OK, 'fill-opacity': 0.18 }), sv('path', { d: `M${Xc(c.d) - 4} ${cy} l3 3 5 -6`, fill: 'none', stroke: OK, 'stroke-width': 2, 'stroke-linecap': 'round' })); else g.append(sv('path', { d: `M${Xc(c.d)} ${cy - 8} l7 8 -7 8 -7 -8z`, fill: c.st === 'over' ? S.c : 'var(--card)', stroke: S.c, 'stroke-width': 2 })); }
105
+ }
106
+ g.append(tx(W - SW + 10, cy + 4, stat(c), { style: 'fill:' + S.c + ';font-weight:500' }));
107
+ const t = sv('title', {}); t.textContent = c.why || c.n; g.append(t);
108
+ if (c.go && o.onOpen && !phone) { const open = () => o.onOpen(c.go, c.n); g.addEventListener('click', open); g.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } }); }
109
+ s.append(g);
110
+ });
111
+ return { svg: s, shown: R, stat, hasFill: R.some((c) => c.kind === 'init' && c.pct != null), more: all.length - R.length };
112
+ }
113
+
114
+ /** The "Progress of commitments" card. title/sub as the mockup's heading. o: { cap, uid, onOpen(id, title) }. */
115
+ export function progressCard(V, title, sub, o) {
116
+ const C = ensureCommits(V); const head = ['target', title, sub || ''];
117
+ const c = (...k) => card(head[0], head[1], head[2], ...k);
118
+ if (C.state !== 'ready') return c(el('div', { class: 'fold', 'data-plan-loading': '' }, c3ic('clock', 'sm'), 'Reading the commitments…'));
119
+ const rows = C.rows;
120
+ const out = rows.length ? progSvg(rows, C.range, C.today, o || {}) : null;
121
+ if (!out || !out.shown.length) { const e = c(el('div', { class: 'fold' }, c3ic('moon', 'sm'), C.error || 'No initiative has a promised date and no task has a due date in these two months. Give an initiative a promised date and a plan, or a task a due date, and it is drawn here with its forecast.')); e.setAttribute('data-plan-empty', 'true'); e.setAttribute('data-plan-card', (o && o.uid) || ''); return e; }
122
+ const sec = c(out.svg, isPhone() && o && o.onOpen ? el('ul', { class: 'list', role: 'list', style: 'margin:0;list-style:none;padding:0', 'data-plan-phone-rows': '' }, out.shown.filter((r) => r.go).map((r) => el('li', null,
123
+ el('button', { type: 'button', class: 'nrow', style: 'width:100%;min-height:44px;border:0;background:none;text-align:left;display:flex;justify-content:space-between;gap:8px;align-items:center;cursor:pointer', 'aria-label': 'Open ' + r.n, onClick: () => o.onOpen(r.go, r.n) },
124
+ el('b', null, r.n), el('span', { class: 'mono', style: 'font-size:var(--fs-2);color:' + ST[r.st].c }, out.stat({ ...r, d: Math.round((utc(r.due) - utc(C.range.start)) / DAY) + 1, f: r.fc ? Math.round((utc(r.fc) - utc(C.range.start)) / DAY) + 1 : 0 })))))) : null,
125
+ out.more > 0 && !window.__c3demo ? el('div', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, '+' + out.more + ' more commitments not drawn') : null,
126
+ el('div', { class: 'pgl' }, el('span', null, '◇ due'), el('span', { style: 'color:' + BAD }, '● forecast'), out.hasFill ? el('span', null, '▬ actual progress') : el('span', null, '▬ planned span (no progress is recorded)'), el('span', null, '| where it should be today'),
127
+ el('span', { style: 'color:' + BAD }, '┅ overdue'), el('span', { style: 'color:' + OK }, '■ on time'), el('span', { style: 'color:' + WARN }, '■ at risk'), el('span', { style: 'color:' + BAD }, '■ delayed')));
128
+ sec.setAttribute('data-plan-card', (o && o.uid) || '');
129
+ return sec;
130
+ }
131
+
132
+ /* ── Why it slips and Dependencies ── */
133
+ export function whySlipsCard(causes) {
134
+ if (!causes) return card('alert', 'Why it slips', '', el('div', { class: 'fold', 'data-plan-empty': 'why' }, c3ic('check', 'sm'), 'No initiative is past its promised date, so nothing slips.'));
135
+ const bars = el('div', { class: 'hbs' }, causes.rows.map((r, i) => el('div', { class: 'hbr', style: '--c:' + r[2] }, el('div', { class: 'l' }, el('span', null, r[0]), el('i', null, r[3])), el('div', { class: 'tk' }, el('u', { style: 'width:' + Math.max(2, Math.min(100, r[1] * 100)) + '%;animation-delay:' + (i * 90) + 'ms' })))));
136
+ const s = card('alert', 'Why it slips', causes.head, bars, el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, causes.note));
137
+ s.setAttribute('data-plan-why', '');
138
+ return s;
139
+ }
140
+ export function dependenciesCard(list) {
141
+ const s = card('org', 'Dependencies', '', list.length ? el('ul', { class: 'chgl' }, list.map((d) => el('li', { class: d.cls }, c3ic('alert', 'sm'), el('span', null, d.text))))
142
+ : el('div', { class: 'fold', 'data-plan-empty': 'deps' }, c3ic('check', 'sm'), 'No item waits on a late item. Dependencies between initiatives are not recorded, so only those inside one initiative are checked.'));
143
+ s.setAttribute('data-plan-deps', '');
144
+ return s;
145
+ }
146
+
147
+ /** Causes and dependencies from the whole-plan frames (GET /api/c3-timeline?initiative=). byInit: { id: { title, frames } }. Days past plan are per item, so chains overlap and do not add up. */
148
+ export function slipFacts(byInit) {
149
+ let worst = null; const deps = [];
150
+ Object.keys(byInit).forEach((id) => {
151
+ const T = byInit[id]; const frames = (T && T.frames) || []; const f = frames[frames.length - 1]; if (!f) return;
152
+ if (f.slip_days > 0 && (!worst || f.slip_days > worst.f.slip_days)) worst = { id, f, title: T.title || id };
153
+ const by = new Map(f.items.map((t) => [t.id, t]));
154
+ f.items.forEach((t) => (t.after || []).forEach((k) => { const p = by.get(k); if (!p || p.state === 'done') return; const ps = p.forecast && p.forecast.status;
155
+ if (ps === 'delayed' || ps === 'overdue' || ps === 'at risk') deps.push({ cls: ps === 'at risk' ? 'warn' : 'bad', text: (T.title || id) + ': ' + (t.title || t.id) + ' waits on ' + (p.title || p.id) + ', which is ' + ps + '.', w: ps === 'at risk' ? 1 : 0 }); }));
156
+ });
157
+ deps.sort((a, b) => a.w - b.w);
158
+ if (!worst) return { causes: null, deps: deps.slice(0, 5) };
159
+ const items = worst.f.items.filter((t) => t.state !== 'done' && t.forecast && t.forecast.slip_days > 0).sort((a, b) => b.forecast.slip_days - a.forecast.slip_days).slice(0, 4);
160
+ const top = Math.max(1, ...items.map((t) => t.forecast.slip_days));
161
+ return { causes: { head: worst.title + ', ' + worst.f.slip_days + ' day' + (worst.f.slip_days === 1 ? '' : 's'),
162
+ rows: items.map((t, i) => [t.title || t.id, t.forecast.slip_days / top, i === 0 ? BAD : WARN, t.forecast.slip_days + ' d']), note: 'Days past each item\'s planned end. Items on one chain overlap, so the days do not add up. A cause such as scope or capacity is not recorded.' }, deps: deps.slice(0, 5) };
163
+ }
@@ -0,0 +1,192 @@
1
+ /* pm/c3-plan.js — the small pieces /c3.html adds around the real views so each tab has the mockup's furniture (console3.html):
2
+ * planNode() the "Plan: Decide what gates the day" lens row and the card deck (mountBriefing), one instance per tab. All
3
+ * instances share one read of GET /api/briefing (briefing-deck.js keeps it), so a page with five decks asks once.
4
+ * dueCard() the Calendar's "Due and moving today" list (mockup vCal), from GET /api/pm-items: the items due today or overdue.
5
+ * crossLinks(go) the line under an Initiatives lens header: "Calendar: N events today >" and "Work: N open items >" (mockup
6
+ * c3 lkrow), from GET /api/calendar-views (day) and GET /api/pm-items.
7
+ * newInitLine(ctx) the Initiatives add line "Start something new: one line is enough": the input, a disabled microphone, the spark
8
+ * button (opens the real new-initiative sheet with the line as its title) and the "try" suggestion pills.
9
+ * Nothing here writes; the sheet the spark button opens is the one the main app uses. The microphone is drawn disabled: nothing on
10
+ * a page may record, and the on-device model belongs to a Notes document. */
11
+ import { el } from './dom.js';
12
+ import { pmGet, pmPost } from './http.js';
13
+ import { c3ic, c3ib } from './pm-work-view.js';
14
+ import { mountBriefing, setBriefingScope } from './briefing-deck.js';
15
+ import { needRow } from './c3-brief.js';
16
+
17
+ /** Re-point a plan node at another scope (the Work tab's lens). Only the demo has a deck per scope. */
18
+ export const setPlanScope = (host, scope) => setBriefingScope(host, scope);
19
+
20
+ export function planNode(scope) {
21
+ const host = el('div', { 'data-c3pg': 'plan' });
22
+ mountBriefing(host, { noTicker: true, scope });
23
+ return host;
24
+ }
25
+
26
+ let items = null;
27
+ const readItems = () => (items || (items = pmGet('/api/pm-items').then((j) => (j && j.ok ? j : null), () => null)));
28
+ export const freshItems = () => { items = null; return readItems(); };
29
+
30
+ const dayDiff = (a, b) => Math.round((Date.parse(a + 'T00:00:00Z') - Date.parse(b + 'T00:00:00Z')) / 86400000);
31
+
32
+ export function dueCard() {
33
+ const count = el('span', { class: 'bdg g' }, '0');
34
+ const list = el('ul', { class: 'list', role: 'list' }, el('li', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the repository'));
35
+ const box = el('section', { class: 'card sect', 'data-c3pg': 'due-today', 'aria-label': 'Due and moving today' },
36
+ el('div', { class: 'sh2' }, el('h2', null, c3ic('clock', 'sm'), 'Due and moving today ', count)), list);
37
+ readItems().then((j) => {
38
+ if (!j) { list.replaceChildren(el('li', { class: 'fold' }, c3ic('alert', 'sm'), 'Your items could not be read.')); return; }
39
+ if (Array.isArray(j.due_today)) { // optional (demo): the mockup's "Due and moving today" rows, each with its own sub line, time-left bar and status
40
+ count.textContent = String(j.due_today.length);
41
+ list.replaceChildren(...j.due_today.map((r) => {
42
+ const col = r.tone === 'bad' ? 'var(--destructive)' : r.tone === 'warn' ? 'var(--warning)' : 'var(--acc)';
43
+ return el('li', { class: 'nrow', 'data-due-row': r.id, style: 'grid-template-columns:auto minmax(0,1fr) auto' },
44
+ el('span', { class: 'ki', style: r.tone === 'warn' ? 'color:var(--warning)' : r.tone === 'bad' ? 'color:var(--destructive)' : null }, c3ic('alert')),
45
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, r.title), el('div', { class: 'chips' }, el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, r.sub)),
46
+ r.left ? el('div', { class: 'chips2', style: 'margin-top:6px;gap:8px' }, el('div', { class: 'fuse' }, el('i', { style: 'width:' + Math.round(r.left.pct * 100) + '%;background:' + (r.left.tone === 'warn' ? 'var(--warning)' : 'var(--acc)') })),
47
+ el('span', { class: 'mono', style: 'font-size:var(--fs-1);color:' + (r.left.tone === 'warn' ? 'var(--warning)' : 'var(--acc)') }, r.left.text)) : null),
48
+ el('span', { class: 'mono', style: 'font-size:var(--fs-2);align-self:center;color:' + col }, r.right));
49
+ }));
50
+ return;
51
+ }
52
+ const rows = ((j.lists || {}).today || []).filter((i) => !i.done);
53
+ count.textContent = String(rows.length);
54
+ list.replaceChildren(...(rows.length ? rows.map((i) => {
55
+ const late = i.due && i.due < j.today ? dayDiff(j.today, i.due) : 0;
56
+ return el('li', { class: 'nrow', 'data-due-row': i.id },
57
+ el('span', { class: 'ki' + (late ? ' hot' : '') }, c3ic(late ? 'alert' : 'clock')),
58
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, i.title),
59
+ el('div', { class: 'chips' }, i.due ? el('span', { class: 'chip2' }, c3ic('cal', 'sm'), i.due) : null, i.priority ? el('span', { class: 'fl ' + i.priority.toLowerCase() }, i.priority) : null)),
60
+ late ? el('span', { class: 'mono', style: 'font-size:var(--fs-2);color:var(--destructive)' }, late + ' d late') : null);
61
+ }) : [el('li', { class: 'fold' }, c3ic('check', 'sm'), 'Nothing is due today.')]));
62
+ });
63
+ return box;
64
+ }
65
+
66
+ export function crossLinks(go) {
67
+ const cal = el('button', { type: 'button', class: 'chip2 lk', 'data-c3-link': 'cal', onclick: () => go('cal') }, c3ic('cal', 'sm'), 'Calendar: reading');
68
+ const work = el('button', { type: 'button', class: 'chip2 lk', 'data-c3-link': 'work', onclick: () => go('work') }, c3ic('check', 'sm'), 'Work: reading');
69
+ const d = new Date(); const p = (n) => String(n).padStart(2, '0');
70
+ pmGet('/api/calendar-views?view=day&date=' + d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate())).then((j) => {
71
+ const n = j && j.ok !== false ? (j.cells || []).reduce((t, c) => t + c.events.length, 0) : null;
72
+ cal.replaceChildren(c3ic('cal', 'sm'), n === null ? 'Calendar: not read' : 'Calendar: ' + n + ' event' + (n === 1 ? '' : 's') + ' today', c3ic('next', 'sm'));
73
+ }, () => cal.replaceChildren(c3ic('cal', 'sm'), 'Calendar: not read'));
74
+ readItems().then((j) => {
75
+ const n = j ? (j.items || []).filter((i) => !i.done).length : null;
76
+ work.replaceChildren(c3ic('check', 'sm'), n === null ? 'Work: not read' : 'Work: ' + n + ' open item' + (n === 1 ? '' : 's'), c3ic('next', 'sm'));
77
+ });
78
+ return el('div', { class: 'lkrow', 'data-c3pg': 'cross-links' }, cal, work);
79
+ }
80
+
81
+ /* The Calendar's own gantt rows for the quarter (GET /api/calendar-views), read once per page: the pulse and the portfolio card share them. */
82
+ let quarter = null;
83
+ export function readQuarter() {
84
+ if (!quarter) {
85
+ const d = new Date(); const p2 = (n) => String(n).padStart(2, '0');
86
+ quarter = pmGet('/api/calendar-views?view=quarter&date=' + d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()));
87
+ }
88
+ return quarter;
89
+ }
90
+
91
+ /* The Reports tab's "Portfolio status: promise against forecast" (mockup r2port): promised and forecast dates of the initiatives that have one. */
92
+ export function portfolioCard() {
93
+ const body = el('div', null, el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status' }, 'Reading the repository'));
94
+ const box = el('section', { class: 'card', 'data-c3pg': 'portfolio' }, el('h3', null, c3ic('tree'), 'Portfolio status ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'promise against forecast')), body);
95
+ const ST = { 'on time': ['check', 'var(--success)'], 'at risk': ['alert', 'var(--warning)'], delayed: ['alert', 'var(--destructive)'], overdue: ['alert', 'var(--destructive)'] };
96
+ const end = (o) => (o && (o.end || o.start)) || '';
97
+ const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
98
+ let mock = false; // rows that carry `pulse` (the demo's) are drawn as the mockup draws them: dates as 24 Oct; real rows keep the ISO date
99
+ const dlab = (v) => { const m = mock && /^\d{4}-(\d{2})-(\d{2})$/.exec(v || ''); return m ? Number(m[2]) + ' ' + MON[Number(m[1]) - 1] : v || ''; }; // the mockup's dlab: 24 Oct
100
+ readQuarter().then((j) => {
101
+ if (!j || j.ok === false) { body.replaceChildren(el('div', { class: 'fold w' }, c3ic('alert', 'sm'), 'The initiatives could not be read.')); return; }
102
+ const all = j.gantt || []; const rows = all.filter((g) => g.status !== 'no plan'); mock = rows.some((g) => g.pulse);
103
+ if (!rows.length) { body.replaceChildren(el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'No initiative has a promised date yet.'), all.length ? el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, all.length + ' initiatives have no plan date.') : null); return; }
104
+ body.replaceChildren(el('table', { class: 'tb', style: (mock ? 'width:auto;' : 'width:100%;') + 'border-collapse:collapse;font-size:var(--fs-2)' },
105
+ el('thead', null, el('tr', null, el('th', { style: 'text-align:left' }), el('th', { class: 'mono', style: 'font-size:var(--fs-1);padding:0 8px' }, 'PROMISED'), el('th', { class: 'mono', style: 'font-size:var(--fs-1);padding:0 8px' }, 'FORECAST'), el('th'))),
106
+ el('tbody', null, ...rows.slice(0, 6).map((g) => { const s = mock ? (end(g.forecast) > (g.promised || '') ? ST.delayed : ['check', 'var(--success)']) : (ST[g.status] || ['moon', 'var(--muted-foreground)']); // the mockup's colour is the dates: past the promise is red, otherwise green
107
+ return el('tr', { 'data-portfolio-row': g.id }, el('td', { style: 'padding:' + (mock ? '5px 8px 5px 0' : '4px 0') + ';font-weight:600' }, g.title || g.id), el('td', { class: 'mono', style: mock ? 'padding:0 8px' : null }, dlab(g.promised || '')), el('td', { class: 'mono', style: (mock ? 'padding:0 8px;' : '') + 'color:' + s[1] }, dlab(end(g.forecast))), el('td', { style: mock ? null : 'color:' + s[1] }, c3ic(s[0], 'sm'))); }))),
108
+ // Real rows keep what this call always did: with six rows or fewer it passed null, which replaceChildren writes as the text "null". Demo rows (mock) pass nothing.
109
+ ...(rows.length > 6 ? [el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, '+' + (rows.length - 6) + ' more in the Calendar tab')] : mock ? [] : [null]));
110
+ }, () => body.replaceChildren(el('div', { class: 'fold w' }, c3ic('alert', 'sm'), 'The initiatives could not be read.')));
111
+ return box;
112
+ }
113
+
114
+ const TRY = ['Self-serve billing for teams', 'Why returns hurt', 'Fix flaky CI on search'];
115
+ const INTAKE_STEPS = ['Checking what was rejected before', 'Running the sharpness test', 'Choosing a lane', 'Finding an anchor'];
116
+ /* The mockup's c3nistart (console3.html line 4790): a pill or the spark runs the intake IN PLACE. A card under the line ticks four steps every 0.65 s, then says what the
117
+ * agent suggests (lane, anchor, prior rejection). The real POST /api/intake/draft runs behind the steps. Nothing is created here: "Create it" opens the real form
118
+ * with the line, where the anchor is chosen and the create is pressed, as before. */
119
+ export function newInitLine(ctx) {
120
+ const input = el('input', { type: 'text', placeholder: 'Start something new: one line is enough', 'aria-label': 'New initiative', autocomplete: 'off', 'data-c3pg': 'new-init' });
121
+ const slot = el('div', { 'data-c3pg': 'new-init-card' });
122
+ let run = 0;
123
+ const openForm = () => import('./new-initiative-sheet.js').then((m) => m.openNewInitiative({ repo: ctx.repo, ws: ctx.ws, anchors: (ctx.rows || []).map((r) => r.anchor), prefill: input.value.trim() ? { title: input.value.trim() } : null, onAdded: () => { input.value = ''; slot.replaceChildren(); if (ctx.onAdded) ctx.onAdded(); } }));
124
+ const reset = () => { run++; slot.replaceChildren(); input.value = ''; input.focus(); };
125
+ const steps = (text, n) => el('section', { class: 'card sect', 'data-c3pg': 'new-init-run' }, el('div', { class: 'sh2' }, el('h2', null, c3ic('spark', 'sm'), text)),
126
+ el('ul', { class: 'chgl' }, INTAKE_STEPS.map((st, i) => el('li', { class: i < n ? 'ok' : '' }, i < n ? c3ic('check', 'sm') : i === n ? el('span', { class: 'c3spin' }, c3ic('spark', 'sm')) : el('span', { style: 'width:14px' }), el('span', { class: i > n ? 'dim' : '' }, st)))));
127
+ const answer = (text, d) => {
128
+ const pr = d.prior_rejections || []; const conf = typeof d.confidence === 'number' ? Math.round(d.confidence * 100) : null;
129
+ return el('section', { class: 'card sect', 'data-c3pg': 'new-init-done' }, el('div', { class: 'sh2' }, el('h2', null, c3ic('spark', 'sm'), 'The agent suggests'), el('span', { class: 'r' }, c3ib('Start over', 'x', { onClick: reset }, 'sm'))),
130
+ el('div', { class: 'case' }, el('div', { class: 'sit' }, 'A ' + (d.lane || 'feature') + ' lane fits ' + text + '. Nothing is created until you press Create it.' + (conf != null ? ' ' + conf + '% sure.' : '')),
131
+ el('ul', { class: 'chgl' },
132
+ el('li', null, c3ic('init', 'sm'), el('span', null, 'Lane guess: ' + (d.lane || 'none')), el('span', { class: 'tag' }, 'Lane')),
133
+ el('li', { class: d.anchor_suggestion ? 'ok' : 'bad' }, c3ic(d.anchor_suggestion ? 'target' : 'alert', 'sm'), el('span', null, d.anchor_suggestion ? 'Anchor suggestion: ' + d.anchor_suggestion : 'No anchor yet: pick a KR or a health budget'), el('span', { class: 'tag' }, 'Anchor')),
134
+ el('li', { class: pr.length ? 'warn' : 'ok' }, c3ic(pr.length ? 'alert' : 'check', 'sm'), el('span', null, pr.length ? 'Close to something rejected before: ' + pr.map((r) => r.title || r.path || String(r)).join('; ') : 'Nothing like it was rejected before'), el('span', { class: 'tag' }, 'prior rejection')))),
135
+ el('div', { class: 'chips2' }, el('button', { type: 'button', class: 'chip2 lk', 'data-c3pg': 'new-init-open', onclick: () => openForm() }, c3ic('check', 'sm'), 'Create it: choose the anchor')));
136
+ };
137
+ const start = async (raw) => {
138
+ const text = String(raw || '').trim(); if (!text) return;
139
+ input.value = text; const me = ++run; let n = 0;
140
+ slot.replaceChildren(steps(text, 0));
141
+ const call = pmPost('/api/intake/draft', { line: text }).catch(() => null);
142
+ const t = setInterval(() => { if (me !== run) { clearInterval(t); return; } n++; if (n < INTAKE_STEPS.length) { slot.replaceChildren(steps(text, n)); return; } clearInterval(t);
143
+ call.then((out) => { if (me !== run) return; slot.replaceChildren(out && out.ok && out.draft ? answer(text, out.draft) : el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), (out && out.error) || 'The draft did not answer.')); }); }, 650);
144
+ };
145
+ input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) start(input.value); });
146
+ const mic = c3ib('Speak is off on this line: on-device dictation runs in Notes, in the main app. Type the line here.', 'mic', { disabled: true });
147
+ const spark = c3ib('Start: an agent runs the intake', 'spark', { 'data-a': 'c3nigo', 'data-c3pg': 'new-init-go', onClick: () => start(input.value) }, 'pri');
148
+ return el('div', null, el('div', { class: 'c3add' }, el('div', { class: 'form' }, input, mic, spark),
149
+ el('div', { class: 'chips2' }, el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, 'try'),
150
+ ...TRY.map((t) => el('button', { type: 'button', class: 'pill', 'data-a': 'c3niex', 'data-k': t, onclick: () => start(t) }, t)))), slot);
151
+ }
152
+
153
+ /* The Briefing's "Needs you" row of the mockup (console3.html needRow2, line 2345) is c3-brief.js needRow: it opens its case file in place, swipes and
154
+ * answers keys. This is the old name, kept so a caller of needsRow(n, i) gets the same row. The third argument (the old jump to the deck) is gone: a
155
+ * press on the row opens the row, as the mockup does. */
156
+ export const needsRow = (n, i, ctx) => needRow(n, i, ctx && typeof ctx === 'object' ? ctx : {});
157
+
158
+ /* The mockup's pulse (console3.html lines 1448 to 1457): one bar of the rag colours, a row per initiative with its reason and microTL (promised
159
+ * against forecast on a 40 day line). Drawn only for rows that carry `pulse` ({ c, why }), which the calendar route does not send. */
160
+ const RAGC = { red: ['bad', 'var(--destructive)'], amber: ['warn', 'var(--warning)'], green: ['ok', 'var(--success)'], park: ['', 'var(--ring)'] };
161
+ function microTL(g, today) {
162
+ const NSV = 'http://www.w3.org/2000/svg'; const day = (v) => Math.round((Date.parse(String(v).slice(0, 10) + 'T00:00:00Z') - Date.parse(today + 'T00:00:00Z')) / 86400000);
163
+ const X = (d) => 6 + Math.min(40, Math.max(0, d)) / 40 * 98;
164
+ const s = document.createElementNS(NSV, 'svg'); s.setAttribute('width', 112); s.setAttribute('height', 18); s.setAttribute('viewBox', '0 0 112 18'); s.setAttribute('role', 'img'); s.setAttribute('aria-label', 'Promised against forecast');
165
+ const add = (tag, a) => { const n = document.createElementNS(NSV, tag); for (const k in a) n.setAttribute(k, a[k]); s.appendChild(n); };
166
+ add('line', { x1: 6, x2: 104, y1: 9, y2: 9, stroke: 'var(--border)', 'stroke-width': 2, 'stroke-linecap': 'round' }); add('line', { x1: 6, x2: 6, y1: 3, y2: 15, stroke: 'var(--foreground)', 'stroke-width': 2 });
167
+ if (g.pulse.c === 'park' || !g.promised) add('line', { x1: 6, x2: 40, y1: 9, y2: 9, stroke: 'var(--ring)', 'stroke-width': 2, 'stroke-dasharray': '3 4' });
168
+ else {
169
+ const p = day(g.promised); const f = day((g.forecast && (g.forecast.end || g.forecast.start)) || g.promised); const late = f > p;
170
+ if (late) add('rect', { x: X(p), y: 7, width: X(f) - X(p), height: 4, rx: 2, fill: 'var(--destructive)' });
171
+ add('path', { d: `M${X(p)} 3 l5 6 -5 6 -5 -6z`, fill: 'var(--background)', stroke: 'var(--foreground)', 'stroke-width': 1.8 });
172
+ if (late) add('circle', { cx: X(f), cy: 9, r: 3.5, fill: 'var(--destructive)' });
173
+ }
174
+ return s;
175
+ }
176
+ /* The mockup's pulse (line 1448): each colour of the bar is a button that filters the rows to that status (pressed again, all), and each row is a button that
177
+ * goes to its initiative (`goinit`). `go(id)` is the caller's jump to the Initiatives tab. */
178
+ export const mockPulse = (rows, today, go) => {
179
+ const cnt = {}; rows.forEach((g) => { cnt[g.pulse.c] = (cnt[g.pulse.c] || 0) + 1; });
180
+ const WORD = { red: 'red', amber: 'amber', green: 'green', park: 'parked' };
181
+ let f = null;
182
+ const list = el('div');
183
+ const segs = ['red', 'amber', 'green', 'park'].filter((k) => cnt[k]).map((k) => el('button', { type: 'button', class: 'pseg', 'data-a': 'pulse', 'data-k': k, style: 'flex:' + cnt[k] + ';height:10px;border-radius:5px;border:0;padding:0;background:' + RAGC[k][1] + ';cursor:pointer',
184
+ title: cnt[k] + ' ' + WORD[k], 'aria-label': cnt[k] + ' ' + WORD[k], 'aria-pressed': 'false', onclick: () => { f = f === k ? null : k; draw(); } }));
185
+ const draw = () => {
186
+ segs.forEach((b) => { const on = f === b.dataset.k; b.setAttribute('aria-pressed', String(on)); b.style.opacity = f && !on ? '.28' : '1'; });
187
+ list.replaceChildren(...rows.filter((g) => !f || g.pulse.c === f).map((g) => el('button', { type: 'button', class: 'prow', 'data-c3pg-prow': g.id, 'data-a': 'goinit', 'aria-label': 'Open ' + (g.title || g.id) + ': ' + g.pulse.why, onclick: () => { if (go) go(g.id); } },
188
+ el('span', { class: 'dotc ' + RAGC[g.pulse.c][0] }), el('span', { style: 'min-width:0;text-align:left' }, el('b', { style: 'font-size:var(--fs-3)' }, g.title || g.id), el('div', { class: 'dim', style: 'font-size:var(--fs-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis' }, g.pulse.why)), microTL(g, today))));
189
+ };
190
+ draw();
191
+ return [el('div', { class: 'pbs', role: 'group', 'aria-label': 'Status of initiatives' }, ...segs), list];
192
+ };