create-pmos 0.240.0 → 0.242.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 (68) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-report-structure-real-test.js +211 -0
  7. package/workspace/c3-report-structure-test.js +382 -0
  8. package/workspace/guide/console-calendar.md +14 -0
  9. package/workspace/guide/index.md +2 -0
  10. package/workspace/guide/notes.md +22 -1
  11. package/workspace/guide/report-design.md +4 -0
  12. package/workspace/guide/report-structure.md +91 -0
  13. package/workspace/guide/reports-dashboards.md +1 -1
  14. package/workspace/guide/reports-set-up.md +2 -0
  15. package/workspace/lib/charts-store.js +124 -0
  16. package/workspace/lib/headless.js +17 -3
  17. package/workspace/lib/meeting-material.js +151 -0
  18. package/workspace/lib/meeting-replay.js +170 -0
  19. package/workspace/lib/needs-providers.js +1 -0
  20. package/workspace/lib/notes-screen.js +128 -8
  21. package/workspace/lib/report-structure-needs.js +64 -0
  22. package/workspace/lib/report-structure.js +515 -0
  23. package/workspace/lib/route-modules.js +3 -0
  24. package/workspace/lib/routes-meeting-focus.js +5 -3
  25. package/workspace/lib/routes-meeting-material.js +143 -0
  26. package/workspace/lib/routes-meeting-replay.js +75 -0
  27. package/workspace/lib/routes-notes.js +37 -4
  28. package/workspace/lib/routes-report-setups.js +11 -6
  29. package/workspace/lib/routes-report-structure.js +619 -0
  30. package/workspace/package-lock.json +2 -2
  31. package/workspace/package.json +1 -1
  32. package/workspace/public/pm/c3-calendar.js +15 -3
  33. package/workspace/public/pm/c3-demo-material.js +58 -0
  34. package/workspace/public/pm/c3-demo-rd.js +4 -0
  35. package/workspace/public/pm/c3-demo.js +5 -1
  36. package/workspace/public/pm/c3-design-studio.js +7 -3
  37. package/workspace/public/pm/c3-focus.js +83 -16
  38. package/workspace/public/pm/c3-listen.css +10 -0
  39. package/workspace/public/pm/c3-material.css +22 -0
  40. package/workspace/public/pm/c3-material.js +311 -0
  41. package/workspace/public/pm/c3-notes.js +12 -4
  42. package/workspace/public/pm/c3-replay.css +127 -0
  43. package/workspace/public/pm/c3-replay.js +413 -0
  44. package/workspace/public/pm/c3-report-setup.js +3 -0
  45. package/workspace/public/pm/c3-report-structure.css +40 -0
  46. package/workspace/public/pm/c3-report-structure.js +455 -0
  47. package/workspace/public/pm/c3-reports.css +2 -0
  48. package/workspace/public/pm/c3-reports.js +22 -3
  49. package/workspace/public/pm/c3-shell.js +2 -0
  50. package/workspace/public/pm/dest-notes.js +4 -2
  51. package/workspace/public/pm/listen-sheet.js +94 -0
  52. package/workspace/public/pm/listen-sources.js +82 -0
  53. package/workspace/public/pm/notes-aside.js +62 -0
  54. package/workspace/public/pm/notes-screen-capture.js +23 -10
  55. package/workspace/public/pm/notes-screen-view.js +18 -5
  56. package/workspace/public/pm/notes-shots.js +55 -0
  57. package/workspace/public/pm/shell-recording.js +10 -2
  58. package/workspace/public/pm/view-notes.js +48 -8
  59. package/workspace/server.js +38 -9
  60. package/workspace/skills/report-source-check.skill +45 -0
  61. package/workspace/skills/report-structure-dry.skill +51 -0
  62. package/workspace/skills/report-structure.skill +61 -0
  63. package/workspace/skills/report-template.skill +10 -0
  64. package/workspace/test-lists/meeting-material.json +6 -0
  65. package/workspace/test-lists/meeting-replay.json +6 -0
  66. package/workspace/test-lists/mm-capture.json +15 -0
  67. package/workspace/test-lists/report-structure-backend.json +5 -0
  68. package/workspace/test-lists/report-structure-ui.json +11 -0
