create-pmos 0.124.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 (125) 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/lib/engine/nine-stages.js +81 -0
  16. package/workspace/lib/engine/workflow-state.js +2 -0
  17. package/workspace/lib/facts-async.js +51 -0
  18. package/workspace/lib/item-steps.js +52 -0
  19. package/workspace/lib/jira-status.js +8 -3
  20. package/workspace/lib/jobs-to-be-done.js +52 -0
  21. package/workspace/lib/outbox-store.js +87 -0
  22. package/workspace/lib/pm-items.js +115 -0
  23. package/workspace/lib/route-modules.js +21 -0
  24. package/workspace/lib/routes-briefing.js +100 -0
  25. package/workspace/lib/routes-c3-init.js +53 -0
  26. package/workspace/lib/routes-c3-life.js +119 -0
  27. package/workspace/lib/routes-c3-strategy.js +169 -0
  28. package/workspace/lib/routes-c3-timeline.js +191 -0
  29. package/workspace/lib/routes-calendar-import.js +60 -0
  30. package/workspace/lib/routes-calendar-views.js +61 -0
  31. package/workspace/lib/routes-doc-health.js +79 -0
  32. package/workspace/lib/routes-doc-sheet.js +169 -0
  33. package/workspace/lib/routes-forecast.js +85 -0
  34. package/workspace/lib/routes-hooks.js +121 -0
  35. package/workspace/lib/routes-initiatives.js +3 -0
  36. package/workspace/lib/routes-intake.js +24 -0
  37. package/workspace/lib/routes-outbox.js +97 -0
  38. package/workspace/lib/routes-pm-items.js +216 -0
  39. package/workspace/lib/routes-records.js +61 -0
  40. package/workspace/lib/routes-releases.js +129 -0
  41. package/workspace/lib/routes-report-facts.js +84 -0
  42. package/workspace/lib/routes-report-families.js +32 -0
  43. package/workspace/lib/routes-roadmap-ops.js +141 -0
  44. package/workspace/lib/routes-runs.js +119 -0
  45. package/workspace/lib/routes-signals.js +50 -0
  46. package/workspace/lib/run-answers.js +69 -0
  47. package/workspace/package-lock.json +2 -2
  48. package/workspace/package.json +1 -1
  49. package/workspace/public/c3.html +26 -0
  50. package/workspace/public/pm/briefing-deck.js +415 -0
  51. package/workspace/public/pm/briefing-model.js +176 -0
  52. package/workspace/public/pm/c3-breakdown.js +240 -0
  53. package/workspace/public/pm/c3-brief.js +278 -0
  54. package/workspace/public/pm/c3-cal-strategy.js +528 -0
  55. package/workspace/public/pm/c3-case.js +173 -0
  56. package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
  57. package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
  58. package/workspace/public/pm/c3-demo-cal2.js +66 -0
  59. package/workspace/public/pm/c3-demo-cal3.js +173 -0
  60. package/workspace/public/pm/c3-demo-data.js +560 -0
  61. package/workspace/public/pm/c3-demo-init.js +373 -0
  62. package/workspace/public/pm/c3-demo-life.js +77 -0
  63. package/workspace/public/pm/c3-demo-misc2.js +46 -0
  64. package/workspace/public/pm/c3-demo-plan2.js +30 -0
  65. package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
  66. package/workspace/public/pm/c3-demo-rep2.js +92 -0
  67. package/workspace/public/pm/c3-demo-term.js +153 -0
  68. package/workspace/public/pm/c3-demo-time.js +49 -0
  69. package/workspace/public/pm/c3-demo-work-ix.js +80 -0
  70. package/workspace/public/pm/c3-demo-work2.js +13 -0
  71. package/workspace/public/pm/c3-demo.js +113 -0
  72. package/workspace/public/pm/c3-doc-sheet.js +314 -0
  73. package/workspace/public/pm/c3-dock.js +180 -0
  74. package/workspace/public/pm/c3-docs.js +290 -0
  75. package/workspace/public/pm/c3-expand.js +96 -0
  76. package/workspace/public/pm/c3-init-extras.js +170 -0
  77. package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
  78. package/workspace/public/pm/c3-misc-panels.js +89 -0
  79. package/workspace/public/pm/c3-plan-panels.js +163 -0
  80. package/workspace/public/pm/c3-plan.js +192 -0
  81. package/workspace/public/pm/c3-roadmap.js +249 -0
  82. package/workspace/public/pm/c3-sheet.js +107 -0
  83. package/workspace/public/pm/c3-shell.js +637 -0
  84. package/workspace/public/pm/c3-signals.js +196 -0
  85. package/workspace/public/pm/c3-strategy-panels.js +45 -0
  86. package/workspace/public/pm/c3-swipe.js +138 -0
  87. package/workspace/public/pm/c3-terminal-data.js +26 -0
  88. package/workspace/public/pm/c3-terminal-edge.js +91 -0
  89. package/workspace/public/pm/c3-terminal-icons.js +41 -0
  90. package/workspace/public/pm/c3-terminal-results.js +141 -0
  91. package/workspace/public/pm/c3-terminal.js +52 -0
  92. package/workspace/public/pm/c3-tickets.js +163 -0
  93. package/workspace/public/pm/c3-timeline.js +201 -0
  94. package/workspace/public/pm/c3-toast.js +16 -0
  95. package/workspace/public/pm/calendar-views.js +1015 -0
  96. package/workspace/public/pm/console3-lift.md +220 -0
  97. package/workspace/public/pm/console3.css +1480 -0
  98. package/workspace/public/pm/dest-initiatives.js +16 -3
  99. package/workspace/public/pm/dest-mywork.js +8 -1
  100. package/workspace/public/pm/dest-reports.js +47 -1
  101. package/workspace/public/pm/dest-today.js +55 -1
  102. package/workspace/public/pm/doc-health-panel.js +110 -0
  103. package/workspace/public/pm/doc-rail.js +9 -3
  104. package/workspace/public/pm/lifecycle-lens-model.js +228 -0
  105. package/workspace/public/pm/lifecycle-lens.js +261 -0
  106. package/workspace/public/pm/new-initiative-sheet.js +116 -21
  107. package/workspace/public/pm/outbox-panel.js +125 -0
  108. package/workspace/public/pm/pm-work-view.js +919 -0
  109. package/workspace/public/pm/releases-view.js +248 -0
  110. package/workspace/public/pm/report-families-view.js +503 -0
  111. package/workspace/public/pm/run-card.js +244 -0
  112. package/workspace/public/pm/sessions-panel.js +149 -0
  113. package/workspace/public/pm/shell-bell.js +162 -0
  114. package/workspace/public/pm/shell-model.js +83 -0
  115. package/workspace/public/pm/shell-phone.js +124 -0
  116. package/workspace/public/pm/shell-readaloud.js +53 -0
  117. package/workspace/public/pm/shell-recording.js +110 -0
  118. package/workspace/public/pm/shell-ticker.js +90 -0
  119. package/workspace/public/pm/slide-confirm.js +163 -0
  120. package/workspace/public/pm/timeline-forecast-view.js +164 -0
  121. package/workspace/public/pm/today-day.js +8 -2
  122. package/workspace/public/pm/tokens.css +1 -0
  123. package/workspace/server.js +5 -2
  124. package/workspace/test-files.json +8 -0
  125. package/workspace/test-lists/browser-suites-green.json +5 -0
