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.
- 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/briefing-cards.md +40 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/layout.md +2 -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/needs-overdue.js +41 -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 +109 -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 +413 -0
- package/workspace/public/pm/briefing-model.js +211 -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 +185 -0
- package/workspace/public/pm/c3-daypart.js +176 -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 +674 -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 +230 -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 +1014 -0
- package/workspace/public/pm/console3-lift.md +220 -0
- package/workspace/public/pm/console3.css +1547 -0
- package/workspace/public/pm/dest-initiatives.js +16 -3
- package/workspace/public/pm/dest-mywork.js +8 -1
- 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 +895 -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 +21 -3
- package/workspace/test-lists/browser-suites-green.json +5 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/* pm/lifecycle-lens.js — the Lifecycle lens of Initiatives (console3 wave B1; R4b: on the console3 mockup's own markup and CSS).
|
|
2
|
+
*
|
|
3
|
+
* The mockup (prototype/pmos-console-handoff/mockups/console3.html) is the UI. Lifted: the view bar `.calbar` with the nine stages
|
|
4
|
+
* as `.seg` buttons (icon, label, `.bdg.g` count) and the scope as `.chips2` pills (iniHead, line 3576); the lens header `.lh`
|
|
5
|
+
* (iniLensHead, line 3579); `.calwrap > .calbody > .brief.sbb > .bcols > .bcol.l/.r` (vIni, line 3600); the card deck `.cdeck >
|
|
6
|
+
* .deck > .fc` with the dots `.dk .dkd` (iniDeck, line 3580); the lifecycle line `.lc` for one initiative (lifecycle, line 3487;
|
|
7
|
+
* the mockup's seven columns are nine here, as the mockup's own override at c3:20 makes them); and the stage's list as
|
|
8
|
+
* `section.card.sect > .sh2 + ul.list > li.nrow` with a case file in `.xp > .xi` (c3needs, line 4714). Styles: pm/console3.css.
|
|
9
|
+
*
|
|
10
|
+
* What is real: the rows of the Initiatives view ctx (the roadmap horizons rows) and the nine stages derived from them
|
|
11
|
+
* (lifecycle-lens-model.js). Stage is derived, never set; a stage with no signal reads "no signal yet", never done or pending;
|
|
12
|
+
* a stage the lane skips reads skipped. Park (on hold) and Kill (cancelled) go through setLifecycle with a reason, as the sheet's
|
|
13
|
+
* lifecycle menu does. `nine_stages` is not in that payload yet, so the model mirrors scripts/nine-stages.js; the page says so.
|
|
14
|
+
* Not lifted: the mockup's invented cards (hypotheses, scores, ideas, charts, lines 3490 to 3562) and its right-column charts;
|
|
15
|
+
* the Build and Deliver lists of Jira tickets and releases (Releases is its own view).
|
|
16
|
+
* What a stage needs (c3needs, line 4713): one row per requirement of the stage (REQ in the model), for each initiative at it, each with a
|
|
17
|
+
* case file in place: an agent's requirement offers "Have the agent: draft it" and "Not yet"; the PM's decision offers its two answers;
|
|
18
|
+
* the factory's is read-only. This page cannot start an agent or record a stage decision, so in real mode those slides are drawn
|
|
19
|
+
* disabled with the reason; the demo (?demo=1) lets the slide mark it done. Park or Kill goes through the lifecycle route with the reason
|
|
20
|
+
* as the option. Swipe right or left and the keys e, n, j, k work on every row. A deck card per initiative at the stage (five at most; the rest are in
|
|
21
|
+
* the list) replaces the mockup's five invented cards.
|
|
22
|
+
* Self-registers as the 'lifecycle' Initiatives view, like Timeline and Releases.
|
|
23
|
+
*/
|
|
24
|
+
import { el } from './dom.js';
|
|
25
|
+
import { ensureComponentsCss } from './primitives.js';
|
|
26
|
+
import { registerInitiativesView } from './registry.js';
|
|
27
|
+
import { setLifecycle } from './initiatives-api.js';
|
|
28
|
+
import { STAGES, LABELS, bucketRows, stageNote, nineOf, REQ, reqWho, prevPending, DRAFT_TEXT, DECIDE_TEXT } from './lifecycle-lens-model.js';
|
|
29
|
+
import { rowShell, caseNode, markDone, say } from './c3-init-extras.js';
|
|
30
|
+
import { lensPanels } from './c3-lifecycle-panels.js';
|
|
31
|
+
import { strategyPanel } from './c3-strategy-panels.js';
|
|
32
|
+
import { ensureC3Css, c3ic, c3ring, c3Phone } from './pm-work-view.js';
|
|
33
|
+
|
|
34
|
+
const views = new WeakMap();
|
|
35
|
+
const PILLS = 7; // the mockup's scope row has five initiatives; a repository has hundreds, so the row shows the first few and says so
|
|
36
|
+
const LIST_MAX = 20; // a stage's list shows this many rows, then "Show all"
|
|
37
|
+
const ICON = { discover: 'search', ideate: 'spark', strategize: 'flag', prioritize: 'scale', plan: 'tree', build: 'branch', deliver: 'out', monitor: 'report', evaluate: 'check' }; // ILI, lines 3437 and 4686
|
|
38
|
+
const short = (t, n) => { t = String(t || ''); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
|
39
|
+
|
|
40
|
+
/* ── the mockup's Strategize lens (console3.html nsChart line 2833, sigCard 2883, matCard 2894), drawn only when ctx.strategy is given. The
|
|
41
|
+
* roadmap route does not send one; the /c3.html demo mode does (the mockup's north star series, early signals and maturity table). ───── */
|
|
42
|
+
const SVGNS = 'http://www.w3.org/2000/svg';
|
|
43
|
+
const sv = (tag, attrs, ...kids) => { const n = document.createElementNS(SVGNS, 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; };
|
|
44
|
+
const svt = (x, y, t, a) => { const n = sv('text', { x, y, ...(a || {}) }); n.textContent = t; return n; };
|
|
45
|
+
const SEV = { watch: ['Watch', 'var(--acc)', 'eye'], drift: ['Drift', 'var(--warning)', 'clock'], pivot: ['Pivot', 'var(--destructive)', 'alert'], up: ['Upside', 'var(--success)', 'up'] };
|
|
46
|
+
|
|
47
|
+
function miniSpark(a, c) {
|
|
48
|
+
const W = 64; const H = 26; const mn = Math.min(...a); const mx = Math.max(...a); const X = (i) => 3 + i * (W - 6) / (a.length - 1); const Y = (v) => H - 4 - (v - mn) / (mx - mn || 1) * (H - 8);
|
|
49
|
+
return sv('svg', { class: 'spk', viewBox: `0 0 ${W} ${H}`, 'aria-hidden': 'true', style: 'width:64px;height:26px' }, sv('polyline', { points: a.map((v, i) => X(i) + ',' + Y(v)).join(' '), fill: 'none', stroke: c, 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }), sv('circle', { cx: X(a.length - 1), cy: Y(a[a.length - 1]), r: 3, fill: c }));
|
|
50
|
+
}
|
|
51
|
+
function signalsCard(list) {
|
|
52
|
+
const order = ['pivot', 'drift', 'watch', 'up'];
|
|
53
|
+
return el('section', { class: 'card', 'data-lc-strategy': 'signals' }, el('h3', null, c3ic('alert'), 'Early signals ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'before the numbers say it')),
|
|
54
|
+
el('ul', { class: 'sgl', style: 'list-style:none;margin:0;padding:0' }, list.slice().sort((a, b) => order.indexOf(a.sev) - order.indexOf(b.sev)).slice(0, 4).map((g) => { const V = SEV[g.sev];
|
|
55
|
+
return el('li', { class: 'sgr', style: '--c:' + V[1] }, el('span', { class: 'ki', style: 'color:' + V[1] }, c3ic(V[2])),
|
|
56
|
+
el('div', { style: 'min-width:0' }, el('div', { class: 'st1' }, g.t), el('div', { class: 'st2' }, g.ev), el('div', { class: 'st3' }, el('span', { class: 'sv' }, V[0]), el('span', null, c3ic('target', 'sm'), g.thr), el('span', null, c3ic('clock', 'sm'), g.ttt))), miniSpark(g.sp, V[1])); })));
|
|
57
|
+
}
|
|
58
|
+
/** The mockup's lifecycle(id) line: one cell per stage; done is `d`, the current one `n`. */
|
|
59
|
+
function lcLine(row) {
|
|
60
|
+
const nine = nineOf(row);
|
|
61
|
+
return el('div', { class: 'lc', role: 'img', 'aria-label': 'Lifecycle' }, nine.map((s) => el('div', {
|
|
62
|
+
class: s.status === 'done' ? 'd' : s.status === 'current' ? 'n' : '', title: s.label + ': ' + (s.status === 'no-signal' ? 'no signal yet' : s.status),
|
|
63
|
+
'data-lc-cell': s.key, 'data-status': s.status, style: s.status === 'skipped' ? 'opacity:.4' : null,
|
|
64
|
+
}, el('i'), (s.label || s.key).slice(0, 5))));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One deck card for an initiative at the stage: the mockup's card (`.fc > .cf > .cfa + .cfh + .cff`) with real fields only. */
|
|
68
|
+
function deckCard(V, row, i, n) {
|
|
69
|
+
const nine = nineOf(row);
|
|
70
|
+
const signalled = nine.filter((s) => s.status === 'done' || s.status === 'current' || s.status === 'pending');
|
|
71
|
+
const done = nine.filter((s) => s.status === 'done').length;
|
|
72
|
+
const cur = nine.find((s) => s.status === 'current');
|
|
73
|
+
const open = () => V.ctx.openInitiative(V.ctx.repo, row.id);
|
|
74
|
+
return el('article', { class: 'fc lnk' + (i === 0 ? ' on' : ''), tabindex: '0', role: 'button', 'aria-label': 'Open ' + row.title + ', card ' + (i + 1) + ' of ' + n,
|
|
75
|
+
'data-lc-deck': row.id, style: '--tc:var(--acc)', onClick: open, onKeydown: (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } } },
|
|
76
|
+
el('div', { class: 'ff' },
|
|
77
|
+
el('div', null, el('span', { class: 'tg f', style: '--tc:var(--acc)' }, c3ic('init', 'sm'), row.lane || 'feature')),
|
|
78
|
+
el('div', { class: 'cf' },
|
|
79
|
+
el('div', { class: 'cfa' }, el('h2', null, row.title)),
|
|
80
|
+
el('div', { class: 'cfh' }, el('div', { class: 'hero', 'data-hero': 'ring' }, c3ring(done, Math.max(1, signalled.length), 64, done + '/' + signalled.length))),
|
|
81
|
+
el('div', { class: 'cff' },
|
|
82
|
+
el('div', { class: 'cfx' }, el('span', null, c3ic('clock', 'sm'), cur ? 'now: ' + cur.label : 'all signalled stages are done')),
|
|
83
|
+
el('span', { class: 'cfr n' }, 'Tap to open it ', c3ic('next', 'sm'))))));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ── what a stage needs: one row per requirement, each with a case file (console3.html c3needs, line 4713; C3.cases.rq line 4719) ── */
|
|
87
|
+
const isDemo = () => !!window.__c3demo;
|
|
88
|
+
/** Is a requirement done for an initiative at this stage? The nine-stage rule says a stage that is done has all its requirements; at the current stage nothing is
|
|
89
|
+
* recorded per requirement, so none is shown done unless the PM's slide has just recorded it (the demo also carries the mockup's own table). */
|
|
90
|
+
function reqDone(V, row, L, key) {
|
|
91
|
+
const st = (nineOf(row).find((x) => x.key === L) || {}).status;
|
|
92
|
+
if (st === 'done') return true;
|
|
93
|
+
if ((V.rq || {})[row.id + '|' + key]) return true;
|
|
94
|
+
const d = window.__c3reqDone && window.__c3reqDone[row.id]; return !!(d && d[key]);
|
|
95
|
+
}
|
|
96
|
+
const openIt = (V, row) => V.ctx.openInitiative(V.ctx.repo, row.id);
|
|
97
|
+
|
|
98
|
+
function rqCase(V, row, L, r) {
|
|
99
|
+
const [key, text, who] = r; const nm = row.title; const stg = LABELS[L]; const demo = isDemo();
|
|
100
|
+
const done = reqDone(V, row, L, key);
|
|
101
|
+
const lane = nineOf(row); void lane;
|
|
102
|
+
const base = [['init', nm + ', ' + (row.lane || 'feature') + ' lane', 'Lane'], ['target', 'Anchor: ' + (row.anchor || 'none'), 'Anchor', row.anchor ? 'ok' : 'bad']];
|
|
103
|
+
const go = ['open', row.id, 'Open ' + nm];
|
|
104
|
+
if (done) return { sit: text + ' is done for ' + nm + '.', conf: 90, chg: [['check', 'Counted in the derived stage', '', 'ok']], opts: [], go };
|
|
105
|
+
if (who === 'factory') return { sit: 'The factory builds from the tickets and opens pull requests. Its progress fills this by itself. You cannot tick it.', conf: 88, chg: [['branch', 'Merged work is counted from Jira, read-only', 'Jira, CI', 'ok'], ['shield', 'The factory is propose-only: it never merges and never writes this record', 'D65']], opts: [], go };
|
|
106
|
+
if (who === 'agent') {
|
|
107
|
+
const t = DRAFT_TEXT[key] || ['Draft it', ''];
|
|
108
|
+
return { sit: 'Nothing has this yet. An agent drafts it and you read it before it counts. ' + nm + ' stays at ' + stg + ' until it exists.', conf: 82, chg: base, cta: 'Slide to start the draft', go,
|
|
109
|
+
outboxReason: 'An agent session does the drafting, and this page cannot start one. Ask it in your terminal; the draft comes back for you to read.',
|
|
110
|
+
opts: [{ id: 'go', l: 'Have the agent: ' + t[0].toLowerCase(), rec: 1, outward: !demo, fx: [['spark', t[1], 'ok'], ['shield', 'Inside the repo; nothing goes outside', 'ok']] }, { id: 'nt', l: 'Not yet', fx: [['clock', 'The stage stays at ' + stg, 'warn']] }] };
|
|
111
|
+
}
|
|
112
|
+
const pend = prevPending(L, key, (k) => reqDone(V, row, L, k));
|
|
113
|
+
if (pend.length) return { sit: 'This is your decision, and it comes after the agent’s drafts. Read them first: approving without reading is how errors go out.', conf: 70, chg: [['clock', 'Waiting for: ' + pend.map((q) => q[1]).join('; '), 'agent', 'warn']], opts: [], go };
|
|
114
|
+
const d = DECIDE_TEXT[key] || [['Decide yes', ''], ['Not yet', ''], 'This is your decision.'];
|
|
115
|
+
return { sit: d[2], conf: key === 'cont' ? 62 : 78, chg: [['init', nm + ' · ' + stg, 'Stage'], base[1]], cta: 'Slide to decide', unsure: key === 'cont' ? 'It is a close call.' : '', ign: 'The initiative stays at ' + stg + '.', go,
|
|
116
|
+
outboxReason: 'This page has no route to record that decision yet, so it stays a proposal. Nothing is written.',
|
|
117
|
+
opts: [{ id: 'ok', l: d[0][0], rec: 1, outward: !demo, fx: [['check', d[0][1], 'ok']] }, { id: 'no', l: d[1][0], fx: [['clock', d[1][1], 'warn']] }] };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A requirement row. Swipe right starts the draft (the agent's requirements only); swipe left leaves it. Both say what they did; a draft only starts where the page can start one. */
|
|
121
|
+
function rqRow(V, row, L, r) {
|
|
122
|
+
const key = row.id + '|' + r[0]; const w = reqWho(r[2]); const done = reqDone(V, row, L, r[0]); const scope = V.scope;
|
|
123
|
+
const decide = (o) => {
|
|
124
|
+
if (o.id === 'go' || o.id === 'ok') { V.rq = V.rq || {}; V.rq[key] = 1; const m = o.id === 'go' ? 'Drafted. It waits in Review for you.' : o.l + '. Recorded as your decision.'; say(m, () => { delete V.rq[key]; V.render(); }); markDone(V, 'rq:' + key, m, () => { delete V.rq[key]; }); } else markDone(V, 'rq:' + key, 'Left as it is.');
|
|
125
|
+
};
|
|
126
|
+
const sw = { labelRight: r[2] === 'agent' ? 'start the draft' : 'open it first', labelLeft: 'leave it', iconRight: 'spark', iconLeft: 'moon',
|
|
127
|
+
onRight: () => say(r[2] === 'agent' && !done ? (isDemo() ? 'Open it, pick the draft, then slide: a draft starts only from its case file.' : 'An agent session does the drafting. This page cannot start one.') : done ? 'Already done.' : 'Open it first: a decision needs its case file.'),
|
|
128
|
+
onLeft: () => say('Left as it is.') };
|
|
129
|
+
const pend = !done && r[2] === 'pm' && prevPending(L, r[0], (k) => reqDone(V, row, L, k)).length;
|
|
130
|
+
return rowShell(V, { key: 'rq' + key, attrs: { 'data-rq': key, 'data-id': 'rq|' + key, 'data-lc-card': row.id }, icon: done ? 'check' : w[0], iconColor: done ? 'var(--success)' : null, sw,
|
|
131
|
+
titleNode: el('span', { class: 'tt', style: done ? 'text-decoration:line-through;color:var(--muted-foreground)' : '' }, r[1]),
|
|
132
|
+
xpContent: caseNode(V, 'rq:' + key, rqCase(V, row, L, r), { onConfirm: decide, onGo: () => openIt(V, row) }),
|
|
133
|
+
chips: [el('span', { class: 'tag' }, w[1]), scope ? null : el('span', { class: 'chip2' }, c3ic('init', 'sm'), row.title), pend ? el('span', { class: 'chip2' }, c3ic('clock', 'sm'), 'after the agent’s drafts') : null] });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Park it or kill it (console3.html C3.cases.ini, line 4745): the reason is the option, so a reason is always recorded. Both go through the same lifecycle route as the initiative sheet. */
|
|
137
|
+
function parkRow(V, row) {
|
|
138
|
+
const key = 'par|' + row.id; const demo = isDemo();
|
|
139
|
+
const mk = (id, why, kill) => ({ id, l: (kill ? 'Kill it: ' : 'Park it: ') + why, fx: [['moon', kill ? 'Closed for good' : 'Comes back to the planning list', 'warn']], state: kill ? 'cancelled' : 'on-hold', why });
|
|
140
|
+
const opts = [{ id: 'keep', l: 'Keep it going', rec: 1, fx: [['check', 'Nothing changes', 'ok']] }, mk('p1', 'not now'), mk('p2', 'no evidence'), mk('p3', 'replaced'), mk('k1', 'out of scope', true)];
|
|
141
|
+
const decide = async (o) => {
|
|
142
|
+
if (o.id === 'keep') { markDone(V, 'rq:' + key, 'Kept going.'); return; }
|
|
143
|
+
const out = await setLifecycle(V.ctx.ws, row.id, o.state, o.why);
|
|
144
|
+
if (!out || !out.ok) { say((out && out.error) || 'Not recorded.'); return; }
|
|
145
|
+
try { window.dispatchEvent(new CustomEvent('pmos:initiatives-changed')); } catch { /* no window */ }
|
|
146
|
+
const undo = async () => { await setLifecycle(V.ctx.ws, row.id, 'active', 'Undone from the lifecycle lens'); try { window.dispatchEvent(new CustomEvent('pmos:initiatives-changed')); } catch { /* no window */ } };
|
|
147
|
+
say((o.state === 'cancelled' ? 'Killed: ' : 'Parked: ') + o.why + '.', undo); markDone(V, 'rq:' + key, (o.state === 'cancelled' ? 'Killed: ' : 'Parked: ') + o.why + '. Undo reopens it.', undo);
|
|
148
|
+
};
|
|
149
|
+
void demo;
|
|
150
|
+
return rowShell(V, { key: 'rq' + key, attrs: { 'data-lc-park-row': row.id, 'data-id': 'par|' + row.id }, icon: 'moon', hint: false,
|
|
151
|
+
titleNode: el('span', { class: 'tt' }, 'Park it or kill it'),
|
|
152
|
+
xpContent: caseNode(V, 'rq:' + key, { sit: 'Stopping is a decision too. Pick the reason; it is recorded and the agent reads it before suggesting the idea again.', conf: 74, chg: [['init', row.title, 'Initiative']], opts, cta: 'Slide to record it', go: ['open', row.id, 'Open ' + row.title] }, { onConfirm: decide, onGo: () => openIt(V, row) }),
|
|
153
|
+
chips: [el('span', { class: 'tag' }, 'you'), el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, 'a reason is always recorded'), V.scope ? null : el('span', { class: 'chip2' }, c3ic('init', 'sm'), row.title)] });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function render(V) {
|
|
157
|
+
const rows = V.ctx.rows || [];
|
|
158
|
+
const scope = V.scope;
|
|
159
|
+
const { buckets, unplaced } = bucketRows(rows, scope);
|
|
160
|
+
const ST = V.ctx.strategy || null; // optional (demo): the mockup's Strategize lens content; the lens opens on it
|
|
161
|
+
if (!V.lens) V.lens = ST ? 'strategize' : STAGES.find((k) => buckets[k].length) || 'discover';
|
|
162
|
+
const lens = V.lens;
|
|
163
|
+
const stratOn = lens === 'strategize'; // the Calendar's strategy panels (c3-strategy-panels.js), as the mockup draws them under Strategize (iniLeft 3586, iniRight 3595)
|
|
164
|
+
const list = buckets[lens];
|
|
165
|
+
const note = stageNote(lens, rows, scope);
|
|
166
|
+
|
|
167
|
+
const seg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Initiative lifecycle', style: 'overflow-x:auto;max-width:100%' }, STAGES.map((k) => {
|
|
168
|
+
const n = buckets[k].length;
|
|
169
|
+
return el('button', { type: 'button', 'aria-pressed': String(lens === k), 'data-lc-stage': k, 'data-lc-note': stageNote(k, rows, scope) || '', onClick: () => { V.lens = k; render(V); } },
|
|
170
|
+
c3ic(ICON[k], 'sm'), LABELS[k], n ? [' ', el('span', { class: 'bdg g' }, String(n))] : null);
|
|
171
|
+
}));
|
|
172
|
+
const pills = el('div', { class: 'chips2', 'data-lc-scope': '' },
|
|
173
|
+
el('button', { type: 'button', class: 'pill', 'aria-pressed': String(!scope), onClick: () => { V.scope = null; V.lens = null; render(V); } }, 'All'),
|
|
174
|
+
...rows.slice(0, PILLS).map((r) => el('button', { type: 'button', class: 'pill', 'aria-pressed': String(scope === r.id), 'data-lc-pill': r.id, title: r.title, onClick: () => { V.scope = r.id; V.lens = null; render(V); } }, short(r.title, 14))),
|
|
175
|
+
rows.length > PILLS ? el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)', 'data-lc-more': '' }, '+' + (rows.length - PILLS) + ' more in the list') : null);
|
|
176
|
+
|
|
177
|
+
const placed = STAGES.reduce((n, k) => n + buckets[k].length, 0);
|
|
178
|
+
const PN = stratOn ? null : lensPanels(V, lens, scope); // the panels the mockup draws for this stage (c3-lifecycle-panels.js)
|
|
179
|
+
const HD = PN && PN.head;
|
|
180
|
+
const head = stratOn && ST ? el('div', { class: 'lh', 'data-lc-derived': '' }, el('span', { class: 'ki' }, c3ic('flag')), el('div', { style: 'min-width:0' }, el('b', null, ST.head.h), el('div', { class: 'dim' }, ST.head.s)), c3ring(ST.head.ring[0], ST.head.ring[1], 44, ST.head.ring[0] + '/' + ST.head.ring[1]))
|
|
181
|
+
: HD ? el('div', { class: 'lh', 'data-lc-derived': '', 'data-lc-head': lens }, el('span', { class: 'ki' }, c3ic(HD.icon)), el('div', { style: 'min-width:0' }, el('b', null, HD.h), el('div', { class: 'dim' }, HD.s)), c3ring(HD.ring[0], Math.max(1, HD.ring[1]), 44, HD.ring[0] + '/' + HD.ring[1]))
|
|
182
|
+
: el('div', { class: 'lh', 'data-lc-derived': '' },
|
|
183
|
+
el('span', { class: 'ki' }, c3ic(ICON[lens])),
|
|
184
|
+
el('div', { style: 'min-width:0' },
|
|
185
|
+
el('b', null, note === 'skipped' ? LABELS[lens] + ': skipped by the lane' : note === 'no-signal' ? LABELS[lens] + ': no signal yet' : list.length + ' initiative' + (list.length === 1 ? '' : 's') + ' at ' + LABELS[lens]),
|
|
186
|
+
el('div', { class: 'dim' }, 'Stage is derived from the records; nothing here sets it.')),
|
|
187
|
+
c3ring(placed, Math.max(1, scope ? 1 : rows.length), 44, placed + '/' + (scope ? 1 : rows.length)));
|
|
188
|
+
|
|
189
|
+
const sctx = { goInit: (id) => V.ctx.openInitiative(V.ctx.repo, id) };
|
|
190
|
+
const deckCards = list.slice(0, 5);
|
|
191
|
+
const deck = deckCards.length ? el('section', { class: 'cdeck', 'data-n': '1' },
|
|
192
|
+
el('div', { class: 'deck', tabindex: '-1', role: 'group', 'aria-label': 'Initiatives at ' + LABELS[lens] }, deckCards.map((r, i) => deckCard(V, r, i, deckCards.length))),
|
|
193
|
+
deckCards.length > 1 ? el('div', { class: 'dk', role: 'group', 'aria-label': 'Cards' }, deckCards.map((r, i) => el('button', { type: 'button', class: 'dkd', 'aria-label': 'Card ' + (i + 1), 'aria-current': String(i === 0) }))) : null) : null;
|
|
194
|
+
if (deck) {
|
|
195
|
+
const d = deck.querySelector('.deck');
|
|
196
|
+
d.addEventListener('scroll', () => {
|
|
197
|
+
const f = d.children[0]; if (!f) return;
|
|
198
|
+
const i = Math.max(0, Math.min(d.children.length - 1, Math.round(d.scrollLeft / (f.offsetWidth + 12))));
|
|
199
|
+
[].forEach.call(d.children, (c, j) => c.classList.toggle('on', j === i));
|
|
200
|
+
deck.querySelectorAll('.dkd').forEach((x, j) => x.setAttribute('aria-current', String(j === i)));
|
|
201
|
+
});
|
|
202
|
+
deck.querySelectorAll('.dkd').forEach((b, j) => b.addEventListener('click', () => { const c = d.children[j]; if (c) d.scrollTo({ left: c.offsetLeft - d.children[0].offsetLeft, behavior: 'smooth' }); }));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const scoped = scope ? rows.find((r) => r.id === scope) : null;
|
|
206
|
+
const lcCard = scoped ? el('section', { class: 'card' }, el('h3', null, c3ic('init'), scoped.title + ' ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'lifecycle')), lcLine(scoped)) : null;
|
|
207
|
+
const reqRows = [];
|
|
208
|
+
const needRows = scope ? rows.filter((r) => r.id === scope && (nineOf(r).find((x) => x.key === lens) || {}).status !== 'skipped') : list; // the mockup (c3needs, line 4715): a chosen initiative shows its rows for any stage its lane uses
|
|
209
|
+
for (const row of needRows) { REQ[lens].forEach((r) => reqRows.push(() => rqRow(V, row, lens, r))); reqRows.push(() => parkRow(V, row)); }
|
|
210
|
+
const shown = V.all ? reqRows : reqRows.slice(0, LIST_MAX);
|
|
211
|
+
const needs = el('section', { class: 'card sect', 'data-lc-list': lens },
|
|
212
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('check', 'sm'), 'What ' + LABELS[lens] + ' needs'), el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, 'stage is derived, not chosen')),
|
|
213
|
+
el('ul', { class: 'list' }, needRows.length ? [...shown.map((f) => f()),
|
|
214
|
+
reqRows.length > LIST_MAX && !V.all ? el('li', { class: 'fold' }, el('button', { type: 'button', class: 'pill', 'data-lc-showall': '', onClick: () => { V.all = true; render(V); } }, 'Show all ' + reqRows.length)) : null]
|
|
215
|
+
: el('li', { class: 'fold', 'data-lc-empty': '' }, c3ic(note === 'skipped' ? 'moon' : 'check', 'sm'),
|
|
216
|
+
note === 'skipped' || scope ? 'The ' + ((rows.find((r) => r.id === scope) || {}).lane || 'feature') + ' lane skips ' + LABELS[lens] + '.' : note === 'no-signal' ? 'No signal yet for ' + LABELS[lens] + '. Nothing is done or pending until a record shows it.' : 'No initiative is at ' + LABELS[lens] + ' now.')));
|
|
217
|
+
const right = [
|
|
218
|
+
...(PN && PN.right.length ? [...PN.right, ...(lens === 'monitor' && ST && ST.signals ? [signalsCard(ST.signals.slice(0, 3))] : [])] : lens === 'monitor' && ST && ST.signals ? [signalsCard(ST.signals.slice(0, 3))] : [el('section', { class: 'card' }, el('h3', null, c3ic('info'), 'How the stage is derived'),
|
|
219
|
+
el('ul', { class: 'chgl' },
|
|
220
|
+
el('li', null, c3ic('check', 'sm'), el('span', null, 'Discover, Plan, Build and Evaluate read from the records.')),
|
|
221
|
+
el('li', null, c3ic('clock', 'sm'), el('span', null, 'The other stages have no signal yet, so none is shown done or pending.')),
|
|
222
|
+
el('li', null, c3ic('moon', 'sm'), el('span', null, 'A stage the lane skips reads skipped.'))))]),
|
|
223
|
+
unplaced.length ? el('section', { class: 'card', 'data-lc-unplaced': '' }, el('h3', null, c3ic('alert'), 'No stage shown yet'),
|
|
224
|
+
el('ul', { class: 'chgl' }, unplaced.map((r) => el('li', null, c3ic('init', 'sm'), el('span', null, r.title))))) : null,
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
V.root.replaceChildren(el('div', { class: 'calwrap' },
|
|
228
|
+
el('div', { class: 'calbar' }, pills),
|
|
229
|
+
el('div', { class: 'calbar', style: 'flex-wrap:wrap;gap:8px' }, seg),
|
|
230
|
+
el('div', { class: 'calbody' },
|
|
231
|
+
head, V.ctx.links || null,
|
|
232
|
+
V.notice ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), V.notice) : null,
|
|
233
|
+
stratOn && rows.length
|
|
234
|
+
? el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, V.ctx.plan || deck, lcCard, strategyPanel('maturity', sctx), strategyPanel('bets', sctx), needs), el('div', { class: 'bcol r' }, strategyPanel('north', sctx, { view: 'year', plain: true }), strategyPanel('signals', sctx, { n: 4 }), strategyPanel('synergy', sctx))))
|
|
235
|
+
: rows.length
|
|
236
|
+
? el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, V.ctx.plan || deck, lcCard, ...(PN ? PN.left : []), needs, ...(PN ? PN.after : [])), el('div', { class: 'bcol r' }, right)))
|
|
237
|
+
: el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, V.ctx.plan || null, el('div', { class: 'fold' }, c3ic('check', 'sm'), 'No initiatives yet.')))))));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function mountLifecycle(host, ctx) {
|
|
241
|
+
let V = views.get(host);
|
|
242
|
+
if (V) { V.ctx = ctx; render(V); if (host.firstChild !== V.root) host.replaceChildren(V.root); return; }
|
|
243
|
+
ensureComponentsCss();
|
|
244
|
+
ensureC3Css();
|
|
245
|
+
V = { ctx, scope: null, lens: null, open: {}, notice: null, root: el('div', { class: 'c3', 'data-in-view': 'lifecycle' }) };
|
|
246
|
+
V.render = () => render(V);
|
|
247
|
+
V.stopPhone = c3Phone(V.root);
|
|
248
|
+
views.set(host, V);
|
|
249
|
+
host.replaceChildren(V.root);
|
|
250
|
+
render(V);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function disposeLifecycle(host) {
|
|
254
|
+
const V = views.get(host);
|
|
255
|
+
if (!V) return;
|
|
256
|
+
views.delete(host);
|
|
257
|
+
if (V.stopPhone) V.stopPhone();
|
|
258
|
+
if (V.root.parentNode === host) V.root.remove();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
registerInitiativesView('lifecycle', { label: 'Lifecycle', mount: mountLifecycle, dispose: disposeLifecycle });
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
/* pm/new-initiative-sheet.js — the New-initiative form (pm-os-absorb Run 2, slice 2D; PRD R17, R25).
|
|
2
2
|
*
|
|
3
3
|
* Title, Lane, Anchor, Add. An initiative with no anchor is refused here, before any request, and again by the server.
|
|
4
|
+
* One line of intent can be drafted first (POST /api/intake/draft): it shows the lane guess with its confidence, the anchor
|
|
5
|
+
* suggestion (offered, never filled in: null reads 'no anchor yet') and any prior rejection. A refused draft blocks Add until the
|
|
6
|
+
* PM ticks the acknowledgement, which re-drafts with acknowledged:true. The stage shown at the start is Discover; it is derived.
|
|
4
7
|
* The new initiative starts in Later: only the PM's press moves it into Now. The anchors on offer are the ones the List
|
|
5
8
|
* already shows (a KR or a `health:<id>`), plus one typed by hand.
|
|
6
9
|
*
|
|
7
10
|
* openNewInitiative({ repo, ws, anchors: string[], prefill?: { title, lane, from }, onAdded(id) })
|
|
8
11
|
* `prefill` is a triaged capture that still needs an anchor: the title and the lane are filled in, and `from` names the capture.
|
|
12
|
+
*
|
|
13
|
+
* R4b: the sheet body is the mockup's new-initiative intake (console3.html c3niLine and c3niCard, lines 4786 and 4787): the one-line
|
|
14
|
+
* add `.c3add > .form` (the input and the spark button), the suggestion as `section.card.sect` with `.sh2`, then `.case > .sit +
|
|
15
|
+
* .chgl` rows (the lane guess, the anchor, the prior rejection as `li.ok` or `li.warn` or `li.bad` with a `.tag`), and the
|
|
16
|
+
* fields as `.c3jf` label rows. The sheet itself is the real dialog (primitives sheet), and its Cancel and Add are the sheet's.
|
|
17
|
+
* Not lifted: the microphone (nothing transcribes), the "try" example pills (invented lines), the four-step progress list
|
|
18
|
+
* (the draft answers at once), the lane options with a slide (Add is a press, as before), the anchor pills (the anchor stays a
|
|
19
|
+
* select, since the page and its tests set it by value), and the phone bottom sheet. The anchors on offer are the List's.
|
|
9
20
|
*/
|
|
10
21
|
|
|
11
22
|
import { el } from './dom.js';
|
|
12
|
-
import { button,
|
|
23
|
+
import { button, sheet, toast, ensureComponentsCss } from './primitives.js';
|
|
13
24
|
import { repoLanes, repoProfile } from './data.js';
|
|
14
25
|
import { COPY } from './initiatives-copy.js';
|
|
15
26
|
import { createInitiative } from './initiatives-api.js';
|
|
27
|
+
import { pmPost } from './http.js';
|
|
16
28
|
import { changed } from './initiative-sheet.js';
|
|
29
|
+
import { ensureC3Css, c3ic, c3ib } from './pm-work-view.js';
|
|
30
|
+
import { renderCase } from './c3-case.js';
|
|
17
31
|
|
|
18
32
|
const OTHER = '__other__';
|
|
19
33
|
|
|
@@ -24,38 +38,114 @@ export function laneNames(repo) {
|
|
|
24
38
|
return lanes.length ? lanes : null;
|
|
25
39
|
}
|
|
26
40
|
|
|
41
|
+
const row = (label, control) => el('label', null, el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, label), control);
|
|
42
|
+
|
|
27
43
|
export function openNewInitiative({ repo, ws, anchors, prefill, onAdded }) {
|
|
28
44
|
ensureComponentsCss();
|
|
45
|
+
ensureC3Css();
|
|
29
46
|
const anchorList = Array.from(new Set((anchors || []).filter(Boolean)));
|
|
30
|
-
const t =
|
|
47
|
+
const t = el('input', { type: 'text', 'aria-label': COPY.newForm.titleLabel, 'data-in-new-title': '', autocomplete: 'off', value: (prefill && prefill.title) || '' });
|
|
31
48
|
const lanes = laneNames(repo);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
49
|
+
let l;
|
|
50
|
+
if (lanes) {
|
|
51
|
+
l = el('select', { 'aria-label': COPY.newForm.laneLabel, 'data-in-new-lane': '' }, lanes.map((x) => el('option', { value: x }, x)));
|
|
52
|
+
l.value = prefill && lanes.includes(prefill.lane) ? prefill.lane : lanes[0];
|
|
53
|
+
} else l = el('input', { type: 'text', 'aria-label': COPY.newForm.laneLabel, 'data-in-new-lane': '', placeholder: COPY.newForm.laneHint, value: (prefill && prefill.lane) || '' });
|
|
54
|
+
const a = el('select', { 'aria-label': COPY.newForm.anchorLabel, 'data-in-new-anchor': '' }, [{ value: '', label: COPY.newForm.anchorPick }, ...anchorList.map((x) => ({ value: x, label: x })), { value: OTHER, label: COPY.newForm.anchorOther }].map((o) => el('option', { value: o.value }, o.label)));
|
|
55
|
+
a.value = '';
|
|
56
|
+
const typed = el('input', { type: 'text', 'aria-label': COPY.newForm.anchorLabel, 'data-in-new-anchor-text': '', placeholder: COPY.newForm.anchorHint, autocomplete: 'off' });
|
|
38
57
|
typed.hidden = true;
|
|
39
|
-
a.
|
|
40
|
-
|
|
58
|
+
a.addEventListener('change', () => { typed.hidden = a.value !== OTHER; if (!typed.hidden) typed.focus(); });
|
|
59
|
+
// One-line intake draft (planning-flows run 4). It only suggests: the create below is unchanged and still needs a PM-chosen anchor.
|
|
60
|
+
const line = el('input', { type: 'text', id: 'c3nit', 'aria-label': 'Describe it in one line', 'data-in-new-line': '', placeholder: 'Start something new: one line is enough (e.g. Cut checkout drop-off, KR2)', autocomplete: 'off' });
|
|
61
|
+
const draftBtn = el('button', { type: 'button', class: 'ib pri', title: 'Draft: an agent runs the intake', 'aria-label': 'Draft: an agent runs the intake', 'data-in-draft-go': '' }, c3ic('spark'));
|
|
62
|
+
const draftOut = el('div', { 'data-in-draft': '' });
|
|
63
|
+
draftOut.hidden = true;
|
|
64
|
+
const startNote = el('div', { class: 'fold', 'data-in-start-stage': '' }, c3ic('init', 'sm'), 'Starts in stage: Discover.');
|
|
65
|
+
let refused = false;
|
|
66
|
+
let ack = false;
|
|
67
|
+
const ackInput = el('input', { type: 'checkbox', 'data-in-ack': '' });
|
|
68
|
+
const ackBox = el('label', { class: 'chips2' }, ackInput, el('span', { style: 'font-size:var(--fs-3)' }, 'I have read the prior rejection and still want to create this'));
|
|
69
|
+
ackBox.hidden = true;
|
|
70
|
+
ackInput.addEventListener('change', () => { ack = ackInput.checked; gate(); if (ack) runDraft(true); });
|
|
71
|
+
const gate = () => { add.disabled = busy || (refused && !ack); };
|
|
72
|
+
/* the intake as the mockup shows it (console3.html c3niCard, line 4787): four steps tick while the draft is made, then "The agent suggests" with the lane, the
|
|
73
|
+
* anchor and any prior rejection as a case file, options, and a slide to create. The draft is the real one (POST /api/intake/draft); the steps pace it. */
|
|
74
|
+
const STEPS = ['Checking what was rejected before', 'Running the sharpness test', 'Choosing a lane', 'Finding an anchor'];
|
|
75
|
+
let last = null; // the last draft, so a change to the anchor or the lane redraws the case
|
|
76
|
+
const anchorNow = () => (a.value === OTHER ? typed.value : a.value).trim();
|
|
77
|
+
const stepsCard = (text, n) => el('section', { class: 'card sect', id: 'c3ni', 'data-in-draft-run': '' }, el('div', { class: 'sh2' }, el('h2', null, c3ic('spark', 'sm'), text)),
|
|
78
|
+
el('ul', { class: 'chgl' }, 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)))));
|
|
79
|
+
const suggest = (d, acknowledged) => {
|
|
80
|
+
last = { d, acknowledged };
|
|
81
|
+
const pr = d.prior_rejections || []; const anc = anchorNow() || d.anchor_suggestion || '';
|
|
82
|
+
const blocked = !anchorNow() ? 'Pick an anchor below first: unanchored work is not created.' : refused && !ack ? 'A prior rejection is on record. Tick the acknowledgement first.' : '';
|
|
83
|
+
const lane = l.value || d.lane || 'feature'; const conf = typeof d.confidence === 'number' ? Math.round(d.confidence * 100) : 0;
|
|
84
|
+
const model = { id: 'ni:new', sit: 'The line reads as a ' + (d.lane || 'feature') + '. Nothing is created until you slide, and the anchor is yours to choose.', conf: conf || 60, cta: 'Slide to create', outboxReason: blocked,
|
|
85
|
+
chg: [['init', 'Lane guess: ' + (d.lane || 'none') + ' (confidence ' + (typeof d.confidence === 'number' ? d.confidence : 'unknown') + ')', 'Lane'], [d.anchor_suggestion ? 'target' : 'alert', d.anchor_suggestion ? 'Anchor suggestion: ' + d.anchor_suggestion : 'no anchor yet', 'Anchor', d.anchor_suggestion ? 'ok' : 'bad'],
|
|
86
|
+
[pr.length ? 'alert' : 'check', pr.length ? 'Prior rejections: ' + pr.map((r) => r.title || r.path || String(r)).join('; ') : 'Prior rejections: none', 'prior rejection', pr.length ? 'warn' : 'ok']],
|
|
87
|
+
opts: [{ id: 'mk', l: 'Create it as a ' + lane + ' initiative', rec: 1, outward: !!blocked, fx: [['check', 'Starts in Later; only your press moves it into Now', 'ok'], [anc ? 'target' : 'alert', anc ? 'Anchored to ' + anc : 'No anchor yet', anc ? 'ok' : 'warn']] },
|
|
88
|
+
...(lane !== 'discovery' && (!conf || conf < 70) ? [{ id: 'ds', l: 'Start with discovery first', outward: !!blocked, fx: [['clock', 'A spike brief with a stop rule, no score', '']] }] : []), { id: 'no', l: 'Not now', fx: [] }],
|
|
89
|
+
unsure: conf && conf < 70 ? 'A keyword match is not a judgment.' : '' };
|
|
90
|
+
const c = renderCase(model, { onConfirm: async ({ option }) => {
|
|
91
|
+
if (option.id === 'no') { draftOut.hidden = true; return; }
|
|
92
|
+
if (option.id === 'ds' && l.tagName === 'SELECT') l.value = [...l.options].some((o) => o.value === 'discovery') ? 'discovery' : l.value;
|
|
93
|
+
add.click();
|
|
94
|
+
} });
|
|
95
|
+
['lane', 'anchor', 'prior'].forEach((k, i) => { const li = c.querySelectorAll('.chgl li')[i]; if (li) li.setAttribute('data-in-draft-' + k, ''); });
|
|
96
|
+
const kids = [];
|
|
97
|
+
if (d.anchor_suggestion) kids.push(el('div', { class: 'chips2' }, el('button', { type: 'button', class: 'chip2 lk', 'data-in-use-anchor': '', onClick: () => {
|
|
98
|
+
if (anchorList.includes(d.anchor_suggestion)) a.value = d.anchor_suggestion;
|
|
99
|
+
else { a.value = OTHER; typed.hidden = false; typed.value = d.anchor_suggestion; }
|
|
100
|
+
suggest(d, acknowledged);
|
|
101
|
+
} }, c3ic('target', 'sm'), 'Use ' + d.anchor_suggestion)));
|
|
102
|
+
kids.push(c);
|
|
103
|
+
if (d.refused) kids.push(el('div', { class: 'fold w', role: 'alert', 'data-in-draft-refused': '' }, c3ic('alert', 'sm'), d.refused_reason || 'Refused: a prior rejection is on record.'));
|
|
104
|
+
draftOut.replaceChildren(el('section', { class: 'card sect', id: 'c3ni' }, el('div', { class: 'sh2' }, el('h2', null, c3ic('spark', 'sm'), 'The agent suggests'), el('span', { class: 'r' }, c3ib('Start over', 'x', { 'data-in-draft-reset': '', onClick: () => { draftOut.hidden = true; last = null; line.value = ''; line.focus(); } }, 'sm'))), ...kids));
|
|
105
|
+
};
|
|
106
|
+
async function runDraft(acknowledged) {
|
|
107
|
+
const text = line.value.trim();
|
|
108
|
+
if (!text) return;
|
|
109
|
+
draftOut.hidden = false;
|
|
110
|
+
let n = 0; if (!acknowledged) draftOut.replaceChildren(stepsCard(text, n));
|
|
111
|
+
const pace = acknowledged ? null : setInterval(() => { if (n < STEPS.length - 1) { n++; draftOut.replaceChildren(stepsCard(text, n)); } }, 260);
|
|
112
|
+
const started = Date.now();
|
|
113
|
+
const out = await pmPost('/api/intake/draft', { line: text, ...(acknowledged ? { acknowledged: true } : {}) });
|
|
114
|
+
if (!acknowledged) { const wait = Math.max(0, 260 * (STEPS.length - 1) + 160 - (Date.now() - started)); if (wait) await new Promise((r) => setTimeout(r, wait)); }
|
|
115
|
+
if (pace) clearInterval(pace);
|
|
116
|
+
if (!out || !out.ok || !out.draft) { draftOut.hidden = false; draftOut.replaceChildren(el('div', { class: 'fold w' }, c3ic('alert', 'sm'), (out && out.error) || 'The draft did not answer.')); return; }
|
|
117
|
+
const d = out.draft;
|
|
118
|
+
if (!t.value.trim() && d.title) t.value = d.title;
|
|
119
|
+
if (!prefill && lanes && lanes.includes(d.lane)) l.value = d.lane;
|
|
120
|
+
refused = !!d.refused && !acknowledged;
|
|
121
|
+
if (!acknowledged) { ack = false; ackInput.checked = false; }
|
|
122
|
+
suggest(d, acknowledged);
|
|
123
|
+
ackBox.hidden = !d.refused;
|
|
124
|
+
gate();
|
|
125
|
+
}
|
|
126
|
+
const redraw = () => { if (last && !draftOut.hidden) suggest(last.d, last.acknowledged); };
|
|
127
|
+
a.addEventListener('change', redraw); typed.addEventListener('input', redraw); l.addEventListener('change', redraw);
|
|
128
|
+
line.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); runDraft(false); } });
|
|
129
|
+
draftBtn.addEventListener('click', () => runDraft(false));
|
|
130
|
+
const err = el('div', { class: 'fold w', role: 'alert', 'data-in-error': '' });
|
|
41
131
|
err.hidden = true;
|
|
42
|
-
const lands = el('
|
|
132
|
+
const lands = el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, COPY.newForm.lands);
|
|
43
133
|
const fail = (text) => { err.textContent = text; err.hidden = false; };
|
|
44
134
|
|
|
45
135
|
let busy = false;
|
|
46
136
|
const add = button({
|
|
47
137
|
label: COPY.newForm.add, variant: 'primary', attrs: { 'data-in-new-add': '' },
|
|
48
138
|
onClick: async () => {
|
|
49
|
-
if (busy) return;
|
|
139
|
+
if (busy || (refused && !ack)) return;
|
|
50
140
|
err.hidden = true;
|
|
51
|
-
const title = t.
|
|
52
|
-
const pick = a.
|
|
53
|
-
const anchor = (pick === OTHER ? typed.
|
|
54
|
-
if (!title) { fail(COPY.newForm.noTitle); t.
|
|
141
|
+
const title = t.value.trim();
|
|
142
|
+
const pick = a.value;
|
|
143
|
+
const anchor = (pick === OTHER ? typed.value : pick).trim();
|
|
144
|
+
if (!title) { fail(COPY.newForm.noTitle); t.focus(); return; }
|
|
55
145
|
if (!anchor) { fail(COPY.newForm.noAnchor); return; }
|
|
56
|
-
busy = true;
|
|
57
|
-
const out = await createInitiative(ws, { title, ...(l.
|
|
58
|
-
busy = false;
|
|
146
|
+
busy = true; gate();
|
|
147
|
+
const out = await createInitiative(ws, { title, ...(l.value.trim() ? { lane: l.value.trim() } : {}), anchor, ...(prefill && prefill.from ? { from: prefill.from } : {}) });
|
|
148
|
+
busy = false; gate();
|
|
59
149
|
if (out && out.ok) {
|
|
60
150
|
const id = (out.initiative && out.initiative.id) || out.id || (out.ids && out.ids[0]) || null;
|
|
61
151
|
s.close();
|
|
@@ -65,10 +155,15 @@ export function openNewInitiative({ repo, ws, anchors, prefill, onAdded }) {
|
|
|
65
155
|
} else fail(COPY.refused((out && out.error) || 'The server did not answer.'));
|
|
66
156
|
},
|
|
67
157
|
});
|
|
158
|
+
const body = el('div', { class: 'c3 c3e' },
|
|
159
|
+
el('div', { class: 'c3add' }, el('div', { class: 'form' }, line, draftBtn)),
|
|
160
|
+
draftOut, ackBox,
|
|
161
|
+
el('div', { class: 'c3jf' }, row(COPY.newForm.titleLabel, t), row(COPY.newForm.laneLabel, l), row(COPY.newForm.anchorLabel, a), typed),
|
|
162
|
+
startNote, lands, err);
|
|
68
163
|
const s = sheet({
|
|
69
|
-
mode: 'auto', title: COPY.newForm.title, body: [
|
|
164
|
+
mode: 'auto', title: COPY.newForm.title, body: [body], attrs: { 'data-in-sheet': 'new' },
|
|
70
165
|
actions: [button({ label: COPY.cancel, variant: 'outline', onClick: () => s.close(), attrs: { 'data-in-cancel': '' } }), add],
|
|
71
166
|
});
|
|
72
|
-
t.
|
|
167
|
+
if (prefill && prefill.title && !prefill.lane && !prefill.from) { line.value = prefill.title; runDraft(false); } else t.focus();
|
|
73
168
|
return s;
|
|
74
169
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* pm/outbox-panel.js — the outbox queue view (console3 wave B3), on the console3 mockup's own markup and CSS (R4b).
|
|
2
|
+
*
|
|
3
|
+
* mountOutbox(host, ctx) draws the queue from GET /api/outbox; ctx is optional: { get, post } (defaults to http.js)
|
|
4
|
+
* disposeOutbox(host) removes it and its slides
|
|
5
|
+
*
|
|
6
|
+
* The markup is the mockup's message queue (console3.html lines 4283 to 4284, r2queue 4338): `section.card.sect > .sh2` with the
|
|
7
|
+
* count in `.bdg.g`, `ul.list > li.nrow` with `.ki`, `.nb > .tt + .chips + .xp > .xi`, and the case file `.case > .sit + .qd
|
|
8
|
+
* + .chgl + .slt`. Each entry shows its channel, recipient, an editable body (editing voids the hash, and the panel says so),
|
|
9
|
+
* the claim check result as `.chgl` rows, a slide bound to the CURRENT hash and, after a slide, the plain result. No channel
|
|
10
|
+
* is connected in this wave, so a slide ends in "no channel connected" and the entry stays queued. Nothing is sent.
|
|
11
|
+
* Not lifted: the mockup's swipe on a message row (right = send, left = hold): a send here is only the slide; the mockup's
|
|
12
|
+
* three states (draft, held, sent) and its "external, you approve" chip, which the entry does not carry. */
|
|
13
|
+
import { el } from './dom.js';
|
|
14
|
+
import { pmGet, pmPost } from './http.js';
|
|
15
|
+
import { mountSlide, disposeSlide } from './slide-confirm.js';
|
|
16
|
+
import { ensureC3Css, c3ic } from './pm-work-view.js';
|
|
17
|
+
|
|
18
|
+
const live = new WeakMap(); // host → { slideHosts: [], gone }
|
|
19
|
+
|
|
20
|
+
export function disposeOutbox(host) {
|
|
21
|
+
const s = live.get(host);
|
|
22
|
+
if (!s) return;
|
|
23
|
+
s.gone = true;
|
|
24
|
+
s.slideHosts.forEach(disposeSlide);
|
|
25
|
+
live.delete(host);
|
|
26
|
+
host.textContent = '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function mountOutbox(host, ctx = {}) {
|
|
30
|
+
disposeOutbox(host);
|
|
31
|
+
ensureC3Css();
|
|
32
|
+
const get = ctx.get || pmGet, post = ctx.post || pmPost;
|
|
33
|
+
const st = { slideHosts: [], gone: false };
|
|
34
|
+
live.set(host, st);
|
|
35
|
+
const results = {}; // id → plain result text, kept across redraws
|
|
36
|
+
const voided = {}; // id → true after an edit, until the next press
|
|
37
|
+
const open = {}; // id → false when the PM folded the row
|
|
38
|
+
|
|
39
|
+
async function draw() {
|
|
40
|
+
const r = await get('/api/outbox');
|
|
41
|
+
if (st.gone) return;
|
|
42
|
+
st.slideHosts.forEach(disposeSlide);
|
|
43
|
+
st.slideHosts = [];
|
|
44
|
+
const root = el('div', { class: 'c3 c3e', 'data-outbox': '' });
|
|
45
|
+
if (!r || r.ok === false) {
|
|
46
|
+
root.append(el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), (r && r.error) || 'Could not read the outbox.'));
|
|
47
|
+
} else {
|
|
48
|
+
const entries = r.entries || [];
|
|
49
|
+
root.append(el('section', { class: 'card sect' },
|
|
50
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('out', 'sm'), 'Outbox ', el('span', { class: 'bdg g' }, String(entries.length))),
|
|
51
|
+
el('span', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Nothing here is sent. No channel is connected.')),
|
|
52
|
+
el('ul', { class: 'list' }, entries.length ? entries.map((e) => row(e)) : el('li', { class: 'fold' }, c3ic('check', 'sm'), 'The outbox is empty.'))));
|
|
53
|
+
}
|
|
54
|
+
host.replaceChildren(root);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function row(e) {
|
|
58
|
+
const op = open[e.id] !== false;
|
|
59
|
+
const li = el('li', { class: 'nrow', 'data-row': '', 'data-outbox-entry': e.id, tabindex: '0', 'aria-expanded': String(op) });
|
|
60
|
+
const xp = el('div', { class: 'xp' + (op ? ' open' : '') });
|
|
61
|
+
const toggle = () => { open[e.id] = !(open[e.id] !== false); li.setAttribute('aria-expanded', String(open[e.id])); xp.classList.toggle('open', open[e.id]); };
|
|
62
|
+
li.addEventListener('click', (ev) => { if (ev.target.closest('.xi')) return; toggle(); });
|
|
63
|
+
li.addEventListener('keydown', (ev) => { if (ev.target === li && (ev.key === 'Enter' || ev.key === ' ')) { ev.preventDefault(); toggle(); } });
|
|
64
|
+
const claimTone = e.claims.ok ? 'ok' : 'bad';
|
|
65
|
+
xp.append(el('div', { class: 'xi' }, theCase(e)));
|
|
66
|
+
li.append(
|
|
67
|
+
el('span', { class: 'ki' }, c3ic('out')),
|
|
68
|
+
el('div', { class: 'nb' },
|
|
69
|
+
el('span', { class: 'tt' }, e.recipient || e.channel),
|
|
70
|
+
el('div', { class: 'chips' },
|
|
71
|
+
el('span', { class: 'chip2' }, e.state),
|
|
72
|
+
el('span', { class: 'chip2' }, c3ic('user', 'sm'), e.recipient || 'no recipient'),
|
|
73
|
+
el('span', { class: 'tag' }, e.channel),
|
|
74
|
+
el('span', { class: 'tag' }, e.origin),
|
|
75
|
+
el('span', { class: 'st ' + claimTone }, c3ic(e.claims.ok ? 'check' : 'alert', 'sm'), e.claims.ok ? 'claims labelled' : 'blocked')),
|
|
76
|
+
xp));
|
|
77
|
+
return li;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function theCase(e) {
|
|
81
|
+
const ta = el('textarea', { 'aria-label': 'Body of the entry for ' + (e.recipient || e.channel), style: 'width:100%;min-height:84px;box-sizing:border-box' });
|
|
82
|
+
ta.value = e.body;
|
|
83
|
+
const save = el('button', { type: 'button', class: 'pill', disabled: true }, c3ic('check', 'sm'), 'Save edit');
|
|
84
|
+
const hashLine = el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, 'hash ' + e.hash.slice(0, 10));
|
|
85
|
+
const slideHost = el('div');
|
|
86
|
+
ta.addEventListener('input', () => {
|
|
87
|
+
const changed = ta.value !== e.body;
|
|
88
|
+
save.disabled = !changed;
|
|
89
|
+
hashLine.textContent = changed ? 'Editing voids hash ' + e.hash.slice(0, 10) + '. Save, then slide on the new hash.' : 'hash ' + e.hash.slice(0, 10);
|
|
90
|
+
slideHost.style.display = changed ? 'none' : '';
|
|
91
|
+
});
|
|
92
|
+
save.addEventListener('click', async () => {
|
|
93
|
+
const r = await post('/api/outbox/edit', { id: e.id, body: ta.value });
|
|
94
|
+
if (r && r.ok) { voided[e.id] = true; delete results[e.id]; await draw(); } else hashLine.textContent = (r && r.error) || 'The edit failed.';
|
|
95
|
+
});
|
|
96
|
+
const chg = el('ul', { class: 'chgl' });
|
|
97
|
+
if (!e.claims.ok) {
|
|
98
|
+
chg.append(el('li', { class: 'bad' }, c3ic('alert', 'sm'), el('span', null, 'Blocked: these claim lines have no label ([fact], [estimate], [assumption], [reported] or a ticket key).'), el('span', { class: 'tag' }, 'claims check')));
|
|
99
|
+
for (const l of e.claims.refused) chg.append(el('li', { class: 'bad' }, c3ic('x', 'sm'), el('span', { class: 'mono' }, l)));
|
|
100
|
+
} else chg.append(el('li', { class: 'ok' }, c3ic('check', 'sm'), el('span', null, 'Claim check passed.'), el('span', { class: 'tag' }, 'claims check')));
|
|
101
|
+
const c = el('div', { class: 'case', 'data-case': e.id, style: '--tc:var(--acc)' },
|
|
102
|
+
el('p', { class: 'sit' }, results[e.id] || e.last_result || 'Nothing has been sent. The slide below sends exactly this text, bound to its hash.'),
|
|
103
|
+
ta,
|
|
104
|
+
el('div', { class: 'chips2' }, save, hashLine),
|
|
105
|
+
voided[e.id] ? el('div', { class: 'fold w' }, c3ic('alert', 'sm'), 'The old hash is void. The slide below is bound to the new content.') : null,
|
|
106
|
+
chg,
|
|
107
|
+
slideHost);
|
|
108
|
+
if (e.claims.ok) {
|
|
109
|
+
st.slideHosts.push(slideHost);
|
|
110
|
+
mountSlide(slideHost, {
|
|
111
|
+
label: 'Slide to confirm', hash: e.hash,
|
|
112
|
+
onConfirm: async (hash) => {
|
|
113
|
+
const r = await post('/api/outbox/confirm', { id: e.id, hash });
|
|
114
|
+
const text = !r ? 'No answer.' : r.ok ? 'Confirmed.' : (r.reason || r.error || 'Refused.');
|
|
115
|
+
results[e.id] = text;
|
|
116
|
+
delete voided[e.id];
|
|
117
|
+
await draw();
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
} else slideHost.appendChild(el('div', { class: 'cft' }, el('span', null, c3ic('lock', 'sm'), 'The slide is held until every claim line has a label.')));
|
|
121
|
+
return c;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await draw();
|
|
125
|
+
}
|