@@ -0,0 +1,455 @@
1
+ /* pm/c3-report-structure.js — the STRUCTURE STEP of the report studio and the saved STRUCTURE in the report setup sheet (initiative report-structure;
2
+ * PRD planning/prd/report-structure-prd.md; API contract planning/prd/report-structure-contract.md; mockup 43-reportstructure.js).
3
+ *
4
+ * openStructure({ ws, id, name, view, onClose }) the wide sheet: the conversation on the left (answer chips, the PMOS lines that name each version),
5
+ * the live report plan on the right (the version strip S1..Sn and three views: Plan, Other
6
+ * structures, Try it with real data); Use this structure saves the version on screen
7
+ * structureSection({ ws, id, name }) the saved structure as a section of the report setup sheet (c3-design-studio.js openBrief):
8
+ * what it answers, sources, charts, outline, Refine the structure, Try it with real data
9
+ * Entry points: "Shape it with PMOS" on an Ask PMOS answer and "Shape the report" on a set-up row (c3-report-setup.js), the step bar of the Design studio
10
+ * (c3-design-studio.js) and ?structure=<id> on the address (c3-shell.js).
11
+ * A turn starts ONE report-structure job (POST /api/report-structure/turn); the page shows it running and reads GET /api/report-structure until the
12
+ * version lands. The page can be left: the job goes on. Sources are read only. Nothing here sends anything outside the workspace. */
13
+ import { el } from './dom.js';
14
+ import { icon, pbtn, btn, chip, sect, put, lk, kv } from './c3-ini-ui.js';
15
+ import { openSheet, closeSheet } from './c3-sheet.js';
16
+ import { toast } from './c3-toast.js';
17
+ import { pmGet, pmPost } from './http.js';
18
+ import { goTo } from './c3-report-store.js';
19
+ import { ensureDeckCss } from './c3-report-deck.js';
20
+
21
+ const q = (o) => Object.entries(o).filter(([, v]) => v != null && v !== '').map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&');
22
+ const post = (p, body) => Promise.resolve(pmPost('/api/' + p, body)).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
23
+ const get = (p) => Promise.resolve(pmGet(p)).catch(() => null);
24
+ /** The routes of the API contract (report-structure-contract.md section 2). */
25
+ export const RSX = {
26
+ structure: (ws, id) => get('/api/report-structure?' + q({ ws, id })),
27
+ version: (ws, id, n) => get('/api/report-structure/version?' + q({ ws, id, n })),
28
+ dry: (ws, id, run) => get('/api/report-structure/dry?' + q({ ws, id, run })),
29
+ charts: (ws) => get('/api/charts?' + q({ ws })),
30
+ turn: (b) => post('report-structure/turn', b),
31
+ accept: (b) => post('report-structure/accept', b),
32
+ revert: (b) => post('report-structure/revert', b),
33
+ source: (b) => post('report-structure/source', b),
34
+ remove: (b) => post('report-structure/remove', b),
35
+ check: (b) => post('report-structure/check', b),
36
+ runDry: (b) => post('report-structure/dry', b),
37
+ };
38
+ const failed = (r, what) => toast({ text: what + ': ' + ((r && (r.error || r.reason)) || 'the workspace did not answer') + '.', tone: 'error', timeout: 6000 });
39
+
40
+ /** Link c3-report-structure.css once. */
41
+ export function ensureStructureCss() {
42
+ if (typeof document === 'undefined' || document.querySelector('link[data-c3-report-structure]')) return;
43
+ const l = document.createElement('link');
44
+ l.rel = 'stylesheet'; l.href = new URL('./c3-report-structure.css', import.meta.url).href; l.setAttribute('data-c3-report-structure', '');
45
+ document.head.appendChild(l);
46
+ }
47
+
48
+ const hhmm = (iso) => { const m = /T(\d{2}):(\d{2})/.exec(String(iso || '')); return m ? m[1] + ':' + m[2] : ''; };
49
+ const short = (t, n = 60) => { const s = String(t || '').replace(/\s+/g, ' ').trim(); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
50
+ const slug = (t) => String(t || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32) || 'source';
51
+ const KIND_IC = { 'pmos-level': 'init', dashboard: 'report', chart: 'report', document: 'doc', 'past-report': 'cal', connector: 'report', link: 'sheet', upload: 'drop', person: 'user', chrome: 'out', computer: 'term' };
52
+ const STATE_TAG = { reached: ['ok', 'Reached'], 'not-connected': ['', 'Not connected'], later: ['', 'First cycle: nothing to read'], 'needs-you': ['bad', 'Needs you'], off: ['', 'Not connected'] };
53
+ const STEP_TAG = { works: ['check', 'ok', 'Works'], 'needs-you': ['alert', 'bad', 'Needs you'], waiting: ['clock', '', 'Waiting'] };
54
+
55
+ /* the options of Add a source: [kind, icon, label, what it does, form fields]; a field is [key, label, placeholder, required] */
56
+ const PMOS_OPTS = [
57
+ ['pmos-level', 'init', 'A portfolio, board or initiative', 'Sets what the report covers: its initiatives, releases, key results and risks', [['name', 'Which one?', 'Its name, for example the MySPH board', true]]],
58
+ ['dashboard', 'report', 'A dashboard', 'Brings in its charts, so a number is defined once', [['name', 'Which dashboard?', 'Its name, for example MySPH health', true]]],
59
+ ['chart', 'report', 'A chart', 'One chart from Reports, Charts', [['name', 'Which chart?', 'Its name, for example Release frequency', true]]],
60
+ ['document', 'doc', 'A document', 'Context for the notes, like the strategy or the decision log. Not numbers', [['name', 'Which document?', 'Its name, for example MySPH strategy 2026', true]]],
61
+ ['past-report', 'cal', 'A past report', 'Last cycle\'s numbers, and what it said that needs a follow-up', []],
62
+ ];
63
+ const OUT_OPTS = [
64
+ ['connector', 'report', 'A connector you have', 'For example Google Analytics through Supermetrics, Google Drive or Jira. Read only, through your own Claude connectors', [['name', 'Which service?', 'For example Google Analytics 4: MySPH news site', true], ['via', 'Through which connector?', 'For example Supermetrics', false], ['ref', 'Which account, property or board?', 'Optional', false]]],
65
+ ['link', 'sheet', 'A link', 'A Google Sheet or a Confluence page. PMOS reads it through your connectors', [['name', 'What is it?', 'For example Subscription revenue 2026', true], ['ref', 'Link', 'https://…', true]]],
66
+ ['upload', 'drop', 'A file you upload each cycle', 'CSV or .xlsx. PMOS asks for it in Work when the draft is due', [['name', 'What is the file?', 'For example Partner payouts', true]]],
67
+ ['person', 'user', 'A person who gives a number each cycle', 'It shows in their Work, for example: Lina Kusuma gives the NPS score by the 3rd', [['name', 'Who?', 'Their name', true], ['use', 'Which number?', 'For example the NPS score', false]]],
68
+ ['chrome', 'out', 'A page you open in Chrome', 'For a site with no connector, like Looker Studio or an admin page. Claude in Chrome reads it on your Mac, signed in as you', [['name', 'Which page?', 'For example Looker Studio: Paid acquisition', true], ['ref', 'Link', 'https://…', true]]],
69
+ ['computer', 'term', 'An app on your Mac', 'Computer use, when no connector and no web page has the numbers', [['name', 'Which app and screen?', 'For example Finance Desk, Monthly revenue', true]]],
70
+ ];
71
+
72
+ /* one studio session per report setup: what the PM is looking at waits here between openings */
73
+ const LOCAL = {};
74
+ const local = (id) => LOCAL[id] || (LOCAL[id] = { view: 'plan', ver: null, open: null, panel: false, form: null, text: '', check: null });
75
+
76
+ /* ── the studio ── */
77
+ export function openStructure({ ws, id, name, view, onClose } = {}) {
78
+ ensureDeckCss(); ensureStructureCss();
79
+ const K = local(id); if (view) K.view = view;
80
+ const S = { D: null, setup: null, cache: {}, dry: null, charts: null, err: '', timer: null, closed: false, busy: false, shown: '' };
81
+ const body = el('div', { class: 'brief rs rd-dk rd-dks-wrap rs-st', 'data-st-studio': id });
82
+ const ui = openSheet({ title: 'Report studio', icon: 'spark', wide: true, dw: true, cls: 'rd-wide', body, onClose: () => { S.closed = true; clearTimeout(S.timer); if (onClose) onClose(); } });
83
+
84
+ const ver = (n) => ((S.D && S.D.versions) || []).find((v) => v.n === n);
85
+ const cur = () => (S.D ? S.D.current : null);
86
+ const planOf = (n) => (n == null ? null : n === cur() ? S.D.plan : (S.cache[n] || null));
87
+ const planShown = () => planOf(K.ver);
88
+ const vlabel = (v) => (v ? (v.turn ? short(v.turn, 48) : v.n === 1 ? 'From your description' : 'Your turn') : '');
89
+ const running = () => !!(S.D && S.D.running);
90
+ const dryRunning = () => running() && /dry/.test(JSON.stringify(S.D.job || ''));
91
+ const checksRunning = () => Object.values((S.D && S.D.checks) || {}).some((c) => c && c.running);
92
+ const scrollLog = () => requestAnimationFrame(() => { const l = body.querySelector('.rd-dks-log'); if (l) l.scrollTop = l.scrollHeight; });
93
+
94
+ /* follow: show the current version when it changed (a job landed). A read that changed nothing paints nothing: the poll reads the structure
95
+ * every 1.5 seconds while a job runs, and a repaint would rebuild the conversation and the box the PM types in. */
96
+ async function load(follow) {
97
+ const r = await RSX.structure(ws, id);
98
+ if (!r || r.ok === false) { S.err = (r && (r.error || r.reason)) || 'The structure could not be read.'; S.D = null; paint(true); return; }
99
+ S.err = '';
100
+ const prev = S.D ? S.D.current : null;
101
+ S.D = r.structure; S.setup = r.setup || { id, name };
102
+ if (K.ver == null || (S.D.current != null && !ver(K.ver)) || (follow && S.D.current !== prev && S.D.current != null)) K.ver = S.D.current;
103
+ if (running() || checksRunning()) poll(); else clearTimeout(S.timer);
104
+ await Promise.all([need(K.ver), need(K.ver > 1 ? K.ver - 1 : null), readDry(), readCharts()]);
105
+ paint(false);
106
+ }
107
+ async function need(n) {
108
+ if (n == null || !S.D || n === cur() || S.cache[n]) return;
109
+ const r = await RSX.version(ws, id, n);
110
+ if (r && r.ok && r.plan) S.cache[n] = r.plan;
111
+ }
112
+ async function readDry() {
113
+ const d = S.D && S.D.dry;
114
+ if (!d || !d.run) { S.dry = null; return; }
115
+ if (S.dry && S.dry.run === d.run) return;
116
+ const r = await RSX.dry(ws, id, d.run);
117
+ S.dry = r && r.ok && r.dry ? { run: d.run, ...r.dry } : null;
118
+ }
119
+ async function readCharts() {
120
+ const p = planShown();
121
+ if (S.charts || !p || !(p.charts || []).some((c) => c.chart)) return;
122
+ const r = await RSX.charts(ws);
123
+ if (r && r.ok) S.charts = r.charts || [];
124
+ }
125
+ function poll() {
126
+ clearTimeout(S.timer);
127
+ S.timer = setTimeout(async () => { if (S.closed) return; const was = cur(); await load(true); if (S.D && !running() && cur() !== was) scrollLog(); }, 1500);
128
+ }
129
+
130
+ /* ── the conversation ── */
131
+ function msg(l) {
132
+ const you = l.who === 'pm';
133
+ const v = l.n != null ? ver(l.n) : null;
134
+ const qs = (l.questions || []).map((x) => el('div', { class: 'rs-st-q', 'data-st-question': '' }, el('b', null, x.q),
135
+ el('div', { class: 'chips' }, (x.chips || []).map((c) => lk({ 'data-st-act': 'chip', 'data-v': c, onclick: () => pick(c) }, c)))));
136
+ const opts = (l.options || []).length ? lk({ 'data-st-act': 'options', onclick: () => { K.view = 'options'; if (l.n != null) K.ver = l.n; need(K.ver).then(() => paint(true)); } }, icon('cols', 'sm'), 'See the three structures side by side') : null;
137
+ return el('li', { class: 'rd-dks-msg ' + (you ? 'you' : 'pmos'), 'data-st-log': you ? 'you' : 'pmos' },
138
+ el('span', { class: 'ds-av ' + (you ? 'h' : 'a') }, you ? icon('user', 'sm') : icon('spark', 'sm')),
139
+ el('div', { class: 'bub' }, el('div', { class: 'who' }, you ? 'You' : 'PMOS'), l.text ? el('p', null, l.text) : null, qs, opts,
140
+ v ? lk({ class: 'rd-dks-ver', 'data-st-act': 'ver', 'data-v': String(v.n), 'aria-pressed': String(K.ver === v.n && K.view !== 'options'), onclick: () => look(v.n) }, icon('todo', 'sm'), 'S' + v.n + ': ' + vlabel(v)) : null));
141
+ }
142
+ function runningMsg() {
143
+ const n = (S.D.versions.length ? Math.max(...S.D.versions.map((v) => v.n)) : 0) + 1;
144
+ return el('li', { class: 'rd-dks-msg pmos', 'data-st-running': '' }, el('span', { class: 'ds-av a' }, icon('spark', 'sm')),
145
+ el('div', { class: 'bub' }, el('div', { class: 'who' }, 'PMOS'), el('p', null, dryRunning() ? 'Running the plan on the last complete period. This takes a minute or two. You can close the studio; the result shows here when the job ends.'
146
+ : 'Working on S' + n + '. This takes a minute or two. You can close the studio; the version lands here when the job ends.')));
147
+ }
148
+ function errorMsg() {
149
+ const lastAt = ((S.D.log || [])[(S.D.log || []).length - 1] || {}).at || '';
150
+ const e = (S.D.errors || []).filter((x) => String(x.at || '') >= lastAt).pop();
151
+ if (!e || running()) return null;
152
+ return el('li', { class: 'rd-dks-msg pmos err', 'data-st-error': '' }, el('span', { class: 'ds-av w' }, icon('alert', 'sm')), el('div', { class: 'bub' }, el('div', { class: 'who' }, 'PMOS'), el('p', null, 'That did not work: ' + e.text + ' Nothing changed. Try the turn again.')));
153
+ }
154
+ function pick(c) { K.text = (K.text.trim() ? K.text.trim() + ' ' : '') + c + '.'; paint(true); const i = body.querySelector('[data-st-in]'); if (i) { i.focus(); i.setSelectionRange(i.value.length, i.value.length); } }
155
+ function panel() {
156
+ if (!K.panel) return null;
157
+ const opt = ([kind, ic, label, desc, fields]) => btn({ class: 'rd-dks-opt', 'data-st-act': 'opt', 'data-v': kind, 'aria-expanded': String(K.form === kind), onclick: () => { if (!fields.length) { addSource(kind, label, {}, desc); return; } K.form = K.form === kind ? null : kind; paint(true); } },
158
+ icon(ic, 'sm'), el('span', null, label, el('br'), el('span', { class: 'dim rs-st-small' }, desc)));
159
+ const form = (o) => {
160
+ const [kind, , label, desc, fields] = o; const inputs = fields.map(([k, lab, ph, req]) => el('label', { class: 'rs-st-field' }, el('span', { class: 'dim' }, lab + (req ? '' : ' (optional)')),
161
+ el('input', { type: 'text', 'data-st-field': k, placeholder: ph, autocomplete: 'off', onKeyDown: (e) => { if (e.key === 'Enter') submit(); } })));
162
+ const submit = () => {
163
+ const v = {}; let bad = null;
164
+ for (const [k, , , req] of fields) { const i = form_.querySelector('[data-st-field="' + k + '"]'); v[k] = i ? i.value.trim() : ''; if (req && !v[k] && !bad) bad = i; }
165
+ if (bad) { bad.focus(); return; }
166
+ if ((kind === 'link' || kind === 'chrome') && !/^https?:\/\//i.test(v.ref)) { toast({ text: 'A link starts with https://', tone: 'error' }); return; }
167
+ if (['pmos-level', 'dashboard', 'chart', 'document'].includes(kind)) v.ref = v.name;
168
+ addSource(kind, label, v, desc);
169
+ };
170
+ const form_ = el('div', { class: 'rs-st-form', 'data-st-form': kind }, ...inputs, el('div', { class: 'ds-acts' }, pbtn('primary', 'Add', 'plus', { 'data-st-act': 'add', onclick: submit }), pbtn('ghost', 'Cancel', 'x', { onclick: () => { K.form = null; paint(true); } })));
171
+ return form_;
172
+ };
173
+ const group = (opts) => opts.flatMap((o) => [opt(o), K.form === o[0] ? form(o) : null]);
174
+ return el('div', { class: 'rd-dks-panel', 'data-st-panel': '' }, el('h3', { class: 'ds-h' }, 'From PMOS'), ...group(PMOS_OPTS),
175
+ el('h3', { class: 'ds-h' }, 'Outside PMOS'), ...group(OUT_OPTS),
176
+ el('div', { class: 'dim rs-st-small' }, 'PMOS only reads. It never writes to a source, and a connector you have not allowed stays closed.'));
177
+ }
178
+ function chat() {
179
+ const D = S.D; const p = planOf(cur());
180
+ const reached = ((p && p.sources) || []).filter((s) => s.state === 'reached');
181
+ const first = !D.log.length;
182
+ const ta = el('textarea', { 'data-st-in': '', 'aria-label': 'Tell PMOS what to change', placeholder: first ? 'Say what the report is for, who reads it and what they decide with it.' : 'Talk it through, for example: split new users by campaign, or what would leadership miss here?',
183
+ onInput: (e) => { K.text = e.target.value; }, onKeyDown: (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); send(); } } });
184
+ ta.value = K.text;
185
+ return el('div', { class: 'rd-dks-chat' }, el('div', { class: 'rd-dks-desc' }, el('span', { class: 'dim mono' }, 'WHAT IT ANSWERS, WHERE THE NUMBERS COME FROM, HOW'),
186
+ reached.length ? el('div', { class: 'chips' }, reached.map((s) => chip(icon(KIND_IC[s.kind] || 'doc', 'sm'), s.name))) : null),
187
+ el('ul', { class: 'rd-dks-log', 'data-st-chat': '' }, (D.log || []).map(msg), D.running ? runningMsg() : null, errorMsg(),
188
+ first && !D.running ? el('li', { class: 'fold' }, icon('info', 'sm'), ' Start with what the report is for and who reads it. PMOS asks what it needs, then proposes a structure.') : null),
189
+ panel(),
190
+ el('div', { class: 'rd-dks-in' }, ta,
191
+ el('div', { class: 'ds-acts rd-between' }, btn({ class: 'ds-btn', 'data-st-act': 'panel', 'aria-expanded': String(K.panel), onclick: () => { K.panel = !K.panel; if (!K.panel) K.form = null; paint(true); } }, icon('plus', 'sm'), 'Add a source'),
192
+ btn({ class: 'ds-btn ag', 'data-st-act': 'send', disabled: !!D.running || S.busy, onclick: () => send() }, icon('spark', 'sm'), first ? 'Make the first version' : 'Make the next version')),
193
+ el('div', { class: 'dim rs-st-small' }, 'Ask anything: which chart fits, what a number means, what is missing. A turn that changes the plan makes a new version.')));
194
+ }
195
+ async function send(textIn) {
196
+ const i = body.querySelector('[data-st-in]');
197
+ const text = String(textIn != null ? textIn : (i ? i.value : K.text) || '').trim();
198
+ if (!text) { if (i) i.focus(); return false; }
199
+ if (running()) { toast('A version is being made. Wait for it to land.'); return false; }
200
+ S.busy = true; paint(true);
201
+ const r = await RSX.turn({ ws, id, text });
202
+ S.busy = false;
203
+ if (!r || !r.ok) { paint(true); if (r && (r.status === 409 || /running|already/i.test(String(r.error || '')))) toast('A version is being made. Wait for it to land.'); else failed(r, 'PMOS did not start'); return false; }
204
+ K.text = ''; K.view = 'plan';
205
+ await load(true); scrollLog();
206
+ return true;
207
+ }
208
+ async function addSource(kind, label, v, desc) {
209
+ const p = planOf(cur()); const base = slug(v.name || label); const have = new Set(((p && p.sources) || []).map((s) => s.id));
210
+ let sid = base; for (let k = 2; have.has(sid); k++) sid = base + '-' + k;
211
+ const source = { id: sid, kind, name: String(v.name || label).slice(0, 200), use: String(v.use || desc).slice(0, 600) };
212
+ if (v.via) source.via = v.via; if (v.ref) source.ref = v.ref;
213
+ S.busy = true;
214
+ const r = await RSX.source({ ws, id, source });
215
+ S.busy = false;
216
+ if (!r || !r.ok) { failed(r, 'The source was not added'); paint(true); return; }
217
+ K.panel = false; K.form = null; K.view = 'plan';
218
+ if (kind === 'chrome' || kind === 'computer') K.check = sid;
219
+ S.charts = null; await load(true); scrollLog();
220
+ toast(kind === 'chrome' || kind === 'computer' ? 'Added. PMOS is checking it step by step.' : 'Added as a source. It is in the next version.');
221
+ }
222
+
223
+ /* ── the result ── */
224
+ function look(n) { K.ver = n; K.view = 'plan'; K.open = null; need(n).then(() => need(n > 1 ? n - 1 : null)).then(() => paint(true)); }
225
+ async function revert(n) {
226
+ const r = await RSX.revert({ ws, id, n }); if (!r || !r.ok) { failed(r, 'It did not go back'); return; }
227
+ await load(true); toast('Back to S' + n + '. The next turn builds on it. The later versions stay in the strip.');
228
+ }
229
+ async function accept() {
230
+ const n = K.ver != null ? K.ver : cur(); if (n == null) { toast('Make a version first.'); return; }
231
+ S.busy = true; paint(true);
232
+ const r = await RSX.accept({ ws, id, n }); S.busy = false;
233
+ if (!r || !r.ok) { paint(true); failed(r, 'The structure was not saved'); return; }
234
+ const made = (r.charts || []).length;
235
+ await load(true);
236
+ toast('Saved S' + n + ' as the structure of this report.' + (made ? ' ' + made + (made === 1 ? ' new chart is' : ' new charts are') + ' in Reports, Charts.' : '') + ' Next: Design how it looks. Nothing is sent.');
237
+ const m = await import('./c3-design-studio.js');
238
+ m.openBrief({ ws, id, name: (S.setup && S.setup.name) || name });
239
+ }
240
+ async function drop(kind, ref, what) {
241
+ const r = await RSX.remove({ ws, id, kind, ref }); if (!r || !r.ok) { failed(r, 'It was not removed'); return; }
242
+ K.open = null; S.charts = null; await load(true); scrollLog(); toast('Removed ' + what + '. That makes the next version; the old one stays in the strip.');
243
+ }
244
+ function discuss(c) {
245
+ K.text = 'About ' + c.name + ': '; paint(true);
246
+ const i = body.querySelector('[data-st-in]'); if (i) { i.focus(); i.setSelectionRange(i.value.length, i.value.length); }
247
+ }
248
+ async function check(sid) {
249
+ K.check = sid; const r = await RSX.check({ ws, id, source: sid});
250
+ if (!r || !r.ok) { failed(r, 'The check did not start'); paint(true); return; }
251
+ await load(true);
252
+ }
253
+ async function dryRun() {
254
+ const r = await RSX.runDry({ ws, id, n: K.ver }); if (!r || !r.ok) { failed(r, 'The test did not start'); return; }
255
+ S.dry = null; await load(true);
256
+ }
257
+ async function testPull(s) {
258
+ const r = await RSX.turn({ ws, id, text: 'Test pull from ' + s.name + ': read it now for the last complete period and tell me which charts that use it returned a number.' });
259
+ if (!r || !r.ok) { if (r && r.status === 409) toast('A version is being made. Wait for it to land.'); else failed(r, 'The test pull did not start'); return; }
260
+ toast('PMOS is reading ' + s.name + '. The answer shows in the conversation.'); await load(true); scrollLog();
261
+ }
262
+ function openIt(s) {
263
+ closeSheet();
264
+ if (s.kind === 'dashboard') goTo({ tab: 'rep', view: 'dash' }); else if (s.kind === 'chart') goTo({ tab: 'rep', view: 'charts' }); else if (s.kind === 'past-report') goTo({ tab: 'rep', view: 'reports' }); else goTo({ tab: 'init' });
265
+ }
266
+
267
+ function purposeSect(p) {
268
+ const u = p.purpose || {};
269
+ return sect({ ic: 'target', title: 'What it answers', attrs: { 'data-st-purpose': '' } }, kv([['Readers', u.readers || ''], ['Decision', u.decision || ''], ['Scope', u.scope || ''], ['Every', u.cadence || '']]),
270
+ (u.questions || []).length ? el('ul', { class: 'chgl' }, u.questions.map((t) => el('li', null, icon('target', 'sm'), el('span', null, t), el('span')))) : null);
271
+ }
272
+ function srcRow(s, p) {
273
+ const uses = (p.charts || []).filter((c) => c.source === s.id).length; const pm = s.group === 'pmos';
274
+ const [tone, word] = STATE_TAG[s.state] || ['', s.state];
275
+ const eye = s.kind === 'chrome' || s.kind === 'computer';
276
+ const act = pm ? pbtn('ghost', 'Open', 'out', { 'data-st-act': 'openpm', 'data-v': s.id, onclick: () => openIt(s) })
277
+ : eye ? pbtn('ghost', 'Check it works', 'shield', { 'data-st-act': 'chkopen', 'data-v': s.id, onclick: () => check(s.id) })
278
+ : s.state === 'reached' ? pbtn('ghost', 'Test pull', 'play', { 'data-st-act': 'test', 'data-v': s.id, onclick: () => testPull(s) })
279
+ : pbtn('ghost', 'How to connect', 'info', { 'data-st-act': 'connect', 'data-v': s.id, onclick: () => toast('Connect ' + s.name + ' in your Claude connectors (Settings, Connectors), then come back and press Test pull. PMOS reads it read only.') });
280
+ const li = el('li', { class: 'nrow', 'data-st-src': s.id }, el('span', { class: 'ki' }, icon(KIND_IC[s.kind] || 'doc')),
281
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, s.name),
282
+ el('div', { class: 'chips' }, el('span', { class: 'tag' + (tone ? ' ' + tone : ''), 'data-st-state': s.state }, word), s.via ? chip(icon(s.state === 'reached' ? 'lock' : 'info', 'sm'), s.via) : null,
283
+ uses ? chip(icon('report', 'sm'), uses + (uses === 1 ? ' chart' : ' charts')) : null, eye ? chip(icon('clock', 'sm'), 'Re-checked 1 hour before each draft') : null),
284
+ s.use ? el('div', { class: 'dim rs-st-small' }, s.use) : null),
285
+ el('div', { class: 'acts' }, act, pbtn('ghost', 'Remove', 'trash', { 'data-st-act': 'dropsrc', 'data-v': s.id, onclick: () => drop('source', s.id, s.name) })));
286
+ return li;
287
+ }
288
+ function srcSect(p) {
289
+ const all = p.sources || []; const grp = (g) => all.filter((s) => s.group === g);
290
+ const list = (xs) => (xs.length ? el('ul', { class: 'list' }, xs.map((s) => srcRow(s, p))) : el('div', { class: 'fold' }, icon('info', 'sm'), ' Nothing here yet.'));
291
+ return sect({ ic: 'drop', title: 'Sources', count: all.length, attrs: { 'data-st-sources': '' } }, el('h3', { class: 'ds-h', 'data-st-group': 'pmos' }, 'From PMOS'), list(grp('pmos')),
292
+ el('h3', { class: 'ds-h', 'data-st-group': 'outside' }, 'Outside PMOS'), list(grp('outside')),
293
+ el('div', { class: 'ds-acts' }, pbtn('outline', 'Add a source', 'plus', { 'data-st-act': 'panel', onclick: () => { K.panel = true; paint(true); const pn = body.querySelector('[data-st-panel]'); if (pn) pn.scrollIntoView({ block: 'nearest' }); } })));
294
+ }
295
+ function usersOf(c) {
296
+ const m = (S.charts || []).find((x) => x.id === c.chart); const us = (m && m.users) || [];
297
+ return c.chart ? (us.length ? 'Charts, also used in ' + us.map((u) => (u.kind === 'dashboard' ? 'dashboard ' : 'report ') + u.name).join(', ') + ', and this report' : 'Charts, and this report') : 'This report. Saved in Charts when you use this structure';
298
+ }
299
+ function chartDetail(c, p) {
300
+ const dv = S.dry && S.dry.n === K.ver ? (S.dry.values || []).find((v) => v.chart === c.id) : null;
301
+ const tp = dv ? [el('b', null, dv.value), ' ', el('span', { class: 'dim' }, dv.was || ''), ' ', dv.delta ? el('span', { class: 'chip2 mono' }, dv.delta) : null, ' ', dv.flag ? el('span', { class: 'tag warn' }, 'Flagged') : null]
302
+ : ['Not tried on this version. Press Try it with real data.'];
303
+ const f = (lab, ...c2) => el('div', null, el('span', { class: 'dim mono' }, lab), ...c2);
304
+ return el('tr', { class: 'rs-st-det', 'data-st-detail': c.id }, el('td', { colspan: '5' }, el('div', { class: 'rs-st-detb' },
305
+ f('QUESTION IT ANSWERS', el('p', { 'data-st-q': '' }, c.question || '')), f('USED IN', el('p', { 'data-st-used': '' }, usersOf(c))),
306
+ f('EXACTLY WHAT PMOS READS', el('p', { class: 'mono rs-st-code', 'data-st-query': '' }, c.query || '')), f('WHY PMOS PROPOSED IT', el('p', { 'data-st-why': '' }, c.why || '')),
307
+ f('TEST PULL', el('p', { 'data-st-testpull': '' }, ...tp)),
308
+ el('div', { class: 'ds-acts' }, pbtn('outline', 'Discuss this chart', 'mail', { 'data-st-act': 'discuss', 'data-v': c.id, onclick: () => discuss(c) }),
309
+ c.chart ? pbtn('ghost', 'Open in Charts', 'out', { 'data-st-act': 'opencharts', onclick: () => { closeSheet(); goTo({ tab: 'rep', view: 'charts' }); } }) : null,
310
+ pbtn('ghost', 'Remove it', 'trash', { 'data-st-act': 'drop', 'data-v': c.id, onclick: () => drop('chart', c.id, c.name) })))));
311
+ }
312
+ function chartsSect(p) {
313
+ const cs = p.charts || []; const nm = (id) => { const s = (p.sources || []).find((x) => x.id === id); return s ? s.name.split(':')[0] : ''; };
314
+ const rows = cs.flatMap((c) => [el('tr', { class: 'rs-st-row' + (K.open === c.id ? ' on' : ''), 'data-st-chart': c.id },
315
+ el('td', null, btn({ class: 'tt', 'data-st-act': 'chart', 'data-v': c.id, 'aria-expanded': String(K.open === c.id), onclick: () => { K.open = K.open === c.id ? null : c.id; paint(true); } }, c.name),
316
+ el('div', null, c.chart ? el('span', { class: 'tag', 'data-st-tag': 'from' }, 'From Charts') : el('span', { class: 'tag acc', 'data-st-tag': 'new' }, 'New chart'))),
317
+ el('td', null, c.from || '', el('div', { class: 'dim rs-st-tiny' }, nm(c.source))), el('td', null, c.how || ''), el('td', null, c.compare || ''), el('td', null, c.flag || '')),
318
+ K.open === c.id ? chartDetail(c, p) : null]);
319
+ const nNew = cs.filter((c) => !c.chart).length;
320
+ return sect({ ic: 'report', title: 'Charts', count: cs.length, attrs: { 'data-st-metrics': '' }, right: el('span', { class: 'dim mono rs-st-tiny' }, nNew + ' new, ' + (cs.length - nNew) + ' from Charts') },
321
+ cs.length ? el('div', { class: 'rs-st-tw' }, el('table', { class: 'tbl rs-st-tbl' }, el('thead', null, el('tr', null, ['Chart', 'From', 'How it is worked out', 'Compared with', 'Flag when'].map((h) => el('th', null, h)))), el('tbody', null, rows)))
322
+ : el('div', { class: 'fold' }, icon('info', 'sm'), ' No chart yet. Tell PMOS what the report must answer.'),
323
+ el('div', { class: 'dim rs-st-small' }, 'Each number is a chart: one question, its data and its logic. "Use this structure" saves the new ones in Reports, Charts, where any report or dashboard can use them. A chart from Charts is defined there once; changing it changes it everywhere it is used. Open one to see exactly what PMOS reads.'));
324
+ }
325
+ function outlineSect(p) {
326
+ const os = p.outline || []; const prev = K.ver > 1 ? planOf(K.ver - 1) : null; const prevIds = prev ? new Set((prev.outline || []).map((o) => o.id)) : null;
327
+ const nm = (id) => { const c = (p.charts || []).find((x) => x.id === id); return c ? c.name : id; };
328
+ return sect({ ic: 'todo', title: 'Outline', count: os.length, attrs: { 'data-st-outline': '' } }, el('ol', { class: 'rs-st-ol' }, os.map((o, i) => el('li', { 'data-st-section': o.id },
329
+ el('span', { class: 'rs-st-n mono' }, String(i + 1)), el('div', null, el('b', null, o.title), o.fixed ? [' ', el('span', { class: 'tag' }, 'In every report')] : null, prevIds && !prevIds.has(o.id) ? [' ', el('span', { class: 'tag acc' }, 'New in S' + K.ver)] : null,
330
+ o.notes ? el('div', { class: 'dim rs-st-small' }, o.notes) : null,
331
+ el('div', { class: 'chips' }, (o.charts || []).map((cid) => chip(icon('report', 'sm'), nm(cid))), o.fixed ? null : chip(icon('spark', 'sm'), 'PMOS notes'), o.show ? chip(icon('img', 'sm'), o.show) : null))))),
332
+ el('div', { class: 'dim rs-st-small' }, 'Each section places charts and the notes PMOS writes about them. Headline, Needs your call and Sources stay in every report; everything between them is yours. How each part looks is the Design step.'));
333
+ }
334
+ function checkSect(sid, p) {
335
+ const s = ((p && p.sources) || []).find((x) => x.id === sid); const c = (S.D.checks || {})[sid];
336
+ const kind = (c && c.kind) || (s && s.kind) || 'chrome'; const chrome = kind === 'chrome'; const nm = s ? s.name : sid;
337
+ const steps = (c && c.steps) || []; const bad = steps.filter((x) => x.state === 'needs-you').length;
338
+ const rows = steps.map((x) => { const [ic, , word] = STEP_TAG[x.state] || STEP_TAG.waiting; const tone = (STEP_TAG[x.state] || STEP_TAG.waiting)[1];
339
+ return el('li', { 'data-st-step': x.id, 'data-st-step-state': x.state }, icon(ic, 'sm'), el('span', null, el('b', null, x.label), el('br'), el('span', { class: 'dim rs-st-small' }, x.reason || '')), el('span', { class: 'tag' + (tone ? ' ' + tone : '') }, word)); });
340
+ const right = !c ? el('span', { class: 'tag' }, 'Not checked') : c.running ? el('span', { class: 'tag', 'data-st-check-state': 'running' }, 'Checking') : c.ok ? el('span', { class: 'tag ok', 'data-st-check-state': 'ok' }, 'All steps work') : el('span', { class: 'tag bad', 'data-st-check-state': 'fail' }, bad + (bad === 1 ? ' step needs you' : ' steps need you'));
341
+ return sect({ ic: 'shield', title: 'Check: ' + nm, right, attrs: { 'data-st-check': sid } },
342
+ rows.length ? el('ul', { class: 'chgl rs-st-chk' }, rows) : el('div', { class: 'fold', role: 'status' }, icon('clock', 'sm'), c && c.running ? ' Starting the check.' : ' Not checked yet. Press Check again to go through each step.'),
343
+ el('h3', { class: 'ds-h' }, 'What the session may do'),
344
+ el('ul', { class: 'chgl' }, el('li', null, icon('lock', 'sm'), el('span', null, chrome ? 'Only the browser tools: open the page, read it, scroll, set the date range. It cannot submit a form or change a setting, and it closes the tab when it is done.'
345
+ : 'Open the app, read the screen, and nothing else. It takes over the mouse while it reads, so PMOS runs it only when you have been away for 15 minutes, or asks you first. It is used only when your own Claude Code settings allow computer use.'), el('span'))),
346
+ el('h3', { class: 'ds-h' }, 'When a cycle runs'),
347
+ el('ul', { class: 'chgl' }, el('li', null, icon('clock', 'sm'), el('span', null, 'PMOS runs this check again 1 hour before the draft.'), el('span')),
348
+ el('li', null, icon('alert', 'sm'), el('span', null, 'If your Mac is asleep, ' + (chrome ? 'Chrome is closed or the sign-in has expired' : 'the app is closed or macOS blocks it') + ', Work shows what to do. The draft waits up to 4 hours, then goes ahead without this source and says so under Sources. It never guesses a number.'), el('span'))),
349
+ el('div', { class: 'ds-acts' }, pbtn(c && c.ok ? 'outline' : 'primary', 'Check again', 'undo', { 'data-st-act': 'chkagain', 'data-v': sid, disabled: !!(c && c.running), onclick: () => check(sid) })));
350
+ }
351
+ function planView() {
352
+ const p = planShown(); const v = ver(K.ver);
353
+ if (!p) return [el('div', { class: 'fold', role: 'status' }, icon('clock', 'sm'), ' ' + (running() ? 'PMOS is making the first version.' : S.D.versions.length ? 'Reading S' + K.ver + '.' : 'No version yet. Tell PMOS what the report is for on the left, then press Make the first version.'), ' ')];
354
+ const ids = [...new Set([K.check, ...Object.entries(S.D.checks || {}).filter(([, c]) => c && c.running).map(([k]) => k)].filter(Boolean))].filter((sid) => (p.sources || []).some((s) => s.id === sid));
355
+ return [el('div', { class: 'dim rs-st-small', 'data-st-vcap': String(K.ver) }, el('b', { class: 'rs-st-fg' }, 'S' + K.ver), ' · ' + vlabel(v) + (v && v.at ? ' · made ' + hhmm(v.at) : '') + '.' + (K.ver !== cur() ? ' You are looking at an older version.' : '')),
356
+ purposeSect(p), ...ids.map((sid) => checkSect(sid, p)), srcSect(p), chartsSect(p), outlineSect(p)];
357
+ }
358
+ function optionsView() {
359
+ let os = (planShown() || {}).options || [];
360
+ if (!os.length) { const l = [...(S.D.log || [])].reverse().find((x) => (x.options || []).length); os = l ? l.options : []; }
361
+ os = [...os].sort((a, b) => (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0));
362
+ if (!os.length) return [el('div', { class: 'fold', 'data-st-nooptions': '' }, icon('info', 'sm'), ' PMOS has not proposed other structures yet. Ask for another angle in the chat.')];
363
+ return [el('div', { class: 'rs-st-opts', 'data-st-options': '' }, os.map((o) => el('section', { class: 'card sect rs-st-opt', 'data-st-option': o.name },
364
+ el('div', { class: 'sh2' }, el('h2', null, icon('todo', 'sm'), o.name), o.recommended ? el('span', { class: 'tag acc' }, 'Recommended') : null),
365
+ el('ol', { class: 'rs-st-mini' }, (o.sections || []).map((s) => el('li', null, s))),
366
+ el('div', null, el('span', { class: 'dim mono' }, 'GOOD FOR'), el('p', null, o.good || '')), el('div', null, el('span', { class: 'dim mono' }, 'MISSES'), el('p', null, o.misses || '')),
367
+ el('div', { class: 'ds-acts' }, pbtn(o.recommended ? 'primary' : 'outline', 'Use this instead', 'next', { 'data-st-act': 'choose', 'data-v': o.name, onclick: () => { K.text = 'Use the "' + o.name + '" structure instead.'; K.view = 'plan'; paint(true); const i = body.querySelector('[data-st-in]'); if (i) i.focus(); } }))))),
368
+ el('div', { class: 'dim rs-st-small' }, 'PMOS shaped these from your answers: readers, the decision, and what must be in it. Ask for another angle in the chat.')];
369
+ }
370
+ function dryView() {
371
+ const p = planShown() || {}; const d = S.dry; const isNow = d && d.n === K.ver;
372
+ const run = pbtn(d ? 'outline' : 'primary', d ? 'Run it again' : 'Try it on the last complete period', 'play', { 'data-st-act': 'dryrun', disabled: running() || !planShown(), onclick: () => dryRun() });
373
+ if (dryRunning()) return [el('div', { class: 'fold a', role: 'status', 'data-st-dryrunning': '' }, icon('clock', 'sm'), ' PMOS is running the plan on the last complete period. Nothing is saved.')];
374
+ if (!d) return [el('div', { class: 'fold', 'data-st-nodry': '' }, icon('info', 'sm'), ' Run the plan on the last complete period before any design: the headline PMOS would write, each chart\'s value, the flags, and what it could not get. It reads only and saves nothing in the report.'), el('div', { class: 'ds-acts' }, run)];
375
+ if (S.D.dry && S.D.dry.ok === false) return [el('div', { class: 'fold w', role: 'alert', 'data-st-dryfail': '' }, icon('alert', 'sm'), ' The test did not finish. Nothing was saved. Try it again.'), el('div', { class: 'ds-acts' }, run)];
376
+ const nm = (cid) => { const c = (p.charts || []).find((x) => x.id === cid); return c ? c.name : cid; };
377
+ const vs = d.values || []; const flags = vs.filter((x) => x.flag); const miss = d.missing || [];
378
+ const line = (x) => el('li', null, icon(x.flag ? 'flag' : 'check', 'sm'), el('span', null, el('b', null, nm(x.chart)), ': ' + (x.value || ''), ' ', el('span', { class: 'dim' }, x.was || ''), ' ', x.delta ? el('span', { class: 'chip2 mono' }, x.delta) : null), el('span', null, x.flag ? el('span', { class: 'tag warn' }, 'Flagged') : null));
379
+ const srcNm = (sid) => { const s = (p.sources || []).find((x) => x.id === sid); return s ? s.name : sid; };
380
+ return [el('div', { class: 'fold', 'data-st-dry': '' }, icon('info', 'sm'), ' The plan run on ' + (d.period || 'the last complete period') + ', before any design' + (isNow ? '' : ', for S' + d.n + ' (you are looking at S' + K.ver + ')') + '. Plain content only: this is what the draft would say. Read ' + hhmm(S.D.dry.at) + '; nothing was saved or sent.'),
381
+ sect({ ic: 'doc', title: 'Headline, as PMOS would write it', attrs: { 'data-st-dry-headline': '' } }, el('p', { class: 'rs-st-p' }, d.headline || '')),
382
+ sect({ ic: 'scale', title: 'Numbers', count: vs.length, attrs: { 'data-st-dry-values': '' } }, el('ul', { class: 'chgl' }, vs.map(line))),
383
+ sect({ ic: 'flag', title: 'Flags this period', count: flags.length, attrs: { 'data-st-dry-flags': '' } }, flags.length ? el('ul', { class: 'chgl' }, flags.map((x) => el('li', null, icon('flag', 'sm'), el('span', null, nm(x.chart) + ': ' + (x.note || 'flagged by its rule') + '.'), el('span', null, el('span', { class: 'tag warn' }, 'Flagged'))))) : el('div', { class: 'fold' }, icon('check', 'sm'), ' Nothing is flagged.')),
384
+ sect({ ic: 'alert', title: 'Could not get', count: miss.length, attrs: { 'data-st-dry-missing': '' } }, miss.length ? el('ul', { class: 'chgl' }, miss.map((x) => el('li', null, icon('alert', 'sm'), el('span', null, srcNm(x.source) + ': ' + (x.reason || '')), el('span')))) : el('div', { class: 'fold' }, icon('check', 'sm'), ' Every chart returned a number.')),
385
+ (d.options || []).length ? sect({ ic: 'spark', title: 'Options PMOS would propose', count: d.options.length, attrs: { 'data-st-dry-options': '' } }, el('ul', { class: 'chgl' }, d.options.map((t) => el('li', null, icon('spark', 'sm'), el('span', null, t), el('span')))), el('div', { class: 'dim rs-st-small' }, 'You pick or rewrite these before you issue. PMOS never decides them.')) : null,
386
+ el('div', { class: 'ds-acts' }, run)];
387
+ }
388
+ function outHTML() {
389
+ const D = S.D;
390
+ const strip = el('div', { class: 'rd-dks-vers' }, el('div', { class: 'seg', role: 'group', 'aria-label': 'Versions', 'data-st-versions': '' }, D.versions.map((v) => btn({ 'data-st-v': String(v.n), 'aria-pressed': String(K.view === 'plan' && K.ver === v.n), title: vlabel(v), onclick: () => look(v.n) }, 'S' + v.n, D.saved === v.n ? icon('check', 'sm') : null))),
391
+ el('div', { class: 'seg', role: 'group', 'aria-label': 'View' }, [['plan', 'todo', 'Plan'], ['options', 'cols', 'Other structures'], ['dry', 'play', 'Try it with real data']].map(([k, ic, l]) => btn({ 'data-st-view': k, 'aria-pressed': String(K.view === k), onclick: () => { K.view = k; paint(true); } }, icon(ic, 'sm'), l))),
392
+ K.ver != null && K.ver !== D.current ? pbtn('outline', 'Go back to S' + K.ver, 'undo', { 'data-st-act': 'revert', onclick: () => revert(K.ver) }) : null);
393
+ return [strip, ...(K.view === 'options' ? optionsView() : K.view === 'dry' ? dryView() : planView())];
394
+ }
395
+
396
+ /* ── painting ── */
397
+ function paint(force) {
398
+ if (S.closed) return;
399
+ if (!S.D) { put(body, S.err ? el('div', { class: 'fold w', role: 'alert', 'data-st-err': '' }, icon('alert', 'sm'), ' ' + S.err) : el('div', { class: 'fold', role: 'status' }, icon('clock', 'sm'), ' Reading the structure.')); return; }
400
+ const sig = JSON.stringify([S.D, S.setup, S.dry, S.charts, K.view, K.ver, K.open, K.panel, K.form, K.check, S.busy, Object.keys(S.cache), Object.keys(S.cache).map((k) => S.cache[k] && 1)]);
401
+ if (!force && sig === S.shown) return;
402
+ S.shown = sig;
403
+ const D = S.D; const nm = (S.setup && S.setup.name) || name || 'Report';
404
+ const sb = body.closest('.sb'); const y = sb ? sb.scrollTop : 0; const log = body.querySelector('.rd-dks-log'); const ly = log ? log.scrollTop : null;
405
+ const focused = document.activeElement && body.contains(document.activeElement) && document.activeElement.matches('[data-st-in],[data-st-field]') ? (document.activeElement.hasAttribute('data-st-in') ? '[data-st-in]' : '[data-st-field="' + document.activeElement.getAttribute('data-st-field') + '"]') : null;
406
+ const fvals = {}; body.querySelectorAll('[data-st-field]').forEach((i) => { fvals[i.getAttribute('data-st-field')] = i.value; });
407
+ put(body,
408
+ el('div', { class: 'rs-st-bar' }, el('div', { class: 'chips2 rd-wrap' },
409
+ el('div', { class: 'seg', role: 'group', 'aria-label': 'Studio step', 'data-st-steps': '' }, btn({ 'data-st-step-btn': 'structure', 'aria-pressed': 'true' }, icon('todo', 'sm'), '1 Structure'),
410
+ btn({ 'data-st-step-btn': 'design', 'aria-pressed': 'false', onclick: () => toDesign() }, icon('img', 'sm'), '2 Design')),
411
+ el('b', null, nm), D.saved != null ? el('span', { class: 'chip2 ds-good', 'data-st-saved': String(D.saved) }, icon('check', 'sm'), 'Saved structure: S' + D.saved) : el('span', { class: 'chip2', 'data-st-saved': '' }, 'Not saved yet')),
412
+ el('div', { class: 'chips2' }, pbtn('primary', 'Use this structure', 'check', { 'data-st-act': 'save', disabled: !D.versions.length || S.busy, onclick: () => accept() }))),
413
+ el('div', { class: 'fold' }, icon('info', 'sm'), ' Decide what the report answers, where each number comes from and how it is worked out. PMOS proposes, you refine by talking it through, then Design makes it look right. Sources are read only. Nothing is sent.'),
414
+ el('div', { class: 'rd-dks' }, chat(), el('div', { class: 'rd-dks-out', 'data-st-preview': '' }, ...outHTML())));
415
+ if (sb) sb.scrollTop = y; const l2 = body.querySelector('.rd-dks-log'); if (l2) l2.scrollTop = ly == null ? l2.scrollHeight : ly;
416
+ for (const [k, v] of Object.entries(fvals)) { const i = body.querySelector('[data-st-field="' + k + '"]'); if (i) i.value = v; }
417
+ if (focused) { const n = body.querySelector(focused); if (n) n.focus(); }
418
+ }
419
+ async function toDesign() {
420
+ const m = await import('./c3-design-studio.js');
421
+ m.openStudio({ ws, id, name: (S.setup && S.setup.name) || name });
422
+ }
423
+ paint(true); load(false).then(scrollLog);
424
+ return { close: () => ui.close(), reload: () => load(true) };
425
+ }
426
+
427
+ /* ── the saved structure: a section of the report setup sheet ── */
428
+ export async function structureSection({ ws, id, name } = {}) {
429
+ ensureStructureCss();
430
+ const r = await RSX.structure(ws, id);
431
+ const open = (view) => () => openStructure({ ws, id, name, view });
432
+ const none = (why) => sect({ ic: 'todo', title: 'Structure', attrs: { 'data-st-brief': '' } }, el('div', { class: 'fold', 'data-st-nosaved': '' }, icon('info', 'sm'), ' ' + why),
433
+ el('div', { class: 'ds-acts' }, pbtn('primary', 'Shape the report', 'todo', { 'data-st-act': 'open', onclick: open() })));
434
+ if (!r || r.ok === false || !r.structure) return none('The structure could not be read.');
435
+ const D = r.structure;
436
+ if (D.saved == null) return none('No saved structure yet. Decide what the report answers, where each number comes from and how it is worked out, then press Use this structure. Until then PMOS drafts it with the standard frame.');
437
+ let p = D.saved === D.current ? D.plan : null;
438
+ if (!p) { const v = await RSX.version(ws, id, D.saved); p = v && v.ok ? v.plan : null; }
439
+ if (!p) return none('The saved version S' + D.saved + ' could not be read.');
440
+ const u = p.purpose || {}; const nNew = (p.charts || []).filter((c) => !c.chart).length; const fromCharts = (p.charts || []).length - nNew;
441
+ const eye = (p.sources || []).filter((s) => s.kind === 'chrome' || s.kind === 'computer');
442
+ return el('div', { class: 'rs-st-saved', 'data-st-saved-wrap': '' },
443
+ sect({ ic: 'todo', title: 'Structure', right: el('span', { class: 'tag acc', 'data-st-saved-tag': '' }, 'Structure S' + D.saved), attrs: { 'data-st-brief': '' } },
444
+ el('div', { class: 'dim rs-st-small' }, 'Written by PMOS from your studio conversation, version S' + D.saved + '. Each cycle PMOS reads the sources with this logic, works out the numbers and writes the notes. If a source cannot be read, the draft says so under Sources and never guesses the number.'
445
+ + (D.current !== D.saved ? ' A newer version, S' + D.current + ', is not saved.' : '')),
446
+ el('h3', { class: 'ds-h' }, 'What it answers'), el('blockquote', { class: 'rd-dks-q', 'data-st-answers': '' }, [u.readers, u.decision, u.scope].filter(Boolean).join('. ') + '.'),
447
+ el('h3', { class: 'ds-h' }, 'Sources'), el('div', { class: 'chips' }, (p.sources || []).map((s) => chip(icon(KIND_IC[s.kind] || 'doc', 'sm'), s.name))),
448
+ el('h3', { class: 'ds-h' }, 'Charts'), el('div', { class: 'chips' }, (p.charts || []).map((c) => chip(icon('report', 'sm'), c.name + (c.chart ? '' : ' · new')))),
449
+ el('div', { class: 'dim rs-st-tiny' }, nNew + (nNew === 1 ? ' new chart' : ' new charts') + ' saved in Reports, Charts. ' + fromCharts + (fromCharts === 1 ? ' comes' : ' come') + ' from Charts.'),
450
+ el('h3', { class: 'ds-h' }, 'Outline'), el('ol', { class: 'rs-st-mini' }, (p.outline || []).map((o) => el('li', null, o.title))),
451
+ el('div', { class: 'ds-acts' }, pbtn('primary', 'Refine the structure', 'spark', { 'data-st-act': 'open', onclick: open() }), pbtn('outline', 'Try it with real data', 'play', { 'data-st-act': 'opendry', onclick: open('dry') }))),
452
+ sect({ ic: 'info', title: 'When the structure changes' }, el('ul', { class: 'chgl' }, el('li', null, icon('clock', 'sm'), el('span', null, 'A change applies from the next cycle. Issued cycles keep the structure they were drafted with.'), el('span')),
453
+ eye.length ? el('li', null, icon('out', 'sm'), el('span', null, eye.map((s) => s.name).join(', ') + (eye.length === 1 ? ' is' : ' are') + ' read on your Mac. PMOS checks it 1 hour before each draft; if the check fails, Work asks you to open Chrome or sign in, and the draft waits up to 4 hours.'), el('span')) : null,
454
+ el('li', null, icon('alert', 'sm'), el('span', null, 'When a source stops answering, or an event is renamed, the draft names the chart it could not get and PMOS asks you in the studio.'), el('span')))));
455
+ }
@@ -64,3 +64,5 @@
64
64
  /* the rule line under the tabs: how Reports is organised (Live, Sent, Building blocks) */