@@ -0,0 +1,503 @@
1
+ /* pm/report-families-view.js — console3: the four report families, the claims check, the send slide and who-holds-what, behaving as the
2
+ * console3 mockup's Reports tab does (console3.html lines 4205 to 4380: R2F, pjSecs/pdSecs/peSecs, rsRow, r2composer, cmRow, caseFor 'rpt:'/'com:').
3
+ *
4
+ * mountReportFamilies(host, ctx) / disposeReportFamilies(host). One persistent root per host.
5
+ * Real mode: GET /api/report-families once; POST /api/report-families/check (pure); sections come from ctx.reportFacts (the facts route).
6
+ * Demo mode (window.__c3demo, with window.__c3repix from c3-demo-rep-ix.js): the mockup's own reports, sections and messages; nothing is requested.
7
+ *
8
+ * What a press does:
9
+ * - a section row opens IN PLACE (c3-expand): why it is here, the claims with their label and a Traced / Needs your input mark, the sources as
10
+ * jump chips (they click the Initiatives, Calendar, Work or Briefing tab), and "Fix with the agent" (disabled, with the reason).
11
+ * - a swipe right approves a section and a swipe left cuts it (c3-swipe). In DEMO the decision is kept on the page, with an Undo toast. In REAL mode
12
+ * no route stores a section decision, so the swipe is off and the legend and the row say why.
13
+ * - the Send row opens the case file (c3-case) with the slide. DEMO: the slide marks the report Sent and adds it to the Sent card (nothing leaves).
14
+ * REAL: opening the row queues ONE draft through POST /api/outbox/draft (the claim lines of the sections, labelled), the slide is bound to that
15
+ * draft's hash and calls POST /api/outbox/confirm. With no channel connected the answer is "no channel connected" and the page says exactly that.
16
+ * A claim line with no label or ticket key blocks the slide with the reason; customer-facing variants allow only fact and cited labels (server rule).
17
+ * - Communications (demo): the message queue (swipe right sends, left holds, external needs the slide) and the who-holds-what truth table.
18
+ * An agent can only draft. Sending is the PM's slide. Nothing here sends anything. */
19
+ import { el } from './dom.js';
20
+ import { strategyPanel } from './c3-strategy-panels.js';
21
+ import { pmGet, pmPost } from './http.js';
22
+ import { ensureC3Css, c3ic, c3ring, c3Phone } from './pm-work-view.js';
23
+ import { attachExpand } from './c3-expand.js';
24
+ import { attachSwipe } from './c3-swipe.js';
25
+ import { renderCase } from './c3-case.js';
26
+ import { openSheet } from './c3-sheet.js';
27
+ import { toast } from './c3-toast.js';
28
+ import { openDocSheet, splitDoc } from './c3-doc-sheet.js';
29
+ import { forecastChecksCard } from './c3-misc-panels.js';
30
+
31
+ const insts = new WeakMap();
32
+ const FAM_ICON = { project: 'tree', product: 'target', communications: 'out', periodic: 'cal' };
33
+ const FIX_WHY = 'Not available here: the agent starts from a terminal session, not from this page.';
34
+ const NO_STORE = 'Approving or cutting a section is not recorded here yet: no record stores a section decision.';
35
+ const dim = (t) => el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, t);
36
+ const rx = () => (typeof window !== 'undefined' && window.__c3demo && window.__c3repix) || null;
37
+ const cap = (s) => { const t = String(s || '').replace(/_/g, ' '); return t.charAt(0).toUpperCase() + t.slice(1); };
38
+
39
+ function make(host) {
40
+ return { host, root: el('div', { class: 'c3', 'data-report-families': '' }), families: null, error: null, sel: null, variant: null, rep: { project: 'pj1', product: 'pd1' }, check: null, seq: 0,
41
+ dec: {}, flagged: {}, sent: {}, csent: {}, chold: {}, open: {}, hist: null, docText: {}, cleanups: [], draft: null, draftErr: null, draftBusy: false, outMsg: {}, outSent: null };
42
+ }
43
+
44
+ /* ── jump to the source: the page's own tab buttons (the mockup's RSRC) ── */
45
+ const RSRC = { time: ['init', 'forecast'], tix: ['init', 'tickets'], wbs: ['init', 'breakdown'], docs: ['init', 'documents'], sig: ['init', 'signals'], life: ['init', 'lifecycle'], roadmap: ['init', 'roadmap'], strat: ['init', 'lifecycle'], comms: ['rep', 'communications'], brief: ['brief'], cal: ['cal'], work: ['work'] };
46
+ function jump(kind) {
47
+ const t = RSRC[kind];
48
+ if (!t) return;
49
+ const tab = document.querySelector('[data-c3-tab="' + t[0] + '"]');
50
+ if (tab) tab.click();
51
+ if (!t[1]) return;
52
+ let n = 0;
53
+ const go = () => {
54
+ const b = t[0] === 'init' ? document.querySelector('[data-c3-init="' + t[1] + '"]') : document.querySelector('[data-family="' + t[1] + '"]');
55
+ if (b) b.click(); else if (++n < 25) setTimeout(go, 80);
56
+ };
57
+ go();
58
+ }
59
+ /* a real claim's source text → the view that holds it (only the ones that name one) */
60
+ function kindOf(text) {
61
+ const t = String(text || '').toLowerCase();
62
+ if (/jira|ticket/.test(t)) return 'tix'; if (/calendar/.test(t)) return 'cal'; if (/timeline|forecast/.test(t)) return 'time'; if (/analytics|signal/.test(t)) return 'sig'; if (/brief/.test(t)) return 'brief';
63
+ return '';
64
+ }
65
+
66
+ /* ── the model: variants, sections and counts, from the demo fixtures or the real facts ── */
67
+ const famOf = (inst) => inst.families.find((f) => f.id === inst.sel);
68
+ const variantsOf = (inst, fam) => { const R = rx(); return R ? R.pills(fam.id) : fam.variants.map((v) => ({ id: v.id, name: v.name, external: !!v.external })); };
69
+ const repIdOf = (inst, fam) => (fam.id === 'periodic' ? inst.variant : (inst.rep[fam.id] || 'r'));
70
+ const keyOf = (inst, fam) => fam.id + '.' + repIdOf(inst, fam) + '.' + (fam.id === 'periodic' ? 'x' : inst.variant);
71
+ const variantOf = (inst, fam) => variantsOf(inst, fam).find((v) => v.id === inst.variant) || variantsOf(inst, fam)[0] || { id: '', name: '' };
72
+ const repOf = (inst, fam) => { const R = rx(); return R ? ((R.REPORTS[fam.id] || []).find((r) => r.id === repIdOf(inst, fam)) || (R.REPORTS[fam.id] || [])[0] || null) : null; };
73
+
74
+ /* a report is a document (the mockup's r2doc): the sections the sheet saved flow back into the rows (the mockup's r2text) */
75
+ const docPath = (fam, rid, variant) => 'planning/reports/' + fam + '-' + rid + '-' + variant + '.md';
76
+ const pathOf = (inst, fam) => docPath(fam.id, repIdOf(inst, fam), inst.variant);
77
+ function withDoc(inst, fam, secs) {
78
+ const text = inst.docText[pathOf(inst, fam)]; if (!text) return secs;
79
+ const sp = splitDoc(text);
80
+ return secs.map((s) => { const m = sp.find((x) => x.name === s.h); return m && m.text !== s.t ? { ...s, t: m.text, edited: true } : s; });
81
+ }
82
+ function sectionsOf(inst, fam, ctx) { return withDoc(inst, fam, baseSections(inst, fam, ctx)); }
83
+ function baseSections(inst, fam, ctx) {
84
+ const R = rx();
85
+ if (R) return R.sections(fam.id, repIdOf(inst, fam), inst.variant);
86
+ const given = (ctx && ctx.reportFacts && ctx.reportFacts[fam.id]) || [];
87
+ return fam.sections.map((name, i) => {
88
+ const g = given.find((s) => s.name === name) || { name, title: cap(name), claims: [] };
89
+ const r = inst.check && inst.check.sections && inst.check.sections[i];
90
+ const claims = (g.claims || []).map((c) => ({ text: (c && (c.text || c.id)) || String(c), label: c && c.basis, source: c && c.source, ok: !(c && c.ok === false) }));
91
+ const st = !r ? 'check' : r.stale_source > 0 ? 'stale' : (r.needs_input > 0 || r.traced === 0) ? 'check' : 'ok';
92
+ const src = []; const seen = new Set();
93
+ for (const c of claims) if (c.source && !seen.has(c.source)) { seen.add(c.source); src.push(['doc', c.source, kindOf(c.source)]); }
94
+ return { id: name, h: g.title ? cap(g.title) : cap(name), t: claims.length ? claims.map((c) => c.text).join(' ') : 'No claim has been written for this section yet.', src, st, by: 'Records', why: '', claims };
95
+ });
96
+ }
97
+ const decOf = (inst, key, id) => inst.dec[key + '|' + id] || '';
98
+ function readyOf(inst, key, secs) {
99
+ const d = (s) => decOf(inst, key, s.id);
100
+ return { n: secs.length - secs.filter((s) => d(s) === 'cut').length, ap: secs.filter((s) => d(s) === 'ok').length, fl: secs.filter((s) => s.st !== 'ok' && d(s) !== 'ok' && d(s) !== 'cut').length };
101
+ }
102
+
103
+ /* ── loading ── */
104
+ async function load(inst, ctx) {
105
+ const j = await pmGet('/api/report-families');
106
+ if (!insts.has(inst.host)) return;
107
+ if (!j || j.ok === false) inst.error = (j && j.error) || 'Could not read the report families.';
108
+ else { inst.families = j.families || []; inst.error = null; if (!inst.sel && inst.families[0]) { inst.sel = inst.families[0].id; inst.variant = defaultVariant(inst, inst.families[0]); } }
109
+ if (!rx()) { const o = await pmGet('/api/outbox'); if (insts.has(inst.host) && o && o.ok) inst.outSent = (o.entries || []).filter((e) => e.state === 'sent'); }
110
+ inst.hist = null;
111
+ await runCheck(inst, ctx);
112
+ }
113
+ const defaultVariant = (inst, fam) => (variantsOf(inst, fam)[0] || {}).id || null;
114
+
115
+ async function runCheck(inst, ctx) {
116
+ const fam = inst.families && famOf(inst);
117
+ if (!fam) { draw(inst, ctx); return; }
118
+ if (rx()) { draw(inst, ctx); return; } // the demo's sections carry their own state; no request
119
+ const seq = ++inst.seq;
120
+ const j = await pmPost('/api/report-families/check', { family: inst.sel, variant: inst.variant, sections: (fam.sections || []).map((name) => { const g = ((ctx && ctx.reportFacts && ctx.reportFacts[inst.sel]) || []).find((s) => s.name === name); return g || { name, title: cap(name), claims: [] }; }) });
121
+ if (seq !== inst.seq || !insts.has(inst.host)) return;
122
+ inst.check = j && j.ok ? j : null;
123
+ draw(inst, ctx);
124
+ }
125
+
126
+ function pick(inst, ctx, fam, variant) { inst.sel = fam.id; inst.variant = variant || defaultVariant(inst, fam); inst.check = null; inst.draft = null; inst.draftErr = null; draw(inst, ctx); runCheck(inst, ctx); }
127
+
128
+ /* ── pieces lifted from the demo path of the earlier pass: the forecast chart, the reports list ── */
129
+ const NSV = 'http://www.w3.org/2000/svg';
130
+ const sv = (tag, attrs, ...kids) => { const n = document.createElementNS(NSV, tag); for (const k of Object.keys(attrs || {})) n.setAttribute(k, attrs[k]); for (const c of kids) if (c) n.appendChild(c); return n; };
131
+ const svt = (x, y, t, a) => { const n = sv('text', { x, y, ...(a || {}) }); n.textContent = t; return n; };
132
+
133
+ function reportsList(inst, ctx, fam, list, selId) {
134
+ const choose = (id) => { inst.rep[fam.id] = id; inst.draft = null; draw(inst, ctx); };
135
+ return el('section', { class: 'card sect', 'data-rp-section': 'reports' }, el('div', { class: 'sh2' }, el('h2', null, c3ic('doc', 'sm'), 'Reports ', el('span', { class: 'bdg g' }, String(list.length)))),
136
+ el('ul', { class: 'list' }, list.map((r) => {
137
+ const on = r.id === selId;
138
+ const li = el('li', { class: 'nrow', 'data-row': '', 'data-a': 'rsel', 'data-report': r.id, tabindex: '0', 'aria-current': String(on), style: on ? 'background:var(--accw);border-radius:8px' : null },
139
+ el('span', { class: 'ki' }, c3ic(on ? 'eye' : 'doc')),
140
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, r.n), el('div', { class: 'chips' }, el('span', { class: 'chip2' }, c3ic('clock', 'sm'), 'Due ' + r.due))));
141
+ li.addEventListener('click', () => choose(r.id));
142
+ li.addEventListener('keydown', (e) => { if (e.target === li && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); choose(r.id); } });
143
+ return li;
144
+ })));
145
+ }
146
+
147
+ /* ── a section row (the mockup's rsRow) ── */
148
+ const ORIG = { Agent: ['spark', 'var(--purple)', 'Agent wrote'], You: ['user', 'var(--acc)', 'You'], Records: ['doc', 'var(--muted-foreground)', 'From records'] };
149
+ const STATE = { ok: ['var(--success)', 'Traced', 'check'], stale: ['var(--warning)', 'Stale source', 'alert'], check: ['var(--acc)', 'Needs your input', 'eye'] };
150
+ const LABEL_TONE = { fact: ['check', 'ok'], cited: ['check', 'ok'], reported: ['user', ''], estimate: ['clock', 'warn'], assumption: ['alert', 'warn'], judgement: ['eye', ''] };
151
+
152
+ function claimRow(c) {
153
+ const L = LABEL_TONE[c.label];
154
+ const bad = c.ok === false || !L;
155
+ const traced = !bad && (c.label === 'fact' || c.label === 'cited' || c.label === 'reported');
156
+ return el('li', { class: bad ? 'bad' : L[1], 'data-claim': '' }, c3ic(bad ? 'alert' : L[0], 'sm'), el('span', null, c.text + ' ', el('b', { class: 'dim', style: 'font-weight:500' }, traced ? 'Traced' : 'Needs your input')),
157
+ el('span', { class: 'tag' }, (c.label || 'unlabelled') + (c.source ? ' · ' + c.source : '')));
158
+ }
159
+ function detail(s) {
160
+ const claims = s.claims && s.claims.length ? s.claims.map(claimRow) : [el('li', { class: s.st === 'ok' ? 'ok' : s.st === 'stale' ? 'warn' : 'warn' }, c3ic(STATE[s.st][2], 'sm'), el('span', null, STATE[s.st][1] + (s.st === 'stale' ? ': a source is out of date. Refresh it before this goes out.' : s.st === 'check' ? ': a person decides this one.' : ': every number has a source.')), el('span', { class: 'tag' }, 'claims check'))];
161
+ const chips = s.src.length ? s.src.map((q) => (q[2] ? el('button', { type: 'button', class: 'chip2 lk', 'data-a': 'rsrc', 'data-k': q[2], onClick: (e) => { e.stopPropagation(); jump(q[2]); } }, c3ic(q[0], 'sm'), q[1], c3ic('next', 'sm'))
162
+ : el('span', { class: 'chip2' }, c3ic(q[0], 'sm'), q[1]))) : [el('span', { class: 'dim', style: 'font-size:var(--fs-2)' }, s.by === 'You' ? 'Your judgement. No source to trace.' : 'No source is recorded for this section.')];
163
+ return el('div', { class: 'dpn' },
164
+ s.why ? el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, c3ic('info', 'sm'), ' Why it is here: ' + s.why) : null,
165
+ el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Claims check'), el('ul', { class: 'chgl' }, claims),
166
+ el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Where it comes from'), el('div', { class: 'chips2', style: 'flex-wrap:wrap' }, chips),
167
+ s.st !== 'ok' ? el('div', { class: 'chips2', style: 'flex-wrap:wrap' }, el('button', { type: 'button', class: 'chip2', disabled: true, title: FIX_WHY, 'data-rp-act': 'fix' }, c3ic('spark', 'sm'), 'Fix with the agent'), el('span', { class: 'dim', style: 'font-size:var(--fs-2)' }, FIX_WHY)) : null);
168
+ }
169
+
170
+ function sectionRow(inst, ctx, fam, key, s) {
171
+ const DEMO = !!rx();
172
+ const k = key + '|' + s.id; const dst = decOf(inst, key, s.id);
173
+ const cc = STATE[s.st]; const o = ORIG[s.by] || ORIG.Agent;
174
+ const li = el('li', { class: 'nrow' + (dst === 'cut' ? ' cutr' : ''), 'data-row': '', 'data-section': s.id, 'data-rs': k, tabindex: '0', style: dst === 'cut' ? 'opacity:.5' : null });
175
+ const xp = el('div', { class: 'xp' }, el('div', { class: 'xi' }, detail(s)));
176
+ li.append(el('span', { class: 'ki', style: 'color:' + (dst === 'ok' ? 'var(--success)' : cc[0]) }, c3ic(dst === 'ok' ? 'check' : dst === 'cut' ? 'x' : cc[2])),
177
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, s.h), el('div', { class: 'dim', style: 'font-size:var(--fs-2);margin-top:2px' }, s.t),
178
+ el('div', { class: 'chips' }, el('span', { class: 'chip2', style: 'color:' + o[1] }, c3ic(o[0], 'sm'), o[2]), el('span', { class: 'chip2', style: 'color:' + cc[0] }, c3ic(cc[2], 'sm'), cc[1]),
179
+ dst ? el('span', { class: 'chip2' }, dst === 'ok' ? 'Approved' : 'Cut') : null), xp));
180
+ if (DEMO) li.append(el('span', { class: 'sg', title: 'Suggested: approve (drag right)' }, el('span', { class: 'g l' }, c3ic('moon')), el('span', { class: 'g r y' }, c3ic('check'))));
181
+ const ex = attachExpand(li, xp, { key: k, initial: !!inst.open[k], onToggle: (on) => { inst.open[k] = on; } });
182
+ inst.cleanups.push(ex.detach);
183
+ if (DEMO) {
184
+ const decide = (side) => {
185
+ const prev = inst.dec[k];
186
+ inst.dec[k] = side === 'right' ? 'ok' : 'cut';
187
+ li.classList.add('leave');
188
+ setTimeout(() => { toast(side === 'right' ? 'Section approved' : 'Section cut', () => { if (prev) inst.dec[k] = prev; else delete inst.dec[k]; inst.draft = null; draw(inst, ctx); }); inst.draft = null; draw(inst, ctx); }, 230);
189
+ };
190
+ inst.cleanups.push(attachSwipe(li, { onRight: () => decide('right'), onLeft: () => decide('left'), labelRight: 'Approve', labelLeft: 'Cut', iconRight: 'check', iconLeft: 'moon' }));
191
+ } else li.setAttribute('aria-description', NO_STORE);
192
+ return li;
193
+ }
194
+
195
+ /* ── the report as a document (the mockup's docopen, console3.html 4136 and 4335): the DOCUMENT SHEET with its six modes ── */
196
+ const mdOf = (secs) => secs.map((s) => '## ' + s.h + '\n' + s.t + '\n').join('\n');
197
+ const EDGE = { ok: ['in sync', 'ok', 'check'], stale: ['out of sync', 'bad', 'alert'], check: ['needs your input', '', 'eye'] };
198
+ const NO_STORE_WHY = 'Issue this report first. Editing by hand and asking the agent change a stored document, and this report has none yet: it is generated from the records each time the page opens. Nothing is lost.';
199
+
200
+ /** Real mode with no stored document: the checks are the claims check, one per section (traced, or not). */
201
+ function generatedHealth(secs) {
202
+ const checks = secs.map((s) => ({ id: s.id, name: s.h, pass: s.st === 'ok', reason: s.st === 'ok' ? 'every number has a source' : s.st === 'stale' ? 'a source is out of date' : 'a person decides this one' }));
203
+ return { passed: checks.filter((c) => c.pass).length, total: checks.length, checks };
204
+ }
205
+
206
+ function reportRelations(inst, secs, repName, getSheet) {
207
+ return (doc) => {
208
+ const seen = new Map();
209
+ secs.forEach((s) => (s.src || []).forEach((q) => { const e = seen.get(q[1]) || { q, st: [] }; e.st.push(s); seen.set(q[1], e); }));
210
+ const stOf = (e) => (e.st.some((s) => s.st === 'stale') ? 'stale' : e.st.some((s) => s.st === 'check') ? 'check' : 'ok');
211
+ const R = rx(); const sent = R ? ((inst.hist && inst.hist[(famOf(inst) || {}).id]) || []).filter((h) => h[1] === repName) : (inst.outSent || []).filter((e) => (e.subject || '').indexOf(repName) === 0);
212
+ const go = (kind) => { const sh = getSheet(); if (sh) sh.close(); jump(kind); };
213
+ return el('div', { class: 'dpn', 'data-doc-relations': '' },
214
+ el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'This report cites'),
215
+ seen.size ? el('ul', { class: 'chgl' }, [...seen.values()].map((e) => { const x = EDGE[stOf(e)]; return el('li', { class: x[1], 'data-edge': stOf(e) }, c3ic(x[2], 'sm'),
216
+ el('span', null, e.q[1] + ' · ', el('b', null, e.st.map((s) => s.h).join(', ')), ' · ' + x[0] + ' ', e.q[2] ? el('button', { type: 'button', class: 'chip2 lk', 'data-a': 'rsrc', 'data-k': e.q[2], onClick: () => go(e.q[2]) }, c3ic('next', 'sm'), 'Open') : null)); }))
217
+ : el('div', { class: 'fold' }, c3ic('info', 'sm'), 'No section cites a source yet.'),
218
+ el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Cited by'),
219
+ sent.length ? el('ul', { class: 'chgl' }, sent.map((h) => (Array.isArray(h)
220
+ ? el('li', { class: 'ok' }, c3ic('out', 'sm'), el('span', null, h[1] + ' · sent to ' + h[2] + ' · ' + h[0] + ' · ' + h[3]))
221
+ : el('li', { class: 'ok' }, c3ic('out', 'sm'), el('span', null, (h.subject || h.channel) + ' · ' + h.state)))))
222
+ : el('div', { class: 'fold' }, c3ic('info', 'sm'), 'Nothing cites this report yet: it has not been sent.'),
223
+ el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Each section of the report is traced to its source in Initiatives, Calendar or Work.'));
224
+ };
225
+ }
226
+
227
+ function secExtraOf(secs, getSheet) {
228
+ return (name) => {
229
+ const s = secs.find((x) => x.h === name); if (!s) return null;
230
+ const claims = s.claims && s.claims.length ? s.claims.map(claimRow) : [];
231
+ const chips = (s.src || []).map((q) => (q[2] ? el('button', { type: 'button', class: 'chip2 lk', 'data-a': 'rsrc', 'data-k': q[2], onClick: () => { const sh = getSheet(); if (sh) sh.close(); jump(q[2]); } }, c3ic(q[0], 'sm'), q[1], c3ic('next', 'sm')) : el('span', { class: 'chip2' }, c3ic(q[0], 'sm'), q[1])));
232
+ const stt = STATE[s.st];
233
+ return el('div', { class: 'dpn', 'data-rp-claims': s.id },
234
+ claims.length ? el('ul', { class: 'chgl' }, claims) : null,
235
+ el('div', { class: 'chips2', style: 'flex-wrap:wrap' }, el('span', { class: 'chip2', style: 'color:' + stt[0] }, c3ic(stt[2], 'sm'), stt[1]), chips.length ? chips : el('span', { class: 'dim', style: 'font-size:var(--fs-2)' }, s.by === 'You' ? 'Your judgement. No source to trace.' : 'No source is recorded for this section.')));
236
+ };
237
+ }
238
+
239
+ /** Open a report (or a communications message) as a document. path/name name it; secs are its sections; mode is the segment to start on. */
240
+ async function openReportDoc(inst, ctx, { fam, path, name, aud, repName, secs, mode }) {
241
+ const ws = (ctx && ctx.ws) || '';
242
+ let text = null;
243
+ try { const r = await fetch('/api/doc?ws=' + encodeURIComponent(ws) + '&path=' + encodeURIComponent(path)); if (r.ok) text = await r.text(); } catch { /* no stored document */ }
244
+ if (!insts.has(inst.host)) return;
245
+ const stored = text != null;
246
+ if (stored) inst.docText[path] = text;
247
+ let sheet = null; const getSheet = () => sheet;
248
+ const doc = { path, name, ty: 'Report', v: stored ? 'v1' : '', st: 'draft', ini: '', iniName: 'Portfolio' };
249
+ sheet = openDocSheet({ ws, doc, mode: mode || 'read', relations: reportRelations(inst, secs, repName || name, getSheet), secExtra: secExtraOf(secs, getSheet),
250
+ generated: stored ? null : { text: mdOf(secs), why: NO_STORE_WHY, health: generatedHealth(secs) },
251
+ onChanged: async () => { try { const r = await fetch('/api/doc?ws=' + encodeURIComponent(ws) + '&path=' + encodeURIComponent(path)); if (r.ok) { inst.docText[path] = await r.text(); draw(inst, ctx); } } catch { /* the rows keep what they show */ } } });
252
+ void aud; void fam;
253
+ return sheet;
254
+ }
255
+
256
+ /* ── Send: the case file and the slide ── */
257
+ function bodyOf(inst, key, fam, aud, secs) {
258
+ const lines = [];
259
+ for (const s of secs) {
260
+ if (decOf(inst, key, s.id) === 'cut') continue;
261
+ for (const c of s.claims || []) { const key = /\b[A-Z][A-Z0-9]+-\d+\b/.exec(c.source || ''); const lab = key ? key[0] : c.label; lines.push('- ' + c.text + (lab ? ' [' + lab + ']' : '')); }
262
+ }
263
+ return lines.length ? fam.name + ' report for ' + aud + '\n' + lines.join('\n') : '';
264
+ }
265
+ async function ensureDraft(inst, ctx, fam, key, aud, secs) {
266
+ const body = bodyOf(inst, key, fam, aud, secs);
267
+ if (!body || inst.draftBusy) return;
268
+ if ((inst.draft && inst.draft.body === body) || (inst.draftErr && inst.draftErr.body === body)) return;
269
+ inst.draftBusy = true;
270
+ const r = await pmPost('/api/outbox/draft', { channel: 'customer-note', recipient: aud, subject: fam.name + ' report for ' + aud, body });
271
+ inst.draftBusy = false;
272
+ if (!insts.has(inst.host)) return;
273
+ if (r && r.ok && r.entry) { inst.draft = { entry: r.entry, body }; inst.draftErr = null; } else { inst.draft = null; inst.draftErr = { body, error: (r && (r.error || r.reason)) || 'The draft could not be queued.' }; }
274
+ draw(inst, ctx);
275
+ }
276
+ async function confirmReal(inst, ctx, hashFull) {
277
+ const d = inst.draft; const hash = String(hashFull).split('|')[0];
278
+ let text;
279
+ if (!d || d.entry.hash !== hash) text = 'The content changed after the slide was prepared. Nothing was sent. Slide again.';
280
+ else {
281
+ const r = await pmPost('/api/outbox/confirm', { id: d.entry.id, hash });
282
+ if (r && r.ok) text = 'Sent.';
283
+ else if (r && r.reason === 'no channel connected') text = 'No channel connected. Nothing was sent. The draft stays queued in the outbox.';
284
+ else text = 'Not sent: ' + ((r && (r.reason || r.error)) || 'no answer') + '.';
285
+ }
286
+ if (!insts.has(inst.host)) return;
287
+ inst.outMsg[inst.draft ? inst.draft.entry.id : 'x'] = text;
288
+ toast(text);
289
+ draw(inst, ctx);
290
+ }
291
+
292
+ function sendCase(inst, ctx, fam, key, secs, q, aud, repName, vr) {
293
+ const DEMO = !!rx();
294
+ /* "Resolve the flagged sections first" (the mockup's confirmCase): the case settles on that choice with an undo, and a toast says "Flags opened." The sections themselves stay as they are. */
295
+ const open = () => { inst.flagged[key] = 1; toast('Flags opened.', () => { delete inst.flagged[key]; draw(inst, ctx); }); draw(inst, ctx); };
296
+ const settled = (cs) => {
297
+ cs.querySelectorAll('.opts,.c3slide').forEach((n) => n.remove());
298
+ cs.append(el('div', { class: 'chips2', 'data-flags-settled': '' }, c3ic('check', 'sm'), el('b', { style: 'font-size:var(--fs-3);color:var(--success)' }, 'Resolve the flagged sections first'),
299
+ el('button', { type: 'button', class: 'chip2 lk', onclick: () => { delete inst.flagged[key]; draw(inst, ctx); } }, c3ic('undo', 'sm'), 'undo')));
300
+ return cs;
301
+ };
302
+ if (DEMO) {
303
+ const R = rx();
304
+ const model = { id: 'rpt:' + key, sit: 'The ' + repName + ' for ' + aud + ' has ' + q.ap + ' of ' + q.n + ' sections approved' + (q.fl ? ' and ' + q.fl + ' still need a look' : '') + '. Sending is final: it goes in the history with what it said.', conf: q.fl ? 64 : 86,
305
+ chg: [['check', q.ap + ' of ' + q.n + ' sections approved', 'sections', q.ap === q.n ? 'ok' : 'warn'], ['alert', q.fl ? q.fl + ' section' + (q.fl > 1 ? 's' : '') + ' stale or unconfirmed' : 'All sections traced', 'claims check', q.fl ? 'warn' : 'ok'], ['user', 'Audience: ' + aud, 'audience']],
306
+ opts: [{ id: 's', l: 'Send it', rec: q.fl ? 0 : 1, outward: true, fx: [['check', 'Recorded with its sources', 'ok']] }, { id: 'r', l: 'Resolve the flagged sections first', rec: q.fl ? 1 : 0, fx: [['eye', 'Review what is stale', 'ok']] }, { id: 'h', l: 'Not yet', fx: [] }] };
307
+ const cs = renderCase(model, { outbox: true, onConfirm: ({ option }) => {
308
+ if (option.id === 's') {
309
+ const hist = inst.hist[fam.id]; const ent = ['Thu 8 Oct', repName, aud, 'just sent', ''];
310
+ inst.sent[key] = 1; hist.unshift(ent);
311
+ toast('Sent to ' + aud + '. It is in the history.', () => { delete inst.sent[key]; const i = hist.indexOf(ent); if (i >= 0) hist.splice(i, 1); draw(inst, ctx); });
312
+ draw(inst, ctx);
313
+ } else if (option.id === 'r') open(); else { toast('Held.'); draw(inst, ctx); }
314
+ } });
315
+ void R;
316
+ return inst.flagged[key] ? settled(cs) : cs;
317
+ }
318
+ /* REAL: the draft is the unit; the slide is bound to its hash */
319
+ const d = inst.draft; const err = inst.draftErr;
320
+ const claimsN = secs.reduce((n, s) => n + (s.claims || []).length, 0);
321
+ const blocked = d && !d.entry.claims.ok;
322
+ const none = !claimsN || (!d && !err && !inst.draftBusy && !bodyOf(inst, key, fam, aud, secs));
323
+ const chg = [];
324
+ chg.push(['out', none ? 'No section has a claim yet: there is nothing to send.' : d ? 'Draft queued in the outbox, hash ' + d.entry.hash.slice(0, 10) : err ? err.error : 'Queuing the draft in the outbox.', 'outbox', none || err ? 'warn' : 'ok']);
325
+ if (d) chg.push([d.entry.claims.ok ? 'check' : 'alert', d.entry.claims.ok ? 'Every claim line has a label.' : d.entry.claims.refused.length + ' claim line' + (d.entry.claims.refused.length > 1 ? 's fail' : ' fails') + ' the outbound trust rules (a label with its source, or a ticket key).', 'claims check', d.entry.claims.ok ? 'ok' : 'bad']);
326
+ if (blocked) d.entry.claims.refused.forEach((l) => chg.push(['x', l, 'refused', 'bad']));
327
+ if (vr && vr.external) chg.push(['shield', 'A customer report allows only fact and cited labels.', 'rule', 'warn']);
328
+ chg.push(['user', 'Audience: ' + aud, 'audience']);
329
+ const msg = d && inst.outMsg[d.entry.id];
330
+ const model = { id: d ? d.entry.hash : 'rpt:' + key, sit: msg || 'Nothing has been sent. The slide sends exactly this draft, bound to its hash, and only when a channel is connected.', conf: q.fl ? 40 : 70, chg,
331
+ outboxReason: none ? 'Nothing to send: no section has a claim yet.' : blocked ? 'Blocked: a claim line has no label the rules accept, or no ticket key. Fix it or cut it.' : err ? err.error : 'The draft is not ready yet.',
332
+ opts: [{ id: 's', l: 'Send it', rec: q.fl ? 0 : 1, outward: true, fx: [['out', 'Goes through the outbox, bound to the hash', 'ok']] }, { id: 'r', l: 'Resolve the flagged sections first', rec: q.fl ? 1 : 0, fx: [['eye', 'Review what is stale', 'ok']] }, { id: 'h', l: 'Not yet', fx: [] }] };
333
+ const cr = renderCase(model, { outbox: !!d && !blocked, onConfirm: ({ option, hash }) => { if (option.id === 's') confirmReal(inst, ctx, hash); else if (option.id === 'r') open(); else { toast('Held.'); draw(inst, ctx); } } });
334
+ return inst.flagged[key] ? settled(cr) : cr;
335
+ }
336
+
337
+ function sendRow(inst, ctx, fam, key, secs, q, aud, repName, vr) {
338
+ const DEMO = !!rx(); const sent = DEMO && inst.sent[key]; const k = key + '|send';
339
+ const li = el('li', { class: 'nrow', 'data-row': '', 'data-send': '', tabindex: '0' });
340
+ const xp = el('div', { class: 'xp' }, el('div', { class: 'xi' }));
341
+ const fill = () => { const xi = xp.querySelector('.xi'); xi.replaceChildren(sendCase(inst, ctx, fam, key, secs, q, aud, repName, vr)); };
342
+ li.append(el('span', { class: 'ki', style: 'color:' + (sent ? 'var(--success)' : 'var(--acc)') }, c3ic(sent ? 'check' : 'out')),
343
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, sent ? 'Sent' : 'Send'), el('div', { class: 'dim', style: 'font-size:var(--fs-2)' },
344
+ sent ? 'It is in the history with its sources.' : DEMO ? q.ap + ' of ' + q.n + ' approved. Slide to send, or swipe sections first.' : 'Slide to send. Nothing is sent while no channel is connected.'), xp));
345
+ if (!sent) fill(); else xp.querySelector('.xi').replaceChildren(el('div', { class: 'fold' }, c3ic('check', 'sm'), 'Sent. It is in the history with its sources.'));
346
+ const ex = attachExpand(li, xp, { key: k, initial: !!inst.open[k], onToggle: (on) => { inst.open[k] = on; if (on && !DEMO) ensureDraft(inst, ctx, fam, key, aud, secs); } });
347
+ inst.cleanups.push(ex.detach);
348
+ if (inst.open[k] && !DEMO) ensureDraft(inst, ctx, fam, key, aud, secs);
349
+ return li;
350
+ }
351
+
352
+ /* ── Communications (demo): the queue ── */
353
+ function cmRow(inst, ctx, c) {
354
+ const st = inst.csent[c.id] ? 'sent' : inst.chold[c.id] ? 'held' : 'draft';
355
+ const sc = { draft: ['var(--acc)', 'Draft'], held: ['var(--mfg)', 'Held'], sent: ['var(--success)', 'Sent'] }[st];
356
+ const k = 'cm' + c.id;
357
+ const sendIt = () => { inst.csent[c.id] = 1; const was = inst.chold[c.id]; delete inst.chold[c.id]; toast(c.ext ? 'Approved. Sent to ' + c.to + '.' : 'Sent to ' + c.to + '.', () => { delete inst.csent[c.id]; if (was) inst.chold[c.id] = was; draw(inst, ctx); }); draw(inst, ctx); };
358
+ const hold = () => { inst.chold[c.id] = 1; toast('Held.', () => { delete inst.chold[c.id]; draw(inst, ctx); }); draw(inst, ctx); };
359
+ const model = { id: 'com:' + c.id, quote: c.body, sit: c.why, conf: c.ext ? 72 : 84,
360
+ chg: [['out', 'To ' + c.to + ' by ' + c.ch, 'channel'], ['user', 'Tone: ' + c.tone, 'agent'], ['alert', c.risk, 'risk', c.ext ? 'warn' : '']],
361
+ opts: [{ id: 's', l: c.ext ? 'Approve and send' : 'Send now', rec: c.ext ? 0 : 1, outward: true, fx: [['check', 'Logged in the history', 'ok']] }, { id: 'o', l: 'Open and edit it first', rec: c.ext ? 1 : 0, fx: [] }, { id: 'h', l: 'Hold it', fx: [['clock', 'Reminder tomorrow']] }] };
362
+ const cs = renderCase(model, { outbox: true, onConfirm: ({ option }) => { if (option.id === 's') sendIt(); else if (option.id === 'h') hold(); else { const m = (id, h, t) => ({ id, h, t, src: [], st: 'ok', by: 'Agent', why: '', claims: [] }); openReportDoc(inst, ctx, { fam: 'communications', path: docPath('communications', c.id, 'msg'), name: c.t, repName: c.t, aud: c.to, mode: 'edit', secs: [m('m', 'Message', c.body), m('w', 'Why now', c.why), m('t', 'Tone and risk', c.tone + '. ' + c.risk)] }); } } });
363
+ inst.cleanups.push(() => cs.c3case.dispose());
364
+ const li = el('li', { class: 'nrow', 'data-row': '', 'data-cm': c.id, tabindex: '0' });
365
+ const xp = el('div', { class: 'xp' }, el('div', { class: 'xi' }, cs));
366
+ li.append(el('span', { class: 'ki', style: 'color:' + sc[0] }, c3ic(c.ext ? 'out' : 'reply')),
367
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, c.t), el('div', { class: 'chips' }, el('span', { class: 'chip2', style: 'color:' + sc[0] }, sc[1]), el('span', { class: 'chip2' }, c3ic('user', 'sm'), c.to), el('span', { class: 'chip2' }, c3ic('clock', 'sm'), c.due), el('span', { class: 'tag' }, c.ch),
368
+ c.ext ? el('span', { class: 'st warn' }, c3ic('shield', 'sm'), 'external, you approve') : null), xp),
369
+ el('span', { class: 'sg', title: 'Suggested: send (drag right)' }, el('span', { class: 'g l' }, c3ic('moon')), el('span', { class: 'g r y' }, c3ic('check'))));
370
+ inst.cleanups.push(attachExpand(li, xp, { key: k, initial: !!inst.open[k], onToggle: (on) => { inst.open[k] = on; } }).detach);
371
+ inst.cleanups.push(attachSwipe(li, { onRight: sendIt, onLeft: hold, labelRight: c.ext ? 'Approve and send' : 'Send', labelLeft: 'Hold', iconRight: 'out', iconLeft: 'moon', disabled: () => st === 'sent' }));
372
+ return li;
373
+ }
374
+ function commsQueue(inst, ctx, fam) {
375
+ const R = rx(); const aud = inst.variant || 'all';
376
+ const L = R.COMMS.filter((c) => aud === 'all' || c.aud === aud);
377
+ return el('section', { class: 'card sect', 'data-rp-section': 'queue' }, el('div', { class: 'sh2' }, el('h2', null, c3ic('out', 'sm'), 'Messages ', el('span', { class: 'bdg g' }, String(L.length))), dim('by when it matters')),
378
+ el('ul', { class: 'list' }, L.map((c) => cmRow(inst, ctx, c))),
379
+ el('div', { class: 'pgl' }, el('span', null, 'swipe right: send'), el('span', null, 'swipe left: hold'), el('span', null, 'external: you approve')));
380
+ }
381
+
382
+ /* ── right-hand cards ── */
383
+ const bars = (rows) => el('div', { class: 'hbs' }, rows.map((r, i) => el('div', { class: 'hbr', style: '--c:' + r[2] }, el('div', { class: 'l' }, el('span', null, r[0]), el('i', null, String(r[3]))), el('div', { class: 'tk' }, el('u', { style: 'width:' + Math.max(2, r[1] * 100) + '%;animation-delay:' + (i * 90) + 'ms' })))));
384
+
385
+ function claimsCard(secs) {
386
+ const t = { ok: 0, stale: 0, check: 0 }; secs.forEach((s) => { t[s.st]++; });
387
+ const all = Math.max(1, secs.length);
388
+ return el('section', { class: 'card', 'data-rp-section': 'claims' }, el('h3', null, c3ic('shield'), 'Claims check ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'every number traced')),
389
+ bars([['Traced to a source', t.ok / all, 'var(--success)', t.ok], ['Source is stale', t.stale / all, 'var(--warning)', t.stale], ['Needs your input', t.check / all, 'var(--acc)', t.check]]),
390
+ el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'A report with an untraced number does not go out. Guesses are labelled as guesses.'));
391
+ }
392
+ function sentCard(inst, fam) {
393
+ const R = rx();
394
+ const head = el('h3', null, c3ic('clock'), 'Sent ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'who read it, what came back'));
395
+ if (R) {
396
+ const list = (inst.hist && inst.hist[fam.id]) || [];
397
+ return el('section', { class: 'card', 'data-rp-section': 'sent' }, head, el('ul', { class: 'hx' }, list.map((h) => el('li', null, el('span', { class: 'chip2' }, c3ic('out', 'sm'), h[2]), el('span', null, h[1], el('small', null, h[3] + (h[4] ? ' · ' + h[4] : ''))), el('span', { class: 'w' }, h[0])))));
398
+ }
399
+ const sent = inst.outSent || [];
400
+ return el('section', { class: 'card', 'data-rp-section': 'sent' }, head, sent.length ? el('ul', { class: 'hx' }, sent.map((e) => el('li', null, el('span', { class: 'chip2' }, c3ic('out', 'sm'), e.recipient || e.channel), el('span', null, (e.subject || e.body.split('\n')[0]), el('small', null, e.channel)), el('span', { class: 'w' }, e.state))))
401
+ : el('div', { class: 'fold' }, c3ic('check', 'sm'), 'Nothing has been sent. No channel is connected, so nothing can go out yet.'));
402
+ }
403
+ function truthBlock(inst, ctx) {
404
+ const R = rx();
405
+ const sec = el('section', { class: 'card', 'data-rp-section': 'who-holds-what' }, el('h3', null, c3ic('user'), 'Who holds what ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'told against true now')));
406
+ if (R) {
407
+ sec.appendChild(el('ul', { class: 'hx' }, R.TRUTH.map((x) => { const cur = !x[4] || inst.csent.c1;
408
+ return el('li', { 'data-person': x[0], 'data-state': cur ? 'in sync' : 'stale' }, el('span', { class: 'chip2', style: 'color:' + (cur ? 'var(--success)' : 'var(--destructive)') }, c3ic(cur ? 'check' : 'alert', 'sm'), x[0]),
409
+ el('span', null, 'Told: ' + x[2], el('small', null, 'Now: ' + x[3] + (cur ? '' : ' · ' + x[4] + ' days apart'))), el('span', { class: 'w' }, x[1])); })));
410
+ return sec;
411
+ }
412
+ const rows = (ctx && ctx.reportFacts && ctx.reportFacts.truthTable) || [];
413
+ if (!rows.length) { sec.appendChild(el('div', { class: 'fold' }, c3ic('check', 'sm'), 'No message has told anyone a claim yet.')); return sec; }
414
+ const tone = (s) => (s === 'contradicted' ? 'var(--destructive)' : s === 'stale' ? 'var(--warning)' : 'var(--success)');
415
+ sec.appendChild(el('ul', { class: 'hx' }, rows.flatMap((p) => p.claims.map((c) => el('li', { 'data-person': p.person, 'data-state': c.state },
416
+ el('span', { class: 'chip2', style: 'color:' + tone(c.state) }, c3ic(c.state === 'contradicted' || c.state === 'stale' ? 'alert' : 'check', 'sm'), p.person),
417
+ el('span', null, c.text || c.id, el('small', null, c.state)), el('span', { class: 'w' }, c.state))))));
418
+ return sec;
419
+ }
420
+ const guardCard = () => el('section', { class: 'card', 'data-rp-section': 'guardrails' }, el('h3', null, c3ic('shield'), 'Guardrails'),
421
+ el('ul', { class: 'chgl' }, ['External messages need your approval. They are never sent automatically.', 'A claim line with no label or ticket key blocks the slide.', 'Every message is logged with what it said.', 'With no channel connected nothing is sent, and the page says so.'].map((t) => el('li', { class: 'ok' }, c3ic('check', 'sm'), el('span', null, t)))));
422
+
423
+ /* ── the page ── */
424
+ function draw(inst, ctx) {
425
+ const root = inst.root;
426
+ inst.cleanups.splice(0).forEach((f) => { try { f(); } catch { /* a detach that fails changes nothing */ } });
427
+ if (inst.error) { root.replaceChildren(el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), inst.error)); return; }
428
+ if (!inst.families) { root.replaceChildren(el('div', { class: 'fold a', role: 'status' }, el('span', { class: 'c3spin' }, c3ic('spark', 'sm')), 'Reading the report families.')); return; }
429
+ const R = rx();
430
+ if (R && !inst.hist) inst.hist = JSON.parse(JSON.stringify(R.HIST));
431
+ const fam = famOf(inst);
432
+ const badges = ctx && ctx.reportFacts && ctx.reportFacts.badges;
433
+ const draftN = R ? R.COMMS.filter((c) => !inst.csent[c.id] && !inst.chold[c.id]).length : (badges && badges.communications) || 0;
434
+ const seg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Report type', 'data-rp-section': 'families' }, inst.families.map((f) => el('button', { type: 'button', 'data-a': 'rfam', 'data-family': f.id, 'aria-pressed': String(f.id === inst.sel), 'aria-current': f.id === inst.sel ? 'true' : null,
435
+ title: f.intent + ' · ' + f.cadence, onClick: () => pick(inst, ctx, f) }, c3ic(FAM_ICON[f.id] || 'doc', 'sm'), f.name, f.id === 'communications' && draftN ? [' ', el('span', { class: 'bdg' }, String(draftN))] : null)));
436
+ const bar = el('div', { class: 'calbar', style: 'flex-wrap:wrap' }, seg,
437
+ fam ? el('div', { class: 'chips2', role: 'group', 'aria-label': fam.name + ' variants' }, variantsOf(inst, fam).map((v) => el('button', { type: 'button', class: 'pill', 'data-a': fam.id === 'periodic' ? 'rsel' : 'raud', 'data-variant': v.id, 'aria-pressed': String(v.id === inst.variant), onClick: () => { inst.variant = v.id; inst.draft = null; inst.draftErr = null; if (!R) { inst.check = null; draw(inst, ctx); runCheck(inst, ctx); } else draw(inst, ctx); } }, v.name))) : null);
438
+ let body = el('div', { class: 'fold' }, c3ic('check', 'sm'), 'No report family is defined.');
439
+ if (fam) {
440
+ const key = keyOf(inst, fam); const vr = variantOf(inst, fam); const rep = repOf(inst, fam);
441
+ const secs = sectionsOf(inst, fam, ctx);
442
+ const q = readyOf(inst, key, secs);
443
+ const audName = R ? (fam.id === 'periodic' ? rep.aud : vr.name) : (vr.name || fam.name);
444
+ const repName = R && rep ? rep.n : fam.name;
445
+ const comms = { drafts: draftN, sent: R ? R.COMMS.filter((c) => inst.csent[c.id]).length : 0 };
446
+ const hd = R ? R.info(fam.id, repIdOf(inst, fam), audName, q, comms) : null;
447
+ const traced = secs.filter((s) => s.st === 'ok').length;
448
+ const head = el('div', { class: 'lh' }, el('span', { class: 'ki' }, c3ic(FAM_ICON[fam.id] || 'doc')),
449
+ el('div', { style: 'min-width:0' }, el('b', null, hd ? hd.h : fam.name + ': ' + traced + ' of ' + secs.length + ' sections traced'), el('div', { class: 'dim', 'data-rp-section-purpose': '' }, hd ? hd.s : fam.purpose)),
450
+ hd ? c3ring(hd.p[0], Math.max(1, hd.p[1]), 44) : c3ring(traced, Math.max(1, secs.length), 44, traced + '/' + secs.length));
451
+ const F = R ? R.FAMS[fam.id] : null;
452
+ const facts = el('div', { class: 'lkrow' }, [['target', 'Purpose', F ? F.purpose : fam.purpose], ['user', 'Audience', F ? F.aud : fam.variants.map((v) => v.audience).join(', ')], ['bolt', 'Intent', F ? F.intent : fam.intent], ['cal', 'Cadence', F ? F.cad : fam.cadence]]
453
+ .map((x) => el('span', { class: 'chip2', title: x[2] }, c3ic(x[0], 'sm'), el('b', { style: 'font-weight:600' }, x[1]), ' ' + String(x[2] || '').slice(0, 38))));
454
+ const c = inst.check;
455
+ const notes = !R && c ? [...(c.reasons || []).map((t) => el('div', { class: 'fold w', 'data-blocked': '' }, c3ic('alert', 'sm'), t)), ...(c.warnings || []).map((t) => el('div', { class: 'fold w' }, c3ic('info', 'sm'), t))] : [];
456
+ /* the report card: a row per section, then Send */
457
+ const doc = el('section', { class: 'card sect', 'data-rp-section': 'family', 'aria-label': fam.name },
458
+ el('div', { class: 'sh2' }, el('h2', null, c3ic('doc', 'sm'), repName + ' ', R ? el('span', { class: 'chip2' }, c3ic('user', 'sm'), audName) : el('span', { class: 'bdg g' }, String(secs.length)), R ? null : dim(vr.name)),
459
+ el('span', { class: 'chips2' }, el('button', { type: 'button', class: 'ib sm', 'data-a': 'docopen', title: 'Open as a document to edit by hand or ask the agent', 'aria-label': 'Open as a document to edit by hand or ask the agent', onClick: () => openReportDoc(inst, ctx, { fam, path: pathOf(inst, fam), name: repName + ' for ' + audName, aud: audName, repName, secs: secs.filter((s) => decOf(inst, key, s.id) !== 'cut') }) }, c3ic('notes', 'sm')))),
460
+ el('ul', { class: 'list' }, secs.length ? [...secs.map((s) => sectionRow(inst, ctx, fam, key, s)), sendRow(inst, ctx, fam, key, secs, q, audName, repName, vr)] : el('li', { class: 'fold' }, c3ic('check', 'sm'), 'This family has no sections.')),
461
+ el('div', { class: 'pgl' }, R ? [el('span', null, 'swipe right: approve'), el('span', null, 'swipe left: cut'), el('span', null, 'tap: where it comes from')] : [el('span', null, 'tap: where it comes from'), el('span', null, 'approve and cut: off, no record stores them')]),
462
+ notes.length ? el('div', { 'data-rp-section': 'check-notes', style: 'display:grid;gap:var(--sp-2)' }, notes) : null);
463
+ const rf = (ctx && ctx.reportFacts) || {};
464
+ const sctx = { goInit: (id) => { if (ctx && ctx.openInitiative) ctx.openInitiative(ctx.repo, id); } };
465
+ const left = [(ctx && ctx.plan) || null];
466
+ let right = [];
467
+ if (R && fam.id === 'communications') { left.push(commsQueue(inst, ctx, fam)); right = [truthBlock(inst, ctx), el('section', { class: 'card', 'data-rp-section': 'channels' }, el('h3', null, c3ic('out'), 'Channels'), bars(R.CHANNELS)), guardCard(), sentCard(inst, fam)]; }
468
+ else {
469
+ if (R && R.REPORTS[fam.id] && fam.id !== 'periodic') left.push(reportsList(inst, ctx, fam, R.REPORTS[fam.id], repIdOf(inst, fam)));
470
+ left.push(doc);
471
+ if (fam.id === 'project') { const rp = repOf(inst, fam); const nm = (R && R.INM) || {}; right.push((ctx && ctx.portfolio) || null, forecastChecksCard(rp && rp.i ? { id: rp.i, title: nm[rp.i] || String(rp.n || '').replace(/ status$/, '') } : {})); }
472
+ if (fam.id === 'product') right.push(strategyPanel('north', sctx, { view: 'year', plain: true }), strategyPanel('outcomes', sctx)); // the mockup's product report (line 4345): north star, quarter outcomes, then the benchmark
473
+ if (R && fam.id === 'product') right.push(el('section', { class: 'card', 'data-rp-section': 'benchmark' }, el('h3', null, c3ic('report'), 'Benchmark ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'checkout conversion')), bars(R.BENCH)));
474
+ if (R && fam.id === 'periodic') right.push(el('section', { class: 'card', 'data-rp-section': 'rhythm' }, el('h3', null, c3ic('cal'), 'Rhythm ', el('span', { class: 'dim', style: 'text-transform:none;letter-spacing:0' }, 'next due')),
475
+ el('ul', { class: 'hx' }, R.REPORTS.periodic.map((x) => el('li', null, el('span', { class: 'chip2', style: 'color:' + (x.pct >= 0.8 ? 'var(--success)' : 'var(--acc)') }, c3ic('doc', 'sm'), Math.round(x.pct * 100) + '%'), el('span', null, x.n, el('small', null, x.cad + ' · ' + x.aud)), el('span', { class: 'w' }, x.due))))));
476
+ right.push(claimsCard(secs));
477
+ if (fam.id === 'communications') right.push(truthBlock(inst, ctx), guardCard());
478
+ right.push(sentCard(inst, fam));
479
+ if (R && fam.id === 'periodic') right.push(el('section', { class: 'card', 'data-rp-section': 'questions' }, el('h3', null, c3ic('reply'), 'Open questions from readers'), el('ul', { class: 'chgl' }, R.QUESTIONS.map((t) => el('li', { class: 'warn' }, c3ic('reply', 'sm'), el('span', null, t))))));
480
+ }
481
+ body = el('div', null, head, facts, el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, left), el('div', { class: 'bcol r' }, right))));
482
+ }
483
+ root.replaceChildren(el('div', { class: 'calwrap' }, bar, el('div', { class: 'calbody' }, body)));
484
+ /* in demo the shell's own outbox host would repeat the Communications queue the page now draws */
485
+ const oh = inst.host.parentNode && inst.host.parentNode.querySelector && inst.host.parentNode.querySelector('[data-c3pg="outbox-host"]');
486
+ if (oh) { if (R) oh.setAttribute('data-rp-hide', ''); else oh.removeAttribute('data-rp-hide'); }
487
+ }
488
+
489
+ export function mountReportFamilies(host, ctx) {
490
+ let inst = insts.get(host);
491
+ if (!inst) { inst = make(host); insts.set(host, inst); inst.stopPhone = c3Phone(inst.root); }
492
+ ensureC3Css();
493
+ if (inst.root.parentNode !== host) host.replaceChildren(inst.root);
494
+ if (ctx && ctx.FIXTURE) { inst.root.replaceChildren(el('div', { class: 'fold' }, c3ic('info', 'sm'), 'Report families are not shown in fixture mode.')); return; }
495
+ if (!inst.families && !inst.loading) { inst.loading = true; draw(inst, ctx); load(inst, ctx).finally(() => { inst.loading = false; }); } else draw(inst, ctx);
496
+ }
497
+
498
+ export function disposeReportFamilies(host) {
499
+ const inst = insts.get(host);
500
+ if (!inst) return;
501
+ inst.seq++; inst.cleanups.splice(0).forEach((f) => { try { f(); } catch { /* nothing */ } });
502
+ if (inst.stopPhone) inst.stopPhone(); inst.root.remove(); insts.delete(host);
503
+ }