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.
Files changed (127) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/scripts/nine-stages.js +81 -0
  6. package/payload/scripts/workflow-state.js +2 -0
  7. package/payload/state/discoveries.md +1 -1
  8. package/payload/state/log.md +1 -1
  9. package/workspace/c3-one-home-test.js +131 -0
  10. package/workspace/fixtures/initiatives-api/list.json +171 -3
  11. package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
  12. package/workspace/fixtures/initiatives-api/sheet.json +57 -1
  13. package/workspace/fixtures/one-home-baseline.json +83 -0
  14. package/workspace/guide/layout.md +2 -0
  15. package/workspace/guide/notes.md +1 -0
  16. package/workspace/lib/engine/nine-stages.js +81 -0
  17. package/workspace/lib/engine/workflow-state.js +2 -0
  18. package/workspace/lib/facts-async.js +51 -0
  19. package/workspace/lib/item-steps.js +52 -0
  20. package/workspace/lib/jira-status.js +8 -3
  21. package/workspace/lib/jobs-to-be-done.js +52 -0
  22. package/workspace/lib/outbox-store.js +87 -0
  23. package/workspace/lib/pm-items.js +115 -0
  24. package/workspace/lib/route-modules.js +21 -0
  25. package/workspace/lib/routes-briefing.js +100 -0
  26. package/workspace/lib/routes-c3-init.js +53 -0
  27. package/workspace/lib/routes-c3-life.js +119 -0
  28. package/workspace/lib/routes-c3-strategy.js +169 -0
  29. package/workspace/lib/routes-c3-timeline.js +191 -0
  30. package/workspace/lib/routes-calendar-import.js +60 -0
  31. package/workspace/lib/routes-calendar-views.js +61 -0
  32. package/workspace/lib/routes-doc-health.js +79 -0
  33. package/workspace/lib/routes-doc-sheet.js +169 -0
  34. package/workspace/lib/routes-forecast.js +85 -0
  35. package/workspace/lib/routes-hooks.js +121 -0
  36. package/workspace/lib/routes-initiatives.js +3 -0
  37. package/workspace/lib/routes-intake.js +24 -0
  38. package/workspace/lib/routes-outbox.js +97 -0
  39. package/workspace/lib/routes-pm-items.js +216 -0
  40. package/workspace/lib/routes-records.js +61 -0
  41. package/workspace/lib/routes-releases.js +129 -0
  42. package/workspace/lib/routes-report-facts.js +84 -0
  43. package/workspace/lib/routes-report-families.js +32 -0
  44. package/workspace/lib/routes-roadmap-ops.js +141 -0
  45. package/workspace/lib/routes-runs.js +119 -0
  46. package/workspace/lib/routes-signals.js +50 -0
  47. package/workspace/lib/run-answers.js +69 -0
  48. package/workspace/package-lock.json +2 -2
  49. package/workspace/package.json +1 -1
  50. package/workspace/public/c3.html +26 -0
  51. package/workspace/public/pm/briefing-deck.js +415 -0
  52. package/workspace/public/pm/briefing-model.js +176 -0
  53. package/workspace/public/pm/c3-breakdown.js +240 -0
  54. package/workspace/public/pm/c3-brief.js +278 -0
  55. package/workspace/public/pm/c3-cal-strategy.js +528 -0
  56. package/workspace/public/pm/c3-case.js +173 -0
  57. package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
  58. package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
  59. package/workspace/public/pm/c3-demo-cal2.js +66 -0
  60. package/workspace/public/pm/c3-demo-cal3.js +173 -0
  61. package/workspace/public/pm/c3-demo-data.js +560 -0
  62. package/workspace/public/pm/c3-demo-init.js +373 -0
  63. package/workspace/public/pm/c3-demo-life.js +77 -0
  64. package/workspace/public/pm/c3-demo-misc2.js +46 -0
  65. package/workspace/public/pm/c3-demo-plan2.js +30 -0
  66. package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
  67. package/workspace/public/pm/c3-demo-rep2.js +92 -0
  68. package/workspace/public/pm/c3-demo-term.js +153 -0
  69. package/workspace/public/pm/c3-demo-time.js +49 -0
  70. package/workspace/public/pm/c3-demo-work-ix.js +80 -0
  71. package/workspace/public/pm/c3-demo-work2.js +13 -0
  72. package/workspace/public/pm/c3-demo.js +113 -0
  73. package/workspace/public/pm/c3-doc-sheet.js +314 -0
  74. package/workspace/public/pm/c3-dock.js +180 -0
  75. package/workspace/public/pm/c3-docs.js +290 -0
  76. package/workspace/public/pm/c3-expand.js +96 -0
  77. package/workspace/public/pm/c3-init-extras.js +170 -0
  78. package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
  79. package/workspace/public/pm/c3-misc-panels.js +89 -0
  80. package/workspace/public/pm/c3-plan-panels.js +163 -0
  81. package/workspace/public/pm/c3-plan.js +192 -0
  82. package/workspace/public/pm/c3-roadmap.js +249 -0
  83. package/workspace/public/pm/c3-sheet.js +107 -0
  84. package/workspace/public/pm/c3-shell.js +637 -0
  85. package/workspace/public/pm/c3-signals.js +196 -0
  86. package/workspace/public/pm/c3-strategy-panels.js +45 -0
  87. package/workspace/public/pm/c3-swipe.js +138 -0
  88. package/workspace/public/pm/c3-terminal-data.js +26 -0
  89. package/workspace/public/pm/c3-terminal-edge.js +91 -0
  90. package/workspace/public/pm/c3-terminal-icons.js +41 -0
  91. package/workspace/public/pm/c3-terminal-results.js +141 -0
  92. package/workspace/public/pm/c3-terminal.js +52 -0
  93. package/workspace/public/pm/c3-tickets.js +163 -0
  94. package/workspace/public/pm/c3-timeline.js +201 -0
  95. package/workspace/public/pm/c3-toast.js +16 -0
  96. package/workspace/public/pm/calendar-views.js +1015 -0
  97. package/workspace/public/pm/console3-lift.md +220 -0
  98. package/workspace/public/pm/console3.css +1480 -0
  99. package/workspace/public/pm/dest-initiatives.js +16 -3
  100. package/workspace/public/pm/dest-mywork.js +8 -1
  101. package/workspace/public/pm/dest-notes.js +25 -3
  102. package/workspace/public/pm/dest-reports.js +47 -1
  103. package/workspace/public/pm/dest-today.js +55 -1
  104. package/workspace/public/pm/doc-health-panel.js +110 -0
  105. package/workspace/public/pm/doc-rail.js +9 -3
  106. package/workspace/public/pm/lifecycle-lens-model.js +228 -0
  107. package/workspace/public/pm/lifecycle-lens.js +261 -0
  108. package/workspace/public/pm/new-initiative-sheet.js +116 -21
  109. package/workspace/public/pm/outbox-panel.js +125 -0
  110. package/workspace/public/pm/pm-work-view.js +919 -0
  111. package/workspace/public/pm/releases-view.js +248 -0
  112. package/workspace/public/pm/report-families-view.js +503 -0
  113. package/workspace/public/pm/run-card.js +244 -0
  114. package/workspace/public/pm/sessions-panel.js +149 -0
  115. package/workspace/public/pm/shell-bell.js +162 -0
  116. package/workspace/public/pm/shell-model.js +83 -0
  117. package/workspace/public/pm/shell-phone.js +124 -0
  118. package/workspace/public/pm/shell-readaloud.js +53 -0
  119. package/workspace/public/pm/shell-recording.js +110 -0
  120. package/workspace/public/pm/shell-ticker.js +90 -0
  121. package/workspace/public/pm/slide-confirm.js +163 -0
  122. package/workspace/public/pm/timeline-forecast-view.js +164 -0
  123. package/workspace/public/pm/today-day.js +8 -2
  124. package/workspace/public/pm/tokens.css +1 -0
  125. package/workspace/server.js +5 -2
  126. package/workspace/test-files.json +8 -0
  127. package/workspace/test-lists/browser-suites-green.json +5 -0