65
65
  .rp3 .rp3-rule { margin: 0 0 var(--sp-3); align-items: center; }
66
66
  .rp3 .rp3-rulelbl { font-size: var(--fs-1); }
67
+ .rp3-libcard .dim.mono{font-size:var(--fs-1)}
68
+ .rp3-dp{margin:2px 0 10px;font-size:var(--fs-3);line-height:1.5;overflow-wrap:anywhere}
@@ -117,7 +117,7 @@ export function mountReports(host, ctx) {
117
117
  ensureC3Css(); ensureComponentsCss(); ensureReportsCss();
118
118
  const C = ctx || {};
119
119
  const get = C.pmGet || httpGet;
120
- const S = { tab: 'dash', scope: 'portfolio', pick: { board: '', initiative: '' }, meta: null, metaErr: null, layouts: {}, edit: false, alive: true, libOpen: new Set(), setup: null };
120
+ const S = { tab: 'dash', scope: 'portfolio', pick: { board: '', initiative: '' }, meta: null, metaErr: null, layouts: {}, edit: false, alive: true, libOpen: new Set(), setup: null, data: [] };
121
121
  const root = el('div', { class: 'rp3 calwrap', 'data-rp3': '' });
122
122
  const stopPhone = host.closest && host.closest('.c3') ? () => {} : c3Phone(root);
123
123
  if (!(host.closest && host.closest('.c3'))) root.classList.add('c3');
@@ -151,7 +151,7 @@ export function mountReports(host, ctx) {
151
151
  };
152
152
 
153
153
  /* tabs */
154
- const badgeOf = (id) => (id === 'dash' ? SCOPES.length : id === 'charts' ? (S.meta ? lib().length : null) : (RS.loaded ? draftsWaiting(RS.docs) : null));
154
+ const badgeOf = (id) => (id === 'dash' ? SCOPES.length : id === 'charts' ? (S.meta ? lib().length + S.data.length : null) : (RS.loaded ? draftsWaiting(RS.docs) : null));
155
155
  function drawTabs() {
156
156
  tabBar.replaceChildren(...TABS.map(([id, label, icon]) => { const n = badgeOf(id); return pill({ type: 'button', role: 'tab', 'aria-selected': String(S.tab === id), 'aria-pressed': String(S.tab === id), tabindex: S.tab === id ? '0' : '-1', 'data-rp3-tab': id, onclick: () => showTab(id) }, c3ic(icon, 'sm'), label,
157
157
  n ? el('span', { class: 'bdg g', 'data-rp3-badge': id, title: id === 'reports' ? 'Drafts waiting for you' : '' }, String(n)) : null); }));
@@ -334,8 +334,22 @@ export function mountReports(host, ctx) {
334
334
  if (S.libOpen.has(r.id)) api.open();
335
335
  return row;
336
336
  });
337
+ /* data charts: the ones a report's Structure step saved (GET /api/charts), beside the built-ins above, with their source and where they are used */
338
+ const usedBy = (c) => (c.users || []).map((u) => (u.kind === 'dashboard' ? 'Dashboard ' : 'Report ') + u.name).join(', ') || 'none yet';
339
+ const dataRows = S.data.map((c) => {
340
+ const src = c.source || {}; const srcName = [src.name, src.via ? 'through ' + src.via : ''].filter(Boolean).join(' ') || 'no source named';
341
+ const def = (lab, v) => v ? el('div', null, el('span', { class: 'dim mono' }, lab), el('p', { class: 'rp3-dp' }, v)) : null;
342
+ const card = el('div', { class: 'rp3-libcard', 'data-rp3-libcard': c.id }, def('EXACTLY WHAT PMOS READS', c.query), def('HOW IT IS WORKED OUT', c.how), def('COMPARED WITH', c.compare), def('FLAGGED WHEN', c.flag), def('WHY IT IS HERE', c.why));
343
+ const xp = makeXp(card);
344
+ const row = el('li', { class: 'nrow rp3-librow', 'data-home': c.id, 'data-rp3-data-chart': c.id },
345
+ el('span', { class: 'ki hot' }, c3ic('report')),
346
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, c.name), el('div', { class: 'dim' }, c.question || ''),
347
+ el('div', { class: 'chips' }, el('span', { class: 'tag acc' }, 'Data chart'), el('span', { class: 'chip2', 'data-rp3-source': c.id }, c3ic('drop', 'sm'), srcName), el('span', { class: 'chip2', 'data-rp3-used': c.id }, c3ic('report', 'sm'), 'Used in: ' + usedBy(c))), xp));
348
+ attachExpand(row, xp, { key: 'rp3d-' + c.id });
349
+ return row;
350
+ });
337
351
  P.replaceChildren(el('div', { class: 'calbody brief' }, el('div', { class: 'fold rp3-note' }, c3ic('cols', 'sm'), 'Each chart lives here once. Dashboards and reports point at it.'),
338
- sect('Charts', 'cols', rows.length, el('ul', { class: 'list', role: 'list', 'data-rp3-lib': '' }, rows))));
352
+ sect('Charts', 'cols', rows.length + dataRows.length, el('ul', { class: 'list', role: 'list', 'data-rp3-lib': '' }, rows, dataRows))));
339
353
  }
