create-pmos 0.123.0 → 0.125.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/nine-stages.js +81 -0
- package/payload/scripts/workflow-state.js +2 -0
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/c3-one-home-test.js +131 -0
- package/workspace/fixtures/initiatives-api/list.json +171 -3
- package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
- package/workspace/fixtures/initiatives-api/sheet.json +57 -1
- package/workspace/fixtures/one-home-baseline.json +83 -0
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/notes.md +1 -0
- package/workspace/lib/engine/nine-stages.js +81 -0
- package/workspace/lib/engine/workflow-state.js +2 -0
- package/workspace/lib/facts-async.js +51 -0
- package/workspace/lib/item-steps.js +52 -0
- package/workspace/lib/jira-status.js +8 -3
- package/workspace/lib/jobs-to-be-done.js +52 -0
- package/workspace/lib/outbox-store.js +87 -0
- package/workspace/lib/pm-items.js +115 -0
- package/workspace/lib/route-modules.js +21 -0
- package/workspace/lib/routes-briefing.js +100 -0
- package/workspace/lib/routes-c3-init.js +53 -0
- package/workspace/lib/routes-c3-life.js +119 -0
- package/workspace/lib/routes-c3-strategy.js +169 -0
- package/workspace/lib/routes-c3-timeline.js +191 -0
- package/workspace/lib/routes-calendar-import.js +60 -0
- package/workspace/lib/routes-calendar-views.js +61 -0
- package/workspace/lib/routes-doc-health.js +79 -0
- package/workspace/lib/routes-doc-sheet.js +169 -0
- package/workspace/lib/routes-forecast.js +85 -0
- package/workspace/lib/routes-hooks.js +121 -0
- package/workspace/lib/routes-initiatives.js +3 -0
- package/workspace/lib/routes-intake.js +24 -0
- package/workspace/lib/routes-outbox.js +97 -0
- package/workspace/lib/routes-pm-items.js +216 -0
- package/workspace/lib/routes-records.js +61 -0
- package/workspace/lib/routes-releases.js +129 -0
- package/workspace/lib/routes-report-facts.js +84 -0
- package/workspace/lib/routes-report-families.js +32 -0
- package/workspace/lib/routes-roadmap-ops.js +141 -0
- package/workspace/lib/routes-runs.js +119 -0
- package/workspace/lib/routes-signals.js +50 -0
- package/workspace/lib/run-answers.js +69 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/c3.html +26 -0
- package/workspace/public/pm/briefing-deck.js +415 -0
- package/workspace/public/pm/briefing-model.js +176 -0
- package/workspace/public/pm/c3-breakdown.js +240 -0
- package/workspace/public/pm/c3-brief.js +278 -0
- package/workspace/public/pm/c3-cal-strategy.js +528 -0
- package/workspace/public/pm/c3-case.js +173 -0
- package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
- package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
- package/workspace/public/pm/c3-demo-cal2.js +66 -0
- package/workspace/public/pm/c3-demo-cal3.js +173 -0
- package/workspace/public/pm/c3-demo-data.js +560 -0
- package/workspace/public/pm/c3-demo-init.js +373 -0
- package/workspace/public/pm/c3-demo-life.js +77 -0
- package/workspace/public/pm/c3-demo-misc2.js +46 -0
- package/workspace/public/pm/c3-demo-plan2.js +30 -0
- package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
- package/workspace/public/pm/c3-demo-rep2.js +92 -0
- package/workspace/public/pm/c3-demo-term.js +153 -0
- package/workspace/public/pm/c3-demo-time.js +49 -0
- package/workspace/public/pm/c3-demo-work-ix.js +80 -0
- package/workspace/public/pm/c3-demo-work2.js +13 -0
- package/workspace/public/pm/c3-demo.js +113 -0
- package/workspace/public/pm/c3-doc-sheet.js +314 -0
- package/workspace/public/pm/c3-dock.js +180 -0
- package/workspace/public/pm/c3-docs.js +290 -0
- package/workspace/public/pm/c3-expand.js +96 -0
- package/workspace/public/pm/c3-init-extras.js +170 -0
- package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
- package/workspace/public/pm/c3-misc-panels.js +89 -0
- package/workspace/public/pm/c3-plan-panels.js +163 -0
- package/workspace/public/pm/c3-plan.js +192 -0
- package/workspace/public/pm/c3-roadmap.js +249 -0
- package/workspace/public/pm/c3-sheet.js +107 -0
- package/workspace/public/pm/c3-shell.js +637 -0
- package/workspace/public/pm/c3-signals.js +196 -0
- package/workspace/public/pm/c3-strategy-panels.js +45 -0
- package/workspace/public/pm/c3-swipe.js +138 -0
- package/workspace/public/pm/c3-terminal-data.js +26 -0
- package/workspace/public/pm/c3-terminal-edge.js +91 -0
- package/workspace/public/pm/c3-terminal-icons.js +41 -0
- package/workspace/public/pm/c3-terminal-results.js +141 -0
- package/workspace/public/pm/c3-terminal.js +52 -0
- package/workspace/public/pm/c3-tickets.js +163 -0
- package/workspace/public/pm/c3-timeline.js +201 -0
- package/workspace/public/pm/c3-toast.js +16 -0
- package/workspace/public/pm/calendar-views.js +1015 -0
- package/workspace/public/pm/console3-lift.md +220 -0
- package/workspace/public/pm/console3.css +1480 -0
- package/workspace/public/pm/dest-initiatives.js +16 -3
- package/workspace/public/pm/dest-mywork.js +8 -1
- package/workspace/public/pm/dest-notes.js +25 -3
- package/workspace/public/pm/dest-reports.js +47 -1
- package/workspace/public/pm/dest-today.js +55 -1
- package/workspace/public/pm/doc-health-panel.js +110 -0
- package/workspace/public/pm/doc-rail.js +9 -3
- package/workspace/public/pm/lifecycle-lens-model.js +228 -0
- package/workspace/public/pm/lifecycle-lens.js +261 -0
- package/workspace/public/pm/new-initiative-sheet.js +116 -21
- package/workspace/public/pm/outbox-panel.js +125 -0
- package/workspace/public/pm/pm-work-view.js +919 -0
- package/workspace/public/pm/releases-view.js +248 -0
- package/workspace/public/pm/report-families-view.js +503 -0
- package/workspace/public/pm/run-card.js +244 -0
- package/workspace/public/pm/sessions-panel.js +149 -0
- package/workspace/public/pm/shell-bell.js +162 -0
- package/workspace/public/pm/shell-model.js +83 -0
- package/workspace/public/pm/shell-phone.js +124 -0
- package/workspace/public/pm/shell-readaloud.js +53 -0
- package/workspace/public/pm/shell-recording.js +110 -0
- package/workspace/public/pm/shell-ticker.js +90 -0
- package/workspace/public/pm/slide-confirm.js +163 -0
- package/workspace/public/pm/timeline-forecast-view.js +164 -0
- package/workspace/public/pm/today-day.js +8 -2
- package/workspace/public/pm/tokens.css +1 -0
- package/workspace/server.js +5 -2
- package/workspace/test-files.json +8 -0
- package/workspace/test-lists/browser-suites-green.json +5 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* pm/c3-terminal-results.js — what a finished run shows, by result type (agent-surfaces.html: claims, hero, fileBox, deck, table, diagram, draft).
|
|
2
|
+
* resultNodes(result, ctx) result = the contract's folded result ({ type, ...value }); returns the nodes under the headline.
|
|
3
|
+
* Everything from a run goes in as a text node (el() never takes HTML). An svg is never inserted: a diagram is drawn from its source.
|
|
4
|
+
* ctx = { ui, redraw, canShare }: ui is the per-run state kept outside the render (the deck slide, the table sort), redraw repaints the card. */
|
|
5
|
+
import { el } from './dom.js';
|
|
6
|
+
import { ic } from './c3-terminal-icons.js';
|
|
7
|
+
import { normalizeCase, slideFor } from './c3-case.js';
|
|
8
|
+
|
|
9
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
10
|
+
const LAB = { fact: ['l-fact', 'fact'], estimate: ['l-est', 'estimate'], assumption: ['l-ass', 'assumption'], reported: ['l-rep', 'reported'] };
|
|
11
|
+
const NO_CHANNEL = 'No channel is connected, so this stays a draft. Nothing is sent.';
|
|
12
|
+
|
|
13
|
+
/** The label chip of one claim: its basis, or "unlabelled" when the contract marked it not ok. */
|
|
14
|
+
export function labelChip(c) {
|
|
15
|
+
const l = c && c.ok !== false && LAB[c.basis] ? LAB[c.basis] : ['l-unl', 'unlabelled'];
|
|
16
|
+
return el('span', { class: 'lab ' + l[0], title: c && c.ok === false ? 'No valid label. It cannot back a message.' : null }, l[1]);
|
|
17
|
+
}
|
|
18
|
+
export function claimsList(list) {
|
|
19
|
+
return el('ul', { class: 'c3t-claims', role: 'list' }, (list || []).map((c) => el('li', null, labelChip(c), el('span', null, c.text, (c.source || c.range) ? el('span', { class: 'dim mono c3t-src' }, ' ' + (c.source || c.range)) : null))));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The hero of a card.update (the mockup's three: a promise-versus-forecast line, a quote, a CI strip). Anything else is not drawn. */
|
|
23
|
+
export function heroNode(h) {
|
|
24
|
+
if (!h || typeof h !== 'object') return null;
|
|
25
|
+
if (h.t === 'tl' && Number.isFinite(+h.p) && Number.isFinite(+h.f)) {
|
|
26
|
+
const pos = (v) => ((v - 1) / 29 * 100).toFixed(1) + '%';
|
|
27
|
+
return el('div', { class: 'c3t-tl', role: 'img', 'aria-label': 'Promise ' + h.p + ' Nov, forecast ' + h.f + ' Nov' }, el('div', { class: 'axis' }),
|
|
28
|
+
+h.f > +h.p ? el('div', { class: 'gap', style: 'left:' + pos(+h.p) + ';width:' + (((+h.f - +h.p) / 29) * 100).toFixed(1) + '%' }) : null,
|
|
29
|
+
el('div', { class: 'mk pr', style: 'left:' + pos(+h.p) }, el('span', null, 'promise ' + h.p + ' Nov')), el('div', { class: 'mk fc', style: 'left:' + pos(+h.f) }, el('span', null, 'forecast ' + h.f + ' Nov')));
|
|
30
|
+
}
|
|
31
|
+
if (h.t === 'q') return el('blockquote', { class: 'c3t-q' }, '“' + String(h.x || '') + '”', el('cite', null, String(h.by || '')));
|
|
32
|
+
if (h.t === 'ci' && Array.isArray(h.p)) return el('div', { class: 'c3t-ci' }, ...h.p.map((s) => el('i', { class: ['ok', 'bad', 'run'].includes(s) ? s : '' })), el('span', { class: 'dim mono' }, String(h.l || '')));
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The mockup's md(): # and ## headings, "- " bullets, **bold**, paragraphs. Built as nodes. */
|
|
37
|
+
export function mdNode(text) {
|
|
38
|
+
const out = []; let ul = null;
|
|
39
|
+
const bold = (s) => String(s).split(/(\*\*[^*]+\*\*)/).map((p) => (/^\*\*[^*]+\*\*$/.test(p) ? el('b', null, p.slice(2, -2)) : p));
|
|
40
|
+
for (const l of String(text || '').split('\n')) {
|
|
41
|
+
if (l.startsWith('- ')) { if (!ul) { ul = el('ul'); out.push(ul); } ul.append(el('li', null, ...bold(l.slice(2)))); continue; }
|
|
42
|
+
ul = null;
|
|
43
|
+
if (l.startsWith('## ') || l.startsWith('# ')) out.push(el('h4', null, ...bold(l.replace(/^#+ /, '')))); else if (l.trim()) out.push(el('p', null, ...bold(l)));
|
|
44
|
+
}
|
|
45
|
+
return el('div', { class: 'c3t-md' }, ...out);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fileNode(r, ctx) {
|
|
49
|
+
const body = ctx.fileBody && ctx.fileBody(r.path);
|
|
50
|
+
return el('div', { class: 'c3t-file' },
|
|
51
|
+
el('div', { class: 'fh' }, ic('doc', 'sm'), el('b', null, r.title), el('span', { class: 'chip2' }, r.media + ' · ' + (r.render || ''))),
|
|
52
|
+
body ? el('div', { class: 'fb' }, mdNode(body)) : el('div', { class: 'fb dim mono' }, 'The page does not read run files. Open the session to read it.'),
|
|
53
|
+
el('div', { class: 'dim mono ft' }, r.path + (r.sha256 ? ' · checksum ' + r.sha256 : ' · no checksum')));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function draftNode(r) {
|
|
57
|
+
const c = normalizeCase({ id: 'draft', outward: true, outboxReason: NO_CHANNEL, options: [{ id: 'send', l: 'Send to ' + r.to, rec: 1, outward: true }] });
|
|
58
|
+
const s = slideFor(c, 'send', { outbox: false });
|
|
59
|
+
return el('div', { class: 'c3t-draft' },
|
|
60
|
+
el('div', { class: 'draft' }, r.body),
|
|
61
|
+
claimsList(r.claims),
|
|
62
|
+
r.can_send === false ? el('div', { class: 'dim mono', style: 'color:var(--destructive)' }, 'Send is blocked: a claim in this draft has no label. Ask for evidence or retry.')
|
|
63
|
+
: el('div', { class: 'dim mono' }, ic('next', 'sm'), ' Queued for ' + r.to + '. The slide sends exactly this text (hash ' + r.hash + ', computed by the workspace, not the agent). The session cannot send.'),
|
|
64
|
+
el('div', { class: 'slt off', 'aria-disabled': 'true', 'data-draft-slide': '', style: '--tc:var(--muted-foreground)' }, el('span', { class: 'lb' }, s.label + ' ›››'),
|
|
65
|
+
el('button', { type: 'button', class: 'th', disabled: true, 'aria-label': 'Send: not available. ' + s.reason }, ic('lock'))),
|
|
66
|
+
el('div', { class: 'slt-why', role: 'note' }, ic('lock', 'sm'), s.reason));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function optionsNode(r) {
|
|
70
|
+
return el('div', { class: 'c3t-opts' }, (r.options || []).map((o) => el('div', { class: 'opt-r' + (o.rec ? ' rec' : '') }, el('b', null, o.label), el('span', { class: 'dim mono' }, o.when || ''), el('span', { class: 'dim mono' }, (o.cost || '') + (o.rec ? ' · suggested' : '') + (r.close_call ? ' · a close call' : '')))));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function deckNode(r, ctx) {
|
|
74
|
+
const ui = ctx.ui; const sl = r.slides || []; const i = Math.max(0, Math.min(sl.length - 1, ui.slide || 0)); const s = sl[i];
|
|
75
|
+
if (!s) return null;
|
|
76
|
+
const go = (d) => { ui.slide = Math.max(0, Math.min(sl.length - 1, i + d)); ctx.redraw(); };
|
|
77
|
+
const box = el('div', { class: 'c3t-dk', 'data-c3t-deck': '' },
|
|
78
|
+
el('div', { class: 'sl' }, el('h4', null, s.title), ...(s.bullets || []).map((b) => el('div', { class: 'c3t-bul' }, labelChip(b), el('span', null, b.text)))),
|
|
79
|
+
el('div', { class: 'nv' }, el('button', { type: 'button', class: 'ib sm', 'aria-label': 'Previous slide', disabled: i === 0, onclick: () => go(-1) }, ic('chevl', 'sm')), el('span', { class: 'dim mono' }, (i + 1) + ' / ' + sl.length + ' · swipe or press'),
|
|
80
|
+
el('button', { type: 'button', class: 'ib sm', 'aria-label': 'Next slide', disabled: i === sl.length - 1, onclick: () => go(1) }, ic('chev', 'sm'))));
|
|
81
|
+
let x0 = null;
|
|
82
|
+
box.addEventListener('pointerdown', (e) => { if (!e.target.closest('button')) x0 = e.clientX; });
|
|
83
|
+
box.addEventListener('pointerup', (e) => { if (x0 === null) return; const dx = e.clientX - x0; x0 = null; if (Math.abs(dx) > 50) go(dx < 0 ? 1 : -1); });
|
|
84
|
+
return el('div', { class: 'c3t-deck' }, box, el('div', { class: 'foot' },
|
|
85
|
+
r.unlabelled ? el('span', { style: 'color:var(--destructive)' }, r.unlabelled + ' bullet' + (r.unlabelled > 1 ? 's' : '') + ' without a label. Cannot be shared.') : el('span', { class: 'dim mono' }, 'All bullets labelled.'),
|
|
86
|
+
el('button', { type: 'button', class: 'pill', disabled: true, title: 'A pptx export is not built on this page. Nothing leaves the machine.' }, ic('doc', 'sm'), 'Export as pptx')));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function tableNode(r, ctx) {
|
|
90
|
+
const ui = ctx.ui; const cols = r.columns || []; ui.col = ui.col == null ? 1 : ui.col; ui.dir = ui.dir || -1;
|
|
91
|
+
const rows = (r.rows || []).slice().sort((a, b) => { const x = a[ui.col], y = b[ui.col]; return (typeof x === 'number' ? x - y : String(x).localeCompare(String(y))) * ui.dir; });
|
|
92
|
+
const tog = el('div', { class: 'chips2' }, el('button', { type: 'button', class: 'pill', 'aria-pressed': String(!ui.cards), onclick: () => { ui.cards = false; ctx.redraw(); } }, 'Table'), el('button', { type: 'button', class: 'pill', 'aria-pressed': String(!!ui.cards), onclick: () => { ui.cards = true; ctx.redraw(); } }, 'Cards'),
|
|
93
|
+
el('span', { class: 'dim mono' }, 'Showing ' + rows.length + ' of ' + (r.total || rows.length) + ' · tap a heading to sort'));
|
|
94
|
+
if (ui.cards) return el('div', { class: 'c3t-tbl' }, tog, el('div', { class: 'c3t-opts' }, rows.map((row) => el('div', { class: 'opt-r' }, el('b', null, String(row[0])), el('span', { class: 'dim mono' }, row.slice(1, 2).join('') + (cols[1] ? ' ' + cols[1].label : '')), el('span', { class: 'dim mono' }, row.slice(2).join(' · '))))));
|
|
95
|
+
const head = el('tr', null, cols.map((c, i) => el('th', { class: c.type === 'number' ? 'n' : '', 'aria-sort': ui.col === i ? (ui.dir > 0 ? 'ascending' : 'descending') : 'none' },
|
|
96
|
+
el('button', { type: 'button', class: 'c3t-th', onclick: () => { ui.dir = ui.col === i ? -ui.dir : 1; ui.col = i; ctx.redraw(); } }, c.label + (ui.col === i ? (ui.dir > 0 ? ' ▲' : ' ▼') : '')))));
|
|
97
|
+
return el('div', { class: 'c3t-tbl' }, tog, el('div', { class: 'tw' }, el('table', { class: 'tbl' }, head, rows.map((row) => el('tr', null, row.map((v, i) => el('td', { class: cols[i] && cols[i].type === 'number' ? 'n' : '' }, v == null ? '' : String(v))))))));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A mermaid "graph LR" of edges, drawn as layers (the longest chain in the accent). Anything it cannot read stays as source text. */
|
|
101
|
+
function diagramNode(r) {
|
|
102
|
+
const src = String(r.source || '');
|
|
103
|
+
const edges = []; const names = [];
|
|
104
|
+
for (const part of src.replace(/^\s*(graph|flowchart)\s+\w+\s*;?/i, '').split(/[;\n]/)) {
|
|
105
|
+
const m = /^\s*([\w-]+)\s*-->\s*([\w-]+)\s*$/.exec(part);
|
|
106
|
+
if (m) { edges.push([m[1], m[2]]); for (const n of [m[1], m[2]]) if (!names.includes(n)) names.push(n); }
|
|
107
|
+
}
|
|
108
|
+
const text = el('details', { class: 'c3t-src' }, el('summary', { class: 'chips2 dim' }, ic('chev', 'sm'), 'Source (' + r.kind + ')'), el('div', { class: 'raw' }, src));
|
|
109
|
+
if (r.kind !== 'mermaid' || !edges.length) return el('div', { class: 'c3t-dg' }, el('div', { class: 'dim mono' }, 'The diagram is shown as its source: this page draws only edge lists.'), text);
|
|
110
|
+
const depth = {}; const longest = {};
|
|
111
|
+
const lay = (n, seen = []) => { if (depth[n] != null) return depth[n]; if (seen.includes(n)) return 0; const ins = edges.filter((e) => e[1] === n).map((e) => e[0]); depth[n] = ins.length ? 1 + Math.max(...ins.map((x) => lay(x, seen.concat(n)))) : 0; return depth[n]; };
|
|
112
|
+
names.forEach((n) => lay(n));
|
|
113
|
+
const maxD = Math.max(...Object.values(depth));
|
|
114
|
+
const end = names.find((n) => depth[n] === maxD); let cur = end; longest[cur] = 1;
|
|
115
|
+
while (cur) { const prev = edges.map((e) => e).filter((e) => e[1] === cur && depth[e[0]] === depth[cur] - 1)[0]; cur = prev && prev[0]; if (cur) longest[cur] = 1; }
|
|
116
|
+
const rowOf = {}; const cnt = {};
|
|
117
|
+
names.forEach((n) => { cnt[depth[n]] = (cnt[depth[n]] || 0); rowOf[n] = longest[n] ? 0 : (cnt[depth[n]] += 1); });
|
|
118
|
+
const W = 120, H = 40, GX = 150, GY = 62; const maxRow = Math.max(...Object.values(rowOf));
|
|
119
|
+
const pt = (n) => ({ x: 10 + depth[n] * GX, y: 10 + rowOf[n] * GY });
|
|
120
|
+
const svg = document.createElementNS(NS, 'svg'); svg.setAttribute('viewBox', '0 0 ' + (20 + maxD * GX + W) + ' ' + (20 + maxRow * GY + H)); svg.setAttribute('role', 'img'); svg.setAttribute('aria-label', r.title || 'Diagram');
|
|
121
|
+
const mk = (tag, a, txt) => { const n = document.createElementNS(NS, tag); for (const k of Object.keys(a)) n.setAttribute(k, a[k]); if (txt) n.textContent = txt; svg.appendChild(n); return n; };
|
|
122
|
+
for (const [a, b] of edges) { const p = pt(a), q = pt(b); const cp = longest[a] && longest[b] && depth[b] === depth[a] + 1; mk('path', { d: 'M' + (p.x + W) + ' ' + (p.y + H / 2) + 'L' + q.x + ' ' + (q.y + H / 2), class: cp ? 'ec' : 'ed' }); }
|
|
123
|
+
for (const n of names) { const p = pt(n); mk('rect', { x: p.x, y: p.y, width: W, height: H, rx: 8, class: 'nd' + (longest[n] ? ' cp' : '') }); mk('text', { x: p.x + W / 2, y: p.y + H / 2 + 4, 'text-anchor': 'middle' }, n); }
|
|
124
|
+
return el('div', { class: 'c3t-dg' }, el('div', { class: 'dg' }, svg), text, el('div', { class: 'dim mono' }, 'The longest chain is blue. Drawn here from its source; the page loads nothing from the network.'));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resultNodes(r, ctx) {
|
|
128
|
+
if (!r) return [];
|
|
129
|
+
switch (r.type) {
|
|
130
|
+
case 'card.update': return [r.hero ? heroNode(r.hero) : null, claimsList(r.claims), r.chip ? el('div', { class: 'chips2' }, el('span', { class: 'chip2 ok' }, r.chip)) : null];
|
|
131
|
+
case 'evidence': return [claimsList(r.claims)];
|
|
132
|
+
case 'options': return [optionsNode(r)];
|
|
133
|
+
case 'draft': return [draftNode(r)];
|
|
134
|
+
case 'note': return [el('p', { class: 'c3t-note' }, r.note)];
|
|
135
|
+
case 'file': return [fileNode(r, ctx)];
|
|
136
|
+
case 'deck': return [deckNode(r, ctx)];
|
|
137
|
+
case 'table': return [tableNode(r, ctx)];
|
|
138
|
+
case 'diagram': return [diagramNode(r)];
|
|
139
|
+
default: return [el('p', { class: 'dim mono' }, 'A result of type ' + String(r.type) + ' that this page does not draw.')];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* pm/c3-terminal.js — the Terminal page: the agent surfaces of agent-surfaces.html, opened by the header's agent button (spark, with the needs-you badge).
|
|
2
|
+
* openTerminalPage({ tab }) closeTerminalPage()
|
|
3
|
+
* One wide sheet (c3-sheet.js: Esc closes, focus is kept inside, a bottom sheet on a phone) with the mockup's two views:
|
|
4
|
+
* Sessions On a card, Started by me, Past (7 days, with the observed / reported / you timeline), Automatic (hidden until switched on): sessions-panel.js,
|
|
5
|
+
* every run a card (run-card.js) with its strip, its questions, its permission cards, its result and next moves.
|
|
6
|
+
* Edge cases the mockup's cases, one card each: c3-terminal-edge.js (example data, also in real mode).
|
|
7
|
+
* The mockup's third view, "Structure", is the design note (how a session is read); it is not drawn on the page.
|
|
8
|
+
* Real mode reads GET /api/runs and records presses; demo mode (the sample content) answers from c3-demo-term.js. Nothing here sends anything outward. */
|
|
9
|
+
import { el } from './dom.js';
|
|
10
|
+
import { openSheet, closeSheet } from './c3-sheet.js';
|
|
11
|
+
import { ic } from './c3-terminal-icons.js';
|
|
12
|
+
import { source, isDemo } from './c3-terminal-data.js';
|
|
13
|
+
import { mountSessions, disposeSessions } from './sessions-panel.js';
|
|
14
|
+
import { mountEdge, disposeEdge } from './c3-terminal-edge.js';
|
|
15
|
+
|
|
16
|
+
const TABS = [['sessions', 'Sessions', 'pulse'], ['edge', 'Edge cases', 'alert']];
|
|
17
|
+
const NEXT = [['', 'replies normally'], ['noresult', 'ends with no result'], ['rejected', 'is unreadable'], ['quiet', 'goes quiet'], ['unlabelled', 'has an unlabelled claim'], ['prose', 'answers in plain text'], ['attention', 'needs a sign-in'], ['crashed', 'crashes']];
|
|
18
|
+
let sheet = null;
|
|
19
|
+
|
|
20
|
+
export function closeTerminalPage() { if (sheet) closeSheet(); }
|
|
21
|
+
|
|
22
|
+
export async function openTerminalPage(o = {}) {
|
|
23
|
+
const src = source();
|
|
24
|
+
const st = { tab: o.tab === 'edge' ? 'edge' : 'sessions' };
|
|
25
|
+
const host = el('div', { class: 'c3t-host', 'data-c3t': 'host' });
|
|
26
|
+
const seg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Agent surfaces view', 'data-c3t': 'tabs' });
|
|
27
|
+
const note = el('div', { class: 'dim mono c3t-note', 'data-c3t': 'note' });
|
|
28
|
+
const demoBar = src.demo ? el('div', { class: 'chips2 c3t-demo', 'data-c3t': 'demo-controls' },
|
|
29
|
+
el('label', { class: 'dim mono', for: 'c3t-next' }, 'Demo: next reply'),
|
|
30
|
+
el('select', { id: 'c3t-next', 'aria-label': 'How the next reply ends', onchange: (e) => src.setNext(e.target.value) }, NEXT.map(([v, l]) => el('option', { value: v }, l))),
|
|
31
|
+
el('button', { type: 'button', class: 'pill', 'data-c3t': 'fire', onclick: () => src.fire() }, ic('bolt', 'sm'), 'Fire a detector')) : null;
|
|
32
|
+
let fx = null;
|
|
33
|
+
const draw = async () => {
|
|
34
|
+
seg.replaceChildren(...TABS.map(([id, label, icon]) => el('button', { type: 'button', 'aria-pressed': String(st.tab === id), 'data-c3t-tab': id, onclick: () => { if (st.tab !== id) { st.tab = id; draw(); } } }, ic(icon, 'sm'), label)));
|
|
35
|
+
disposeSessions(host); disposeEdge(host);
|
|
36
|
+
if (demoBar) demoBar.hidden = st.tab !== 'sessions';
|
|
37
|
+
if (st.tab === 'sessions') {
|
|
38
|
+
note.textContent = isDemo() ? 'Example data. A swipe right accepts a finished result, a swipe left opens the reply tray. Tap the corner icon to flip a card.' : 'Your runs, read from this workspace. A run is a real CLI session; the page reads its replies and records your presses. Nothing is sent outward.';
|
|
39
|
+
mountSessions(host, {});
|
|
40
|
+
} else {
|
|
41
|
+
note.textContent = '';
|
|
42
|
+
if (!fx) { try { fx = (await import('./c3-demo-term.js')).FX; } catch { fx = null; } }
|
|
43
|
+
if (st.tab !== 'edge') return;
|
|
44
|
+
if (fx) mountEdge(host, fx); else host.replaceChildren(el('p', { class: 'dim mono', role: 'status' }, 'The examples could not be loaded.'));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const body = [el('div', { class: 'c3 c3e c3t c3t-page', 'data-c3t': 'page' }, el('div', { class: 'chips2', style: 'justify-content:space-between' }, seg, demoBar), note, host)];
|
|
48
|
+
const s = openSheet({ title: 'Agent surfaces', icon: 'pulse', dw: true, body, onClose: () => { disposeSessions(host); disposeEdge(host); sheet = null; } });
|
|
49
|
+
sheet = s;
|
|
50
|
+
await draw();
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/* pm/c3-tickets.js — the Tickets sub-view of Initiatives on /c3.html (console3.html patch at lines 3764 to 3860 and 4488: tkRow, list20, the
|
|
2
|
+
* "Open points by owner" card, raciCard and the agent-drafts card).
|
|
3
|
+
* Tickets are JIRA ONLY and their status is MIRRORED, read-only: the server never reads Jira (D53). The rows come from the cached Jira
|
|
4
|
+
* read the My work route serves: GET /api/my-work, the items with source 'jira' (id jira:KEY, key, title, status, category todo|doing|done,
|
|
5
|
+
* due, estimate, blocks, open, initiative). Nothing on this view writes.
|
|
6
|
+
* Real: key, title, status, category, due, estimate (XS 1, S 2, M 3, L 5, XL 8 points), blocks, initiative; "late" is an overdue date.
|
|
7
|
+
* Optional fields the real read does not carry (owner, pts, forecast_due, blocker, critical, in_work) and the cards `raci` and `drafts`
|
|
8
|
+
* are drawn only when present: the demo's fixtures carry them, a real read does not, so real mode never shows an invented owner or draft.
|
|
9
|
+
* Late or at risk = a forecast past the due date, an overdue date, or a named blocker. */
|
|
10
|
+
import { el } from './dom.js';
|
|
11
|
+
import { pmGet, pmPost } from './http.js';
|
|
12
|
+
import { c3ic } from './pm-work-view.js';
|
|
13
|
+
import { makeView, frame, hBars, card, listCard, emptyRow, dayLabel, short, rowShell, caseNode, markDone, say } from './c3-init-extras.js';
|
|
14
|
+
|
|
15
|
+
const SIZE_PTS = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
|
|
16
|
+
const OWN_COL = ['var(--acc)', 'var(--success)', 'var(--purple)', 'var(--warning)', 'var(--muted-foreground)', 'var(--destructive)'];
|
|
17
|
+
const dayNum = (iso) => Math.round(Date.parse(String(iso).slice(0, 10) + 'T00:00:00Z') / 86400000);
|
|
18
|
+
|
|
19
|
+
/** One ticket the view draws: the my-work item plus the derived state (late days, late or at risk). */
|
|
20
|
+
export function ticketOf(it, today) {
|
|
21
|
+
const due = it.due || null; const fc = it.forecast_due || due;
|
|
22
|
+
const slip = due && fc ? Math.max(0, dayNum(fc) - dayNum(due)) : 0;
|
|
23
|
+
const over = !slip && due && today && it.category !== 'done' && dayNum(today) > dayNum(due) ? dayNum(today) - dayNum(due) : 0;
|
|
24
|
+
const risk = it.category !== 'done' && (it.blocker || (it.blocks > 0) || (it.open && it.open.length));
|
|
25
|
+
const bad = it.category !== 'done' && (slip > 0 || over > 0 || !!it.blocker);
|
|
26
|
+
return { ...it, due, fc, slip, over, bad, risk: !!risk, owner: it.owner || null, pts: it.pts != null ? it.pts : (SIZE_PTS[it.estimate] || null) };
|
|
27
|
+
}
|
|
28
|
+
export const jiraTickets = (j) => ((j && j.items) || []).filter((i) => i.source === 'jira').map((i) => ticketOf(i, j.today));
|
|
29
|
+
|
|
30
|
+
const goTab = (V, k) => { const b = V.ctx.links && V.ctx.links.querySelector && V.ctx.links.querySelector('[data-c3-link=' + k + ']'); if (b) b.click(); };
|
|
31
|
+
const mineish = (t) => !t.owner || /^(wawan|you)$/i.test(t.owner);
|
|
32
|
+
|
|
33
|
+
/** The case file of a ticket (console3.html genTk, line 3770). READ-ONLY: Jira owns status, dates and owners; this page never changes them.
|
|
34
|
+
* "Take it into today" is the page's own Work list (a page press); a nudge goes outside, so its slide is disabled until a channel is connected. */
|
|
35
|
+
function caseOf(V, t) {
|
|
36
|
+
const chg = [];
|
|
37
|
+
if (t.blocker) chg.push(['alert', t.blocker, 'Jira', 'bad']);
|
|
38
|
+
if (t.slip > 0) chg.push(['clock', 'Forecast ' + dayLabel(t.fc) + ', ' + t.slip + ' day' + (t.slip === 1 ? '' : 's') + ' after plan', 'timeline', 'warn']);
|
|
39
|
+
else if (t.over > 0) chg.push(['clock', 'Overdue by ' + t.over + ' day' + (t.over === 1 ? '' : 's') + ', due ' + dayLabel(t.due), 'Jira', 'bad']);
|
|
40
|
+
if (t.critical) chg.push(['flag', 'On the critical path: it moves the ship date', 'path']);
|
|
41
|
+
if (t.in_work) chg.push(['check', 'Also in your Work list', 'Work']);
|
|
42
|
+
if (!chg.length) chg.push(['check', t.due ? 'On plan, due ' + dayLabel(t.fc || t.due) : 'No date in Jira', 'timeline', t.due ? 'ok' : '']);
|
|
43
|
+
chg.push(['lock', 'Status, dates and owner are Jira’s: ' + (t.status || 'no status read') + (t.owner ? ', ' + t.owner : ''), 'Jira, read-only']);
|
|
44
|
+
const opts = [];
|
|
45
|
+
if (!t.in_work && t.category !== 'done') opts.push({ id: 'tk', l: 'Take it into today', rec: mineish(t), fx: [['check', 'Appears in your Work list', 'ok']] });
|
|
46
|
+
if (t.category !== 'done' && !mineish(t)) opts.push({ id: 'nd', l: 'Ask ' + t.owner + ' for an update', rec: t.slip > 0 || !!t.blocker, outward: true, fx: [['reply', 'A draft to ' + t.owner, 'ok'], ['out', 'Goes out only by your slide', 'warn']] });
|
|
47
|
+
opts.push({ id: 'ok', l: 'Leave it', fx: [] });
|
|
48
|
+
const model = { sit: t.why || 'Ticket ' + t.key + (t.initiative ? ' in ' + (V.ctx.rows || []).reduce((n, r) => (r.id === t.initiative ? r.title : n), t.initiative) : '') + (t.owner ? ', owned by ' + t.owner : '') + '. Jira owns it: a change here is only ever a proposal.',
|
|
49
|
+
conf: Math.round(72 + (t.slip > 0 ? 6 : 10)), chg, opts, cta: 'Slide to confirm', outboxReason: 'This message goes outside. No channel is connected, so it stays a draft and nothing is sent.' };
|
|
50
|
+
return caseNode(V, 'tk:' + t.key, model, { onConfirm: (o) => decide(V, t, o) });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function takeIntoToday(V, t) {
|
|
54
|
+
const r = await pmPost('/api/pm-items/add', { line: t.title + ' (' + t.key + ') today' });
|
|
55
|
+
if (!r || !r.ok) { say((r && r.error) || 'Not added to Work.'); return null; }
|
|
56
|
+
const id = r.item && r.item.id;
|
|
57
|
+
return () => (id ? pmPost('/api/pm-items/remove', { id }) : null);
|
|
58
|
+
}
|
|
59
|
+
async function decide(V, t, o) {
|
|
60
|
+
if (o.id === 'tk') {
|
|
61
|
+
const undo = await takeIntoToday(V, t); if (undo === null) return;
|
|
62
|
+
say('In your Work list for today.', undo); markDone(V, 'tk:' + t.key, 'Added to your Work list for today.', undo);
|
|
63
|
+
} else markDone(V, 'tk:' + t.key, 'Left as it is. No change.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function row(V, t) {
|
|
67
|
+
const col = t.category === 'done' ? 'var(--success)' : t.bad ? 'var(--destructive)' : t.risk ? 'var(--warning)' : 'var(--success)';
|
|
68
|
+
const word = t.category === 'done' ? 'Done' : t.slip ? 'Delayed +' + t.slip + ' d' : t.over ? 'Overdue ' + t.over + ' d' : t.risk ? 'At risk' : 'On time';
|
|
69
|
+
const wcol = t.category === 'done' ? 'var(--success)' : t.slip || t.over ? 'var(--destructive)' : t.risk ? 'var(--warning)' : 'var(--success)';
|
|
70
|
+
const icon = t.category === 'done' ? 'check' : t.bad ? 'clock' : 'check';
|
|
71
|
+
/* the swipe (console3.html line 3879): right takes it into today, left drafts a nudge to the owner. A nudge goes outside: no channel, so nothing is sent. */
|
|
72
|
+
const sw = {
|
|
73
|
+
labelRight: 'take it into today', labelLeft: t.owner && !mineish(t) ? 'draft a nudge to ' + t.owner : 'nothing to nudge', iconRight: 'sun', iconLeft: 'bolt',
|
|
74
|
+
onRight: async () => { if (t.in_work || t.category === 'done') { say('Already in Work'); return; } const undo = await takeIntoToday(V, t); if (undo) { say('In your Work list for today', undo); markDone(V, 'tk:' + t.key, 'Added to your Work list for today.', undo); } },
|
|
75
|
+
onLeft: () => say(t.owner && !mineish(t) ? (window.__c3demo ? 'Nudge drafted for ' + t.owner : 'A nudge to ' + t.owner + ' goes outside. No channel is connected, so nothing was drafted or sent.') : 'Nothing to nudge: the ticket is yours or has no owner.'),
|
|
76
|
+
};
|
|
77
|
+
return rowShell(V, { key: 'tk:' + t.key, attrs: { 'data-in-ticket': t.key, 'data-id': t.key, 'data-tk': '1' }, icon, iconColor: col, sw, xpContent: caseOf(V, t),
|
|
78
|
+
titleNode: el('span', { class: 'tt' }, t.critical ? el('span', { class: 'st bad', title: 'On the critical path', style: 'margin-right:6px' }, c3ic('flag', 'sm')) : null, t.title),
|
|
79
|
+
chips: [el('span', { class: 'chip2 mono' }, t.key),
|
|
80
|
+
el('span', { class: 'chip2', style: 'color:' + wcol }, word),
|
|
81
|
+
t.owner ? el('span', { class: 'chip2' }, el('span', { class: 'av' }, t.owner.slice(0, 1)), t.owner) : null,
|
|
82
|
+
t.fc ? el('span', { class: 'chip2' }, c3ic('cal', 'sm'), dayLabel(t.fc)) : null,
|
|
83
|
+
t.blocker && t.category !== 'done' ? el('span', { class: 'st warn' }, c3ic('alert', 'sm'), t.blocker) : null,
|
|
84
|
+
t.in_work ? el('button', { type: 'button', class: 'chip2 lk', 'data-gowork': t.key, onclick: () => goTab(V, 'work') }, c3ic('check', 'sm'), 'In Work') : null,
|
|
85
|
+
t.status && !t.owner ? el('span', { class: 'chip2' }, t.status) : null,
|
|
86
|
+
el('span', { class: 'tag' }, 'Jira')] });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function raciCard(R) {
|
|
90
|
+
const cell = (v) => el('span', { class: 'c ' + v }, v);
|
|
91
|
+
return card('org', 'Who does what', 'responsible, accountable, consulted, informed',
|
|
92
|
+
el('div', { class: 'rcg' }, el('span'), ...R.people.map((p) => el('span', { class: 'h' }, p.slice(0, 3))),
|
|
93
|
+
...R.rows.flatMap((r) => [el('span', null, r.title), ...r.cells.map(cell)])),
|
|
94
|
+
el('ul', { class: 'chgl' }, R.notes.map((n) => el('li', { class: n.tone }, c3ic('alert', 'sm'), el('span', null, n.text)))));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function load(V) {
|
|
98
|
+
pmGet('/api/my-work').then((j) => {
|
|
99
|
+
if (V.disposed) return;
|
|
100
|
+
V.data = { j: j && j.ok ? j : null, err: j && j.ok ? null : (j && j.error) || 'The Jira read could not be reached.' };
|
|
101
|
+
V.render();
|
|
102
|
+
}, () => { V.data = { j: null, err: 'The Jira read could not be reached.' }; V.render(); });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function render(V) {
|
|
106
|
+
const D = V.data;
|
|
107
|
+
const all = D && D.j ? jiraTickets(D.j) : [];
|
|
108
|
+
const T = V.scope ? all.filter((t) => t.initiative === V.scope) : all;
|
|
109
|
+
const bad = T.filter((t) => t.bad || t.risk);
|
|
110
|
+
const sorted = (f) => T.filter(f).sort((a, b) => String(a.fc || '9').localeCompare(String(b.fc || '9')));
|
|
111
|
+
const late = sorted((t) => t.category !== 'done' && (t.bad || t.risk));
|
|
112
|
+
const doing = sorted((t) => t.category === 'doing' && !late.includes(t));
|
|
113
|
+
const todo = sorted((t) => t.category === 'todo' && !late.includes(t));
|
|
114
|
+
const done = sorted((t) => t.category === 'done');
|
|
115
|
+
const reading = !D;
|
|
116
|
+
const none = !reading && !all.length;
|
|
117
|
+
const head = reading ? { icon: 'check', h: 'Reading the Jira tickets', s: 'Tickets live in Jira. The agent proposes, you accept.', ring: [0, 1] }
|
|
118
|
+
: none ? { icon: 'check', h: 'No Jira tickets read yet', s: D.err || 'The agent has not read Jira yet. A read from My work in the main app fills this view.', ring: [0, 1] }
|
|
119
|
+
: { icon: 'check', h: T.length + ' Jira ticket' + (T.length === 1 ? '' : 's') + ', ' + bad.length + ' late or at risk', s: 'Tickets live in Jira. The agent proposes, you accept.', ring: [done.length, T.length] };
|
|
120
|
+
|
|
121
|
+
const left = [];
|
|
122
|
+
if (reading) left.push(listCard('check', 'Tickets', null, '', [emptyRow('clock', 'Reading the Jira tickets.', { role: 'status' })]));
|
|
123
|
+
else if (none) left.push(listCard('check', 'Tickets', 0, 'Jira only, read-only', [emptyRow('moon', 'The agent has not read Jira yet, so there are no tickets to show. Nothing here is invented.', { 'data-in-empty': '' })]));
|
|
124
|
+
else if (!T.length) left.push(listCard('check', 'Tickets', 0, 'Jira only, read-only', [emptyRow('check', 'No Jira ticket belongs to this initiative.', { 'data-in-empty': '' })]));
|
|
125
|
+
else {
|
|
126
|
+
if (late.length) left.push(listCard('alert', 'Late or at risk', late.length, '', late.map((t) => row(V, t)), { 'data-in-list': 'late' }));
|
|
127
|
+
if (doing.length) left.push(listCard('clock', 'Doing', doing.length, '', doing.map((t) => row(V, t)), { 'data-in-list': 'doing' }));
|
|
128
|
+
if (todo.length) left.push(listCard('moon', 'To do', todo.length, '', todo.map((t) => row(V, t)), { 'data-in-list': 'todo' }));
|
|
129
|
+
if (done.length) left.push(listCard('check', 'Done', done.length, '', done.map((t) => row(V, t)), { 'data-in-list': 'done' }));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const right = [];
|
|
133
|
+
if (T.length) {
|
|
134
|
+
const by = {};
|
|
135
|
+
T.forEach((t) => { if (t.category !== 'done') { const o = t.owner || 'You'; by[o] = (by[o] || 0) + (t.pts || 0); } });
|
|
136
|
+
const names = Object.keys(by).sort((a, b) => by[b] - by[a]);
|
|
137
|
+
const mx = Math.max(1, ...names.map((n) => by[n]));
|
|
138
|
+
if (names.some((n) => by[n] > 0)) right.push(card('user', 'Open points by owner', '', hBars(names.map((n, i) => [n, by[n] / mx, OWN_COL[i % OWN_COL.length], by[n] + ' pts']))));
|
|
139
|
+
}
|
|
140
|
+
if (D && D.j && D.j.raci) right.push(raciCard(D.j.raci));
|
|
141
|
+
if (D && D.j && Array.isArray(D.j.drafts) && D.j.drafts.length) {
|
|
142
|
+
right.push(card('plus', 'Agent drafts', 'propose only, you accept',
|
|
143
|
+
el('ul', { class: 'chgl' }, D.j.drafts.map((t) => el('li', { class: 'warn' }, c3ic('plus', 'sm'), el('span', null, t.title + ' · ' + t.pts + ' pts')))),
|
|
144
|
+
el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Nothing reaches Jira until you accept. Slide the card in the deck.')));
|
|
145
|
+
}
|
|
146
|
+
if (!right.length) right.push(card('info', 'Read-only mirror', '', el('ul', { class: 'chgl' },
|
|
147
|
+
el('li', null, c3ic('lock', 'sm'), el('span', null, 'Status, dates and owners are Jira’s. This view never writes to Jira.')),
|
|
148
|
+
el('li', null, c3ic('clock', 'sm'), el('span', null, 'The agent reads Jira with your connection and the server keeps the last read.')))));
|
|
149
|
+
const readAt = D && D.j && D.j.read_at;
|
|
150
|
+
if (readAt) right.push(el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', 'data-in-readat': '' }, 'Last read of Jira: ' + short(readAt, 25)));
|
|
151
|
+
frame(V, { head, left, right });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const view = makeView('tickets', { init: load, render });
|
|
155
|
+
export const mount = view.mount;
|
|
156
|
+
export const dispose = view.dispose;
|
|
157
|
+
|
|
158
|
+
/** The count on the Tickets button: the Jira tickets that are late or at risk. */
|
|
159
|
+
export async function badge(ctx) {
|
|
160
|
+
const j = await pmGet('/api/my-work' + (ctx && ctx.ws ? '?ws=' + encodeURIComponent(ctx.ws) : ''));
|
|
161
|
+
if (!j || !j.ok) return null;
|
|
162
|
+
return jiraTickets(j).filter((t) => t.category !== 'done' && (t.bad || t.risk)).length;
|
|
163
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/* pm/c3-timeline.js — the drawing of the Initiatives Timeline of /c3.html, ported from the console3 mockup (console3.html, patch21, lines 4014 to 4042):
|
|
2
|
+
* scrubHtml (the track of checks and Replay), ganttTix (the Gantt), timeRight (What changed), driftSVG and creepSVG (forecast and scope at each check),
|
|
3
|
+
* fcMatrix (the forecast of every initiative at every check) and calSyncCard (the dates that reach the Calendar).
|
|
4
|
+
*
|
|
5
|
+
* INPUT is one payload of GET /api/c3-timeline (workspace/lib/routes-c3-timeline.js): `frames`, one whole plan per check. Every function here takes a
|
|
6
|
+
* frame (or the frames) and returns DOM; nothing is fetched and nothing is written. A replay is a loop over frames, so the Gantt is redrawn from the
|
|
7
|
+
* frame of each check: bars move, forecast and slip change, items appear as they were added. The mockup folds ticket histories in the browser (tixAt,
|
|
8
|
+
* planOf); here the server has already folded the events and planned them with plan.js, so the same drawing reads the result.
|
|
9
|
+
* DIFFERENCES FROM THE MOCKUP, kept honest: the mockup's tickets carry a typed "critical" flag and a progress number; here the critical path is derived
|
|
10
|
+
* (slack 0 in plan.js) and progress comes from the item's state. Origin (Jira, Local, Figma, Calendar, Plan) is in each row's tooltip and in What
|
|
11
|
+
* changed; the mockup draws no origin chip inside the Gantt either (its chips are in the Breakdown rows).
|
|
12
|
+
*/
|
|
13
|
+
import { el } from './dom.js';
|
|
14
|
+
import { c3ic } from './pm-work-view.js';
|
|
15
|
+
|
|
16
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
17
|
+
const DAY = 86400000;
|
|
18
|
+
export const dnum = (s) => Math.round(Date.parse(String(s).slice(0, 10) + 'T00:00:00Z') / DAY);
|
|
19
|
+
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
20
|
+
export const dlab = (n) => { const d = new Date(n * DAY); return d.getUTCDate() + ' ' + MON[d.getUTCMonth()]; };
|
|
21
|
+
export const dlabS = (s) => dlab(dnum(s));
|
|
22
|
+
export const short = (t, n) => { t = String(t == null ? '' : t); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
|
23
|
+
const STATUS = { done: ['Done', 'var(--success)'], 'on time': ['On time', 'var(--success)'], 'at risk': ['At risk', 'var(--warning)'], delayed: ['Delayed', 'var(--destructive)'], overdue: ['Overdue', 'var(--destructive)'] };
|
|
24
|
+
const stat = (s) => STATUS[s] || STATUS['on time'];
|
|
25
|
+
|
|
26
|
+
function svg(tag, attrs, ...kids) {
|
|
27
|
+
const n = document.createElementNS(NS, tag);
|
|
28
|
+
for (const k of Object.keys(attrs || {})) if (attrs[k] != null) n.setAttribute(k, attrs[k]);
|
|
29
|
+
for (const c of kids) if (c) n.appendChild(c);
|
|
30
|
+
return n;
|
|
31
|
+
}
|
|
32
|
+
const text = (x, y, s, attrs) => { const t = svg('text', { x, y, ...(attrs || {}) }); t.textContent = s; return t; };
|
|
33
|
+
const title = (s) => { const t = svg('title', {}); t.textContent = s; return t; };
|
|
34
|
+
|
|
35
|
+
/** Who changed it, as the mockup's orig(): You, Agent, or the teammate. */
|
|
36
|
+
export function whoOf(by) {
|
|
37
|
+
const b = String(by || '');
|
|
38
|
+
if (/^(agent|claude-code)/i.test(b)) return { l: 'Agent', c: 'var(--opp, var(--accent))', i: 'spark' };
|
|
39
|
+
if (/^(pm:|human:wawan)/i.test(b) || b === 'Wawan' || b === 'You') return { l: 'You', c: 'var(--acc)', i: 'user' };
|
|
40
|
+
return { l: b.replace(/^(pm|human|agent):/, '') || 'Someone', c: 'var(--muted-foreground)', i: 'user' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A key a person would say out loud: the Jira key or PM-n, else the title alone. */
|
|
44
|
+
const rowName = (t) => (t.key ? t.key + ' ' + short(t.title, 17) : short(t.title || t.id, 22));
|
|
45
|
+
|
|
46
|
+
/** The mockup's ganttTix. frame: one entry of `frames`; prev: the frame before it (the dashed "where it was before"); hl: ids touched at this check. */
|
|
47
|
+
export function ganttSvg(frame, prev, hl, today) {
|
|
48
|
+
const items = frame.items.slice().sort((a, b) => dnum(a.start) - dnum(b.start) || (a.id < b.id ? -1 : 1));
|
|
49
|
+
const crit = new Set(frame.critical || []);
|
|
50
|
+
const ghost = new Map((prev ? prev.items : []).map((t) => [t.id, t.forecast]));
|
|
51
|
+
const idx = {}; items.forEach((t, i) => { idx[t.id] = i; });
|
|
52
|
+
const days = [];
|
|
53
|
+
for (const t of items) days.push(dnum(t.start), dnum(t.due), dnum(t.forecast.end));
|
|
54
|
+
const a = Math.min(...days) - 1; const b = Math.max(...days) + 2;
|
|
55
|
+
const W = 720; const LW = 176; const SW = 76; const CW = W - LW - SW; const N = b - a + 1; const dw = CW / N; const rh = 30; const top = 26; const H = top + items.length * rh + 10;
|
|
56
|
+
const X = (n) => LW + (n - a) * dw; const Xc = (n) => X(n) + dw / 2; const cy = (i) => top + i * rh + rh / 2;
|
|
57
|
+
const s = svg('svg', { class: 'pg', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Ticket timeline with dependencies and the critical path', 'data-tl-gantt': '', 'data-tl-check': frame.check });
|
|
58
|
+
for (let n = a; n <= b; n++) if (new Date(n * DAY).getUTCDay() === 1) { s.appendChild(svg('line', { x1: X(n), x2: X(n), y1: 18, y2: H - 4, stroke: 'var(--border)' })); s.appendChild(text(X(n) + 3, 14, dlab(n))); }
|
|
59
|
+
const td = dnum(today);
|
|
60
|
+
if (td >= a && td <= b) s.appendChild(svg('rect', { x: X(td), y: 20, width: dw, height: H - 24, fill: 'var(--accw)', rx: 6 }));
|
|
61
|
+
if (frame.promised) { const px = Xc(dnum(frame.promised)); s.appendChild(svg('line', { x1: px, x2: px, y1: 20, y2: H - 4, stroke: 'var(--success)', 'stroke-dasharray': '4 4', 'stroke-width': 2, 'data-tl-promised': frame.promised })); s.appendChild(text(px + 4, H - 8, 'promised', { fill: 'var(--success)' })); }
|
|
62
|
+
items.forEach((t, i) => (t.after || []).forEach((dk) => {
|
|
63
|
+
if (idx[dk] == null) return;
|
|
64
|
+
const pd = items[idx[dk]].forecast; const pt = t.forecast;
|
|
65
|
+
const x1 = X(dnum(pd.end) + 1); const y1 = cy(idx[dk]); const x2 = Math.max(X(dnum(pt.start)), x1 + 2); const y2 = cy(i); const xm = x1 + 6; const c = crit.has(t.id) && crit.has(dk);
|
|
66
|
+
s.appendChild(svg('path', { d: `M${x1} ${y1} H${xm} V${y2} H${x2 - 1}`, fill: 'none', stroke: c ? 'var(--destructive)' : 'var(--ring)', 'stroke-width': c ? 2 : 1.2, 'data-tl-dep': dk + '>' + t.id }));
|
|
67
|
+
s.appendChild(svg('path', { d: `M${x2} ${y2} l-5 -3 v6z`, fill: c ? 'var(--destructive)' : 'var(--ring)' }));
|
|
68
|
+
}));
|
|
69
|
+
items.forEach((t, i) => {
|
|
70
|
+
const p = t.forecast; const st = stat(p.status); const y = cy(i); const done = t.state === 'done'; const xs = X(dnum(t.start)); const xe = X(dnum(t.due) + 1); const g = ghost.get(t.id);
|
|
71
|
+
if (hl && hl.has(t.id)) s.appendChild(svg('rect', { x: 0, y: y - 13, width: W, height: 26, rx: 8, fill: 'var(--accw)', 'data-tl-hl': t.id }));
|
|
72
|
+
if (g && (g.end !== p.end || g.start !== p.start)) s.appendChild(svg('rect', { x: X(dnum(g.start)), y: y - 10, width: Math.max(4, X(dnum(g.end) + 1) - X(dnum(g.start))), height: 4, rx: 2, fill: 'none', stroke: 'var(--muted-foreground)', 'stroke-dasharray': '3 2', 'data-tl-ghost': t.id }));
|
|
73
|
+
const grp = svg('g', { opacity: done ? '.6' : '1', 'data-tl-row': t.id, 'data-tl-origin': t.origin || null, 'data-critical': crit.has(t.id) ? 'true' : null });
|
|
74
|
+
grp.appendChild(title(`${t.title || t.id} (${t.src || t.origin || 'plan'}${t.phase ? ', ' + t.phase : ''}${t.owner ? ', ' + t.owner : ''}), forecast ${p.start} to ${p.end}: ${st[0]}${p.slip_days > 0 ? ', ' + p.slip_days + ' days past the plan' : ''}`));
|
|
75
|
+
grp.appendChild(text(0, y + 4, (crit.has(t.id) ? '● ' : '') + rowName(t), { class: 't' + (crit.has(t.id) ? ' crit' : '') }));
|
|
76
|
+
if (t.ms) grp.appendChild(svg('path', { d: `M${Xc(dnum(t.due))} ${y - 9} l8 9 -8 9 -8 -9z`, fill: 'var(--card)', stroke: st[1], 'stroke-width': 2 }));
|
|
77
|
+
else {
|
|
78
|
+
grp.appendChild(svg('rect', { x: xs, y: y - 5, width: Math.max(4, xe - xs), height: 10, rx: 5, fill: st[1], 'fill-opacity': '.2' }));
|
|
79
|
+
const frac = done ? 1 : Math.max(0, Math.min(1, t.progress || 0));
|
|
80
|
+
if (frac > 0) grp.appendChild(svg('rect', { x: xs, y: y - 5, width: Math.max(done ? 4 : 0, (xe - xs) * frac), height: 10, rx: 5, fill: st[1] }));
|
|
81
|
+
}
|
|
82
|
+
if (!done && (p.end > t.due || p.start > t.start)) {
|
|
83
|
+
grp.appendChild(svg('rect', { x: X(dnum(p.start)), y: y + 6, width: Math.max(4, X(dnum(p.end) + 1) - X(dnum(p.start))), height: 4, rx: 2, fill: st[1], 'fill-opacity': '.7', 'data-tl-forecast': t.id }));
|
|
84
|
+
if (p.end > t.due) grp.appendChild(svg('circle', { cx: Xc(dnum(p.end)), cy: y, r: 5, fill: 'var(--destructive)', stroke: 'var(--card)', 'stroke-width': 2 }));
|
|
85
|
+
}
|
|
86
|
+
grp.appendChild(text(W - SW + 6, y + 4, done ? 'Done' : p.slip_days > 0 ? '+' + p.slip_days + ' d' : st[0], { style: 'fill:' + st[1], 'data-tl-status': t.id }));
|
|
87
|
+
s.appendChild(grp);
|
|
88
|
+
});
|
|
89
|
+
return s;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The mockup's scrubHtml: the header (As of …, Replay) and the track of checks. */
|
|
93
|
+
export function scrub(frames, n, h) {
|
|
94
|
+
const last = frames.length - 1;
|
|
95
|
+
const f = frames[n];
|
|
96
|
+
const dateLab = dlabS(f.date);
|
|
97
|
+
const track = el('div', { class: 'sntrack', role: 'group', 'aria-label': 'Timeline history', style: `grid-template-columns:repeat(${frames.length},minmax(0,1fr));--n:${frames.length}`, 'data-n': frames.length },
|
|
98
|
+
frames.map((fr, i) => el('button', { type: 'button', class: 'sn', 'aria-pressed': String(i === n), 'data-past': String(i <= n), 'data-tl-snap': i, onClick: () => h.snap(i) },
|
|
99
|
+
el('i'), el('b', null, dlabS(fr.date)), el('span', null, fr.label))));
|
|
100
|
+
const play = el('button', { type: 'button', class: 'ib sm', title: 'Replay how the plan changed', 'aria-label': 'Replay how the plan changed', 'data-tl-replay': '', onClick: () => h.replay() }, c3ic('next', 'sm'));
|
|
101
|
+
return el('div', null, el('div', { class: 'snh' }, el('span', { class: 'dim', style: 'font-size:var(--fs-2)', 'data-tl-asof': '' }, n === last ? 'Today' : 'As of ' + dateLab), play), track);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The Timeline history card: header, the track, the Gantt, the legend. */
|
|
105
|
+
export function historyCard(frames, n, h, today) {
|
|
106
|
+
const frame = frames[n]; const prev = n > 0 ? frames[n - 1] : null;
|
|
107
|
+
const hl = new Set((frame.events || []).map((e) => e.id));
|
|
108
|
+
return el('section', { class: 'card', 'data-tl-history': '' }, el('h3', null, c3ic('cal'), 'Timeline history ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'scrub or replay how the plan changed')),
|
|
109
|
+
scrub(frames, n, h), frame.items.length ? ganttSvg(frame, prev, hl, today) : el('div', { class: 'fold' }, c3ic('check', 'sm'), 'Nothing was in the plan at this check.'),
|
|
110
|
+
el('div', { class: 'pgl' }, el('span', null, '◆ milestone'), el('span', { style: 'color:var(--destructive)' }, '● critical path'), el('span', { style: 'color:var(--success)' }, '┆ promised'), el('span', null, 'thin bar = forecast'), el('span', null, 'dashed = where it was before')));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const wordsTx = (e) => (e.op === 'add' ? 'added' : e.op === 'remove' ? 'removed' : e.tx || 'edited');
|
|
114
|
+
/** The mockup's timeRight, first card: what changed at this check, who, and why. */
|
|
115
|
+
export function whatChangedCard(frames, n) {
|
|
116
|
+
const f = frames[n]; const p = n > 0 ? frames[n - 1] : null;
|
|
117
|
+
const head = n === 0 ? `Baseline: ${f.items.length} item${f.items.length === 1 ? '' : 's'}, ${f.points} points${f.promised ? ', promised ' + dlabS(f.promised) : ''}`
|
|
118
|
+
: `Since ${dlabS(p.date)}: ${f.points - p.points > 0 ? '+' + (f.points - p.points) + ' points' : f.points - p.points < 0 ? (f.points - p.points) + ' points' : 'no scope change'}${f.forecast_end && p.forecast_end && f.forecast_end !== p.forecast_end ? ', forecast ' + dlabS(p.forecast_end) + ' to ' + dlabS(f.forecast_end) : ', forecast unchanged'}`;
|
|
119
|
+
const ev = (f.events || []).slice(0, 12);
|
|
120
|
+
const more = (f.events || []).length - ev.length;
|
|
121
|
+
return el('section', { class: 'card', 'data-tl-changed': '' }, el('h3', null, c3ic('clock'), 'What changed ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, dlabS(f.date))),
|
|
122
|
+
el('div', { class: 'dim', style: 'font-size:var(--fs-2)', 'data-tl-head': '' }, head),
|
|
123
|
+
el('ul', { class: 'hx' }, ev.length ? ev.map((e) => { const o = whoOf(e.by);
|
|
124
|
+
return el('li', { 'data-tl-change': e.id }, el('span', { class: 'chip2', style: 'color:' + o.c }, c3ic(o.i, 'sm'), o.l),
|
|
125
|
+
el('span', null, (e.title || e.id) + ': ' + wordsTx(e), e.why ? el('small', null, e.why) : (e.origin ? el('small', null, 'No reason recorded · ' + e.origin) : null)), el('span', { class: 'w' }, e.key || '')); })
|
|
126
|
+
: [el('li', null, el('span'), el('span', null, 'No change at this check.'), el('span'))], more > 0 ? el('li', null, el('span'), el('span', { class: 'dim' }, '+' + more + ' more'), el('span')) : null));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The mockup's driftSVG: the forecast date at each check against the promise. */
|
|
130
|
+
export function driftSvg(frames, n) {
|
|
131
|
+
const W = 560; const H = 160; const x0 = 58; const x1 = W - 20; const y0 = 16; const y1 = H - 30;
|
|
132
|
+
const F2 = frames.map((f) => (f.forecast_end ? { fe: dnum(f.forecast_end), e: f.promised ? dnum(f.promised) : null } : null));
|
|
133
|
+
const have = F2.filter(Boolean);
|
|
134
|
+
const first = have[0];
|
|
135
|
+
const pr = first ? (first.e != null ? first.e : first.fe) : 0;
|
|
136
|
+
const vals = have.map((x) => x.fe);
|
|
137
|
+
const mn = Math.min(pr, ...vals) - 2; const mx = Math.max(pr, ...vals) + 2; const span = Math.max(1, mx - mn);
|
|
138
|
+
const X = (i) => x0 + i * (x1 - x0) / Math.max(1, frames.length - 1); const Y = (v) => y1 - (v - mn) / span * (y1 - y0);
|
|
139
|
+
const s = svg('svg', { class: 'mat', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Forecast date at each check against the promise', 'data-tl-drift': '' });
|
|
140
|
+
[pr, Math.round((mn + mx) / 2), mx - 1].forEach((v) => { s.appendChild(svg('line', { x1: x0, x2: x1, y1: Y(v), y2: Y(v), stroke: 'var(--border)' })); s.appendChild(text(x0 - 5, Y(v) + 4, dlab(v), { 'text-anchor': 'end' })); });
|
|
141
|
+
if (first && first.e != null) s.appendChild(svg('line', { x1: x0, x2: x1, y1: Y(pr), y2: Y(pr), stroke: 'var(--success)', 'stroke-dasharray': '5 4', 'stroke-width': 2 }));
|
|
142
|
+
const pts = []; F2.forEach((x, i) => { if (x) pts.push(X(i).toFixed(1) + ',' + Y(x.fe).toFixed(1)); });
|
|
143
|
+
s.appendChild(svg('polyline', { points: pts.join(' '), fill: 'none', stroke: 'var(--destructive)', 'stroke-width': 2.5, 'stroke-linejoin': 'round' }));
|
|
144
|
+
F2.forEach((x, i) => { if (!x) return; s.appendChild(svg('circle', { cx: X(i), cy: Y(x.fe), r: i === n ? 6 : 4, fill: x.fe > pr ? 'var(--destructive)' : 'var(--success)', stroke: 'var(--card)', 'stroke-width': 2 })); s.appendChild(text(X(i), H - 12, dlabS(frames[i].date), { 'text-anchor': 'middle' })); });
|
|
145
|
+
if (first && first.e != null) s.appendChild(text(x1, Y(pr) - 6, 'promised ' + dlab(pr), { 'text-anchor': 'end', fill: 'var(--success)' }));
|
|
146
|
+
return s;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The mockup's creepSVG: scope in points at each check against the baseline. */
|
|
150
|
+
export function creepSvg(frames, n) {
|
|
151
|
+
const W = 560; const H = 170; const x0 = 36; const x1 = W - 20; const y0 = 16; const y1 = H - 32;
|
|
152
|
+
const pp = frames.map((f) => f.points); const b0 = pp[0];
|
|
153
|
+
const mx = Math.max(...pp) + 6; const mn = Math.max(0, Math.min(...pp) - 6); const span = Math.max(1, mx - mn);
|
|
154
|
+
const X = (i) => x0 + i * (x1 - x0) / Math.max(1, frames.length - 1); const Y = (v) => y1 - (v - mn) / span * (y1 - y0);
|
|
155
|
+
const s = svg('svg', { class: 'mat', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Scope in points against the baseline of ' + b0 + ' points', 'data-tl-creep': '' });
|
|
156
|
+
const pts = []; pp.forEach((v, i) => { if (i) pts.push(X(i).toFixed(1) + ',' + Y(pp[i - 1]).toFixed(1)); pts.push(X(i).toFixed(1) + ',' + Y(v).toFixed(1)); });
|
|
157
|
+
const base = []; for (let i = pp.length - 1; i >= 0; i--) base.push(X(i).toFixed(1) + ',' + Y(b0).toFixed(1));
|
|
158
|
+
s.appendChild(svg('polygon', { points: pts.join(' ') + ' ' + base.join(' '), fill: 'var(--destructive)', 'fill-opacity': '.12' }));
|
|
159
|
+
s.appendChild(svg('line', { x1: x0, x2: x1, y1: Y(b0), y2: Y(b0), stroke: 'var(--ring)', 'stroke-dasharray': '5 4', 'stroke-width': 2 }));
|
|
160
|
+
s.appendChild(svg('polyline', { points: pts.join(' '), fill: 'none', stroke: 'var(--destructive)', 'stroke-width': 2.5, 'stroke-linejoin': 'round' }));
|
|
161
|
+
pp.forEach((v, i) => {
|
|
162
|
+
s.appendChild(svg('circle', { cx: X(i), cy: Y(v), r: i === n ? 6 : 4, fill: 'var(--destructive)', stroke: 'var(--card)', 'stroke-width': 2 }));
|
|
163
|
+
s.appendChild(text(X(i), H - 16, dlabS(frames[i].date), { 'text-anchor': 'middle' }));
|
|
164
|
+
if (i && v > pp[i - 1]) {
|
|
165
|
+
const add = (frames[i].events || []).find((e) => e.op === 'add');
|
|
166
|
+
s.appendChild(text(X(i), Y(v) - 10, '+' + (v - pp[i - 1]) + ' ' + short(add ? add.title : '', 16), { 'text-anchor': i === pp.length - 1 ? 'end' : 'middle' }));
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
s.appendChild(text(x1, Y(b0) + 14, 'baseline ' + b0 + ' points', { 'text-anchor': 'end' }));
|
|
170
|
+
return s;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The mockup's calSyncCard: milestones and late dates, the ones that reach the Calendar. */
|
|
174
|
+
export function calSyncCard(frame) {
|
|
175
|
+
const list = frame.items.filter((t) => t.state !== 'done' && (t.ms || t.forecast.status !== 'on time')).sort((a, b) => (a.forecast.end < b.forecast.end ? -1 : a.forecast.end > b.forecast.end ? 1 : 0)).slice(0, 6);
|
|
176
|
+
return el('section', { class: 'card', 'data-tl-cal': '' }, el('h3', null, c3ic('cal'), 'On your Calendar ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'dates that reach it')),
|
|
177
|
+
el('ul', { class: 'chgl' }, list.length ? list.map((t) => { const bad = t.forecast.status === 'delayed' || t.forecast.status === 'overdue';
|
|
178
|
+
return el('li', { class: bad ? 'bad' : 'ok' }, c3ic(bad ? 'clock' : 'check', 'sm'), el('span', null, (t.title || t.id) + ' · ' + dlabS(t.forecast.end) + (bad ? ' (plan ' + dlabS(t.due) + ')' : ''))); })
|
|
179
|
+
: [el('li', null, c3ic('check', 'sm'), el('span', null, 'Nothing dated.'))]),
|
|
180
|
+
el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Milestones and late dates appear in Calendar week and month. Meetings carry their initiative back here.'));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The mockup's fcMatrix: the forecast of every initiative at every check; a cell replays that check. */
|
|
184
|
+
export function matrixCard(data, titles, onCell) {
|
|
185
|
+
const checks = data.checks || [];
|
|
186
|
+
const head = el('tr', null, el('th'), checks.map((c) => el('th', { class: 'mono', style: 'font-size:var(--fs-1)' }, dlabS(c.date))));
|
|
187
|
+
const rows = (data.rows || []).map((r) => el('tr', null, el('th', { style: 'text-align:left' }, titles[r.id] || r.id), r.cells.map((c, n) => {
|
|
188
|
+
if (!c || !c.forecast_end) return el('td', null, '-');
|
|
189
|
+
const bad = c.promised && c.forecast_end > c.promised;
|
|
190
|
+
return el('td', null, el('button', { type: 'button', 'data-a': 'isnapgo', 'data-k': r.id + '|' + n, 'data-tl-cell': r.id + '|' + n, style: `color:${bad ? 'var(--destructive)' : 'var(--success)'};background:${bad ? 'color-mix(in oklab,var(--destructive) 10%,transparent)' : 'transparent'}`, onClick: () => onCell(r.id, n) }, dlabS(c.forecast_end)));
|
|
191
|
+
})));
|
|
192
|
+
return el('section', { class: 'card', 'data-tl-matrix': '' }, el('h3', null, c3ic('clock'), 'Forecast at each check ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'tap a cell to replay it')),
|
|
193
|
+
el('div', { style: 'overflow-x:auto' }, el('table', { class: 'fcm' }, el('tbody', null, head, rows))), el('div', { class: 'pgl' }, el('span', { style: 'color:var(--success)' }, 'on the promised date'), el('span', { style: 'color:var(--destructive)' }, 'after it')));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The Calendar card for the All scope: the commitments of c3-plan-panels.js (rows with a due date and a status) drawn as one frame, so the same card reads them. */
|
|
197
|
+
const FSTAT = { ok: 'on time', risk: 'at risk', late: 'delayed', over: 'overdue', done: 'on time', park: 'on time' };
|
|
198
|
+
export function calSyncFromCommits(rows) {
|
|
199
|
+
const items = (rows || []).filter((r) => r.st !== 'done' && r.st !== 'park').map((r) => ({ id: r.id, title: r.n, state: 'todo', ms: true, due: r.due, forecast: { end: r.fc || r.due, status: FSTAT[r.st] || 'on time' } }));
|
|
200
|
+
return calSyncCard({ items });
|
|
201
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* pm/c3-toast.js — the one toast every console3 view uses (console3.html toast(), line 1166): a dark pill at the bottom, 4.2 s, with an
|
|
2
|
+
* Undo button when the action can be taken back. One at a time. Two call shapes: toast('text', undoFn) like the mockup, or
|
|
3
|
+
* toast({ text, actionLabel, onAction, timeout, tone }) for a view that names its own button. The CSS is `.c3 .toast` in console3.css. */
|
|
4
|
+
import { el } from './dom.js';
|
|
5
|
+
import { ensureC3Css } from './pm-work-view.js';
|
|
6
|
+
|
|
7
|
+
let host = null; let timer = null;
|
|
8
|
+
export function toast(arg, undo) {
|
|
9
|
+
const o = arg && typeof arg === 'object' ? arg : { text: arg, onAction: undo };
|
|
10
|
+
ensureC3Css();
|
|
11
|
+
if (!host || !host.isConnected) { host = el('div', { class: 'c3 c3e c3pop', 'data-shell': 'toast' }); document.body.appendChild(host); }
|
|
12
|
+
const t = el('div', { class: 'toast' + (o.tone === 'error' ? ' err' : ''), role: 'status', 'aria-live': 'polite', 'data-c3-toast': '' }, el('span', null, o.text));
|
|
13
|
+
if (o.onAction) t.append(el('button', { type: 'button', 'data-c3-toast-action': '', onclick: () => { clearTimeout(timer); t.remove(); o.onAction(); } }, o.actionLabel || 'Undo'));
|
|
14
|
+
host.replaceChildren(t);
|
|
15
|
+
clearTimeout(timer); timer = setTimeout(() => t.remove(), o.timeout || 4200);
|
|
16
|
+
}
|