@@ -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, field, sheet, toast, ensureComponentsCss } from './primitives.js';
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 = field({ label: COPY.newForm.titleLabel, kind: 'title', value: prefill && prefill.title, attrs: { 'data-in-new-title': '' } });
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
- const l = lanes
33
- ? field({ label: COPY.newForm.laneLabel, kind: 'select', value: prefill && lanes.includes(prefill.lane) ? prefill.lane : lanes[0], options: lanes.map((x) => ({ value: x, label: x })), attrs: { 'data-in-new-lane': '' } })
34
- : field({ label: COPY.newForm.laneLabel, kind: 'text', value: prefill && prefill.lane, placeholder: COPY.newForm.laneHint, attrs: { 'data-in-new-lane': '' } });
35
- const opts = [{ value: '', label: COPY.newForm.anchorPick }, ...anchorList.map((a) => ({ value: a, label: a })), { value: OTHER, label: COPY.newForm.anchorOther }];
36
- const a = field({ label: COPY.newForm.anchorLabel, kind: 'select', value: '', options: opts, attrs: { 'data-in-new-anchor': '' } });
37
- const typed = field({ label: COPY.newForm.anchorLabel, kind: 'text', placeholder: COPY.newForm.anchorHint, attrs: { 'data-in-new-anchor-text': '' } });
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.control.addEventListener('change', () => { typed.hidden = a.control.value !== OTHER; if (!typed.hidden) typed.control.focus(); });
40
- const err = el('p', { class: 'in-error', role: 'alert', 'data-in-error': '' });
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('p', { class: 'in-note' }, COPY.newForm.lands);
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.control.value.trim();
52
- const pick = a.control.value;
53
- const anchor = (pick === OTHER ? typed.control.value : pick).trim();
54
- if (!title) { fail(COPY.newForm.noTitle); t.control.focus(); return; }
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; add.disabled = true;
57
- const out = await createInitiative(ws, { title, ...(l.control.value.trim() ? { lane: l.control.value.trim() } : {}), anchor, ...(prefill && prefill.from ? { from: prefill.from } : {}) });
58
- busy = false; add.disabled = 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: [t, l, a, typed, lands, err], attrs: { 'data-in-sheet': 'new' },
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.control.focus();
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
+ }