340
354
  function openInLibrary(rid) {
341
355
  S.libOpen.add(rid); showTab('charts');
@@ -350,6 +364,11 @@ export function mountReports(host, ctx) {
350
364
  if (r && r.ok) { S.meta = r; S.layouts = { ...(r.layouts || {}) }; } else S.metaErr = (r && (r.error || r.reason)) || 'not available';
351
365
  drawTabs(); drawDash(); if (S.tab === 'charts') drawLib();
352
366
  }, (e) => { S.metaErr = String((e && e.message) || e); drawDash(); });
367
+ /* the data charts a report's Structure step saved; a workspace without them (or an older server) shows the built-ins alone */
368
+ Promise.resolve(get('/api/charts' + (C.ws ? '?ws=' + encodeURIComponent(C.ws) : ''))).then((r) => {
369
+ if (!S.alive || !r || !r.ok) return;
370
+ S.data = r.charts || []; drawTabs(); if (S.tab === 'charts') drawLib();
371
+ }, () => {});
353
372
 
354
373
  const api = { dispose() { S.alive = false; offReports(); if (livePage === api) livePage = null; stopPhone(); root.remove(); }, root, showTab: (v) => { v = TAB_OF[v] || v; if (panes[v]) showTab(v); } };
355
374
  livePage = api;
@@ -611,6 +611,8 @@ async function boot() {
611
611
  if (q.get('focus') || q.get('fdate')) import('./c3-focus.js').then((m) => m.openFocus({ event: { id: q.get('focus') || '', ...(q.get('fat') ? { at: q.get('fat'), title: q.get('ftitle') || '' } : {}) }, date: q.get('fdate') || '', pop: q.get('pop') === '1', meet: q.get('meet') || '', mtitle: q.get('mtitle') || '' })).catch(() => { /* the focus page is not available */ }); // a reload keeps the Meeting focus page open; ?pop=1 is its compact form for the Chrome extension (focus=now asks which meeting the Meet call is)
612
612
  /* ?studio=<report id> opens that report's design studio over the Reports tab (the PMOS Chrome extension's Open the studio). */
613
613
  if (q.get('studio') && /^[a-z][a-z0-9-]{0,39}$/.test(q.get('studio'))) { if (!start) start = 'rep'; import('./c3-design-studio.js').then((m) => m.openStudio({ ws: S.ws, id: q.get('studio'), name: '' })).catch(() => { /* the studio is not available */ }); }
614
+ /* ?structure=<report id> opens that report's Structure step (the studio's step 1) over the Reports tab. */
615
+ if (q.get('structure') && /^[a-z][a-z0-9-]{0,39}$/.test(q.get('structure'))) { if (!start) start = 'rep'; import('./c3-report-structure.js').then((m) => m.openStructure({ ws: S.ws, id: q.get('structure'), name: '' })).catch(() => { /* the structure step is not available */ }); }
614
616
  if (!S.userNav) showTab(start || 'brief', false); // a press during the read above (the gear, a tab) wins over the start page
615
617
  new ResizeObserver(moveInd).observe(S.tabNav);
616
618
  briefCount(); S.dock.start(); S.dockStarted = true;
@@ -224,6 +224,7 @@ async function loadOpen(inst) {
224
224
  inst.note = r && r.ok ? r.note : null;
225
225
  inst.rec = r && r.ok ? r.recording || null : null;
226
226
  inst.shots = r && r.ok && r.screen ? r.screen.frames || [] : [];
227
+ inst.aside = r && r.ok && r.screen ? r.screen.aside || 0 : 0;
227
228
  drawRight(inst);
228
229
  }
229
230
  /** The replay of the note's kept audio: one <audio> per note, kept across redraws so playback goes on. Null when no audio is kept. */
@@ -282,7 +283,7 @@ function drawRight(inst, force) {
282
283
  const n = inst.note;
283
284
  const live = dock.recordState();
284
285
  const held = !!n && live.mode !== 'idle' && live.id === n.id;
285
- const sig = JSON.stringify([inst.open, n && [n.updated_at, n.title, n.final_notes && n.final_notes.at, (n.transcript || []).length, (n.highlights || []).length, n.agent && n.agent.markdown, iniName(n.initiative), n.state, n.resume_until, n.ended_at, (n.sessions || []).length, n.meeting && n.meeting.context && n.meeting.context.taken_at], inst.rec && [inst.rec.bytes, inst.rec.cleaned], (inst.shots || []).length, live.mode, live.id, !!inst.rows, inst.ctxOpen, inst.info]);
286
+ const sig = JSON.stringify([inst.open, n && [n.updated_at, n.title, n.final_notes && n.final_notes.at, (n.transcript || []).length, (n.highlights || []).length, n.agent && n.agent.markdown, iniName(n.initiative), n.state, n.resume_until, n.ended_at, (n.sessions || []).length, n.meeting && n.meeting.context && n.meeting.context.taken_at], inst.rec && [inst.rec.bytes, inst.rec.cleaned], (inst.shots || []).length, inst.aside || 0, live.mode, live.id, !!inst.rows, inst.ctxOpen, inst.info]);
286
287
  // a held note draws again on every render: the clock, the lines and the paused minutes move. Nothing else draws without a change.
287
288
  if (!force && !held && sig === inst.rightSig) return;
288
289
  // a field being typed in is not redrawn under the cursor, except in the live block, whose fields are kept across redraws
@@ -417,7 +418,8 @@ function noteParts(inst, n, live) {
417
418
  el('p', { class: 'nt-hint' }, 'Kept on this machine. Press a time in the discussion to play from there.')));
418
419
  // the screenshots of the shared screen, on the same clock as the replay and the transcript (notes-screen-capture)
419
420
  const shots = held || n.private ? [] : inst.shots || [];
420
- if (shots.length) out.push(screenStrip({ id: n.id, frames: shots, seek: replay ? (ms) => seekReplay(inst, ms) : null, replay }));
421
+ const asideN = held || n.private ? 0 : inst.aside || 0;
422
+ if (shots.length || asideN) out.push(screenStrip({ id: n.id, frames: shots, aside: asideN, seek: replay ? (ms) => seekReplay(inst, ms) : null, replay }));
421
423
 
422
424
  const cf = conflictsOf(n);
423
425
  if (cf.length) {