create-pmos 0.179.0 → 0.180.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 (51) 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-setup-test.js +190 -0
  7. package/workspace/c3-reports-test.js +30 -97
  8. package/workspace/guide/document-sheet.md +14 -0
  9. package/workspace/guide/index.md +3 -2
  10. package/workspace/guide/reports-dashboards.md +17 -28
  11. package/workspace/guide/reports-page.md +10 -5
  12. package/workspace/guide/reports-set-up.md +56 -0
  13. package/workspace/guide/reports.md +13 -1
  14. package/workspace/guide/settings.md +11 -2
  15. package/workspace/lib/doc-sync.js +28 -6
  16. package/workspace/lib/docs-cli.js +7 -11
  17. package/workspace/lib/four-panel.js +1 -1
  18. package/workspace/lib/headless.js +21 -2
  19. package/workspace/lib/report-checks.js +103 -0
  20. package/workspace/lib/report-cli.js +89 -4
  21. package/workspace/lib/report-export.js +347 -0
  22. package/workspace/lib/report-finder.js +156 -0
  23. package/workspace/lib/report-needs.js +1 -1
  24. package/workspace/lib/report-setups.js +743 -0
  25. package/workspace/lib/route-modules.js +1 -0
  26. package/workspace/lib/routes-c3-reports.js +35 -17
  27. package/workspace/lib/routes-doc-health.js +15 -1
  28. package/workspace/lib/routes-report-setups.js +368 -0
  29. package/workspace/lib/schedule-logic.js +5 -2
  30. package/workspace/package-lock.json +2 -2
  31. package/workspace/package.json +1 -1
  32. package/workspace/public/pm/c3-demo-rep2.js +15 -0
  33. package/workspace/public/pm/c3-demo-rs.js +281 -0
  34. package/workspace/public/pm/c3-demo.js +1 -1
  35. package/workspace/public/pm/c3-doc-formats.js +152 -0
  36. package/workspace/public/pm/c3-doc-sheet.js +115 -25
  37. package/workspace/public/pm/c3-quiet.js +2 -2
  38. package/workspace/public/pm/c3-report-print.css +20 -0
  39. package/workspace/public/pm/c3-report-setup.css +62 -0
  40. package/workspace/public/pm/c3-report-setup.js +260 -0
  41. package/workspace/public/pm/c3-report-store.js +208 -0
  42. package/workspace/public/pm/c3-reports.css +4 -0
  43. package/workspace/public/pm/c3-reports.js +47 -181
  44. package/workspace/public/pm/c3-settings.js +4 -3
  45. package/workspace/public/pm/c3-shell.js +15 -31
  46. package/workspace/server.js +66 -5
  47. package/workspace/skills/doc-sync.skill +17 -8
  48. package/workspace/skills/report-find.skill +75 -0
  49. package/workspace/skills/report-setup.skill +67 -0
  50. package/workspace/test-files.json +10 -0
  51. package/workspace/test-lists/report-workspace-ui.json +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.179.0",
3
+ "version": "0.180.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@21b31ef
1
+ kernel-source: pmos@5ed98ca
2
2
  assembled: 2026-10-13
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-13T02:06:03Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T02:13:11Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-13T02:06:03Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T02:13:11Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-13T02:06:03Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T02:13:11Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* c3-report-setup-test.js — the report workspace on console3, UI slice (initiative report-workspace; mockup 41-reportsetup).
4
+ * DEMO mode: a real server and a real Chromium drive /c3.html?demo=1&tab=rep; c3-demo-rs.js answers the contract routes.
5
+ *
6
+ * tab Ask PMOS, Found in your work (3), Set up (3), Documents (6); the Reports tab badge counts the drafts waiting
7
+ * ask an ask runs, then answers from its own words (not a fixed report); Change revises it; Set up posts ONE setup with the ask and proposal
8
+ * found Set up posts the finding; Not a report hides the row, waits 5 seconds, and Undo posts nothing
9
+ * documents the filters; a draft opens in the document sheet with its Draft, Report and format chips, the format view and the Download items;
10
+ * an issued document is frozen: Edit says so and offers Start the next cycle
11
+ * copy Create the copy waits 5 seconds and Undo posts nothing; a PDF has no copy
12
+ * briefing the Report found card; Settings has the Report finder block and no schedules list in Sharing
13
+ * old tabs the shell event for ?tab=lib and ?tab=drafts lands on Charts and Reports
14
+ * phone at 400 wide none of it scrolls sideways
15
+ * Set RS_SHOTS=<dir> to save screenshots at 1440 and 400 wide. No script error may be raised. */
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { spawn } = require('child_process');
19
+ const { findBrowser } = require('./lib/find-browser');
20
+ const T = require('./test-run').configure({ name: 'c3-report-setup' });
21
+
22
+ const BASE = T.BASE;
23
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
24
+ const until = async (fn, ms = 10000, step = 100) => { const t0 = Date.now(); for (;;) { const v = await fn(); if (v) return v; if (Date.now() - t0 > ms) return v; await sleep(step); } };
25
+ const results = [];
26
+ const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + String(extra).slice(0, 400) : ''}`); };
27
+ let srv = null; let browser = null;
28
+ const finish = async (code) => {
29
+ if (browser) await browser.close().catch(() => {});
30
+ if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
31
+ T.killTmux();
32
+ const bad = results.filter((x) => !x.cond).length;
33
+ console.log(`\n${results.length - bad}/${results.length} c3-report-setup assertions passed`);
34
+ process.exit(code !== undefined ? code : (bad ? 1 : 0));
35
+ };
36
+ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
37
+
38
+ const TAB = '[data-rs-tab]';
39
+ (async () => {
40
+ const exe = findBrowser();
41
+ if (!exe) { console.log('· no Chrome/Chromium on this machine, c3-report-setup suite skipped'); process.exit(0); }
42
+ let chromium;
43
+ try { ({ chromium } = require('playwright-core')); } catch { console.log('· playwright-core not installed, c3-report-setup suite skipped'); process.exit(0); }
44
+ srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
45
+ await T.waitForServer(BASE);
46
+ browser = await chromium.launch({ executablePath: exe });
47
+ require('./lib/c3-full-console').fullConsole(browser); // the desktop layout at phone width (c3-phone-companion)
48
+ const shots = process.env.RS_SHOTS || ''; if (shots) fs.mkdirSync(shots, { recursive: true });
49
+
50
+ const errors = [];
51
+ /* two reads of other initiatives (Horizon scan, Settings) have no demo answer: a 404 on them is not this suite's */
52
+ const watch = (page) => {
53
+ page.on('pageerror', (e) => errors.push('pageerror: ' + String((e && e.message) || e)));
54
+ page.on('response', (r) => { if (r.status() >= 400 && !/\/api\/(scan\/today|settings)\b/.test(r.url())) errors.push('http ' + r.status() + ' ' + r.url()); });
55
+ page.on('dialog', (d) => d.dismiss().catch(() => {}));
56
+ };
57
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
58
+ const page = await ctx.newPage(); watch(page);
59
+ /* demo answers never reach the network, so the posts are counted by a wrapper over the page's fetch (installed after the demo's) */
60
+ const posts = []; await page.exposeFunction('__rsPost', (p, b) => { let j = null; try { j = JSON.parse(b || '{}'); } catch { j = null; } posts.push({ path: p, body: j }); });
61
+ const spy = (pg) => pg.evaluate(() => { if (window.__rsspy) return; window.__rsspy = 1; const f = window.fetch; window.fetch = (i, init) => { if (String((init && init.method) || 'GET').toUpperCase() === 'POST') window.__rsPost(new URL(typeof i === 'string' ? i : i.url, location.origin).pathname, String((init && init.body) || '{}')); return f(i, init); }; });
62
+ const clickToast = () => page.evaluate(() => { const b = document.querySelector('[data-c3-toast-action]'); if (b) b.click(); return !!b; });
63
+ const sent = (p) => posts.filter((x) => x.path === p);
64
+ const shot = async (pg, n) => { if (shots) await pg.screenshot({ path: path.join(shots, n + '.png'), fullPage: true }); };
65
+ const text = (sel) => page.textContent(sel).catch(() => '');
66
+ const goReports = async () => { await page.goto(BASE + '/c3.html?demo=1&tab=rep'); await page.waitForSelector('[data-rp3]'); await page.click('[data-rp3-tab="reports"]'); await page.waitForSelector('[data-rs-row]'); await spy(page); };
67
+
68
+ /* ── tab ── */
69
+ await goReports();
70
+ ok('the Reports tab holds Ask PMOS, Found in your work, Set up and Documents', !!(await page.$(TAB + ' [data-rs-askbox]')) && !!(await page.$('[data-rs-found]')) && !!(await page.$('[data-rs-setups]')) && !!(await page.$('[data-rs-docs]')));
71
+ ok('three findings (Likely, Likely, Not sure), three set-ups and six documents', (await page.$$('[data-rs-row]')).length === 3 && (await page.$$('[data-rs-reg]')).length === 3 && (await page.$$('[data-rs-doc]')).length === 6
72
+ && /Likely/.test(await text('[data-rs-row="rs1"]')) && /Not sure/.test(await text('[data-rs-row="rs3"]')));
73
+ ok('the Reports tab badge counts the drafts waiting (3)', (await text('[data-rp3-badge="reports"]')).trim() === '3');
74
+ ok('every set-up and document says nothing is sent, and a PDF has no live copy', /Nothing is sent/.test(await text(TAB)) && /PDF/.test(await text('[data-rs-reg="rg0"]')));
75
+ await shot(page, 'reports-1440');
76
+
77
+ /* ── ask: answered from its words ── */
78
+ await page.fill('[data-rs-ask-in]', 'Set up my quarterly pricing review for the board');
79
+ await page.click('[data-rs-act="ask"]');
80
+ ok('an ask runs: the card says PMOS is working and offers Cancel', !!(await until(() => page.$('[data-rs-card][data-rs-ask-state="running"] [data-rs-running]'))) && sent('/api/report-setups/ask').length === 1);
81
+ ok('the answer is a proposal made from the words, not a fixed report', !!(await until(() => page.$('[data-rs-card][data-rs-ask-state="ready"]'), 8000)) && /quarterly pricing review for the board/i.test(await text('[data-rs-card]')) && !/IDPF/.test(await text('[data-rs-card]')));
82
+ await page.click('[data-rs-card] [data-rs-act="change"]');
83
+ await page.fill('[data-rs-reply]', 'Draft it on Tuesday instead');
84
+ await page.click('[data-rs-card] [data-rs-act="send"]');
85
+ ok('Change posts the words and the card runs again, then reads Changed', !!(await until(() => page.$('[data-rs-card] [data-rs-changed]'), 8000)) && sent('/api/report-setups/change').length === 1 && sent('/api/report-setups/change')[0].body.text === 'Draft it on Tuesday instead');
86
+ ok('the revised proposal carries the new day', /Tue/.test(await until(async () => { const t = await text('[data-rs-card][data-rs-ask-state="ready"]'); return /Tue/.test(t) ? t : ''; }, 8000)));
87
+ await shot(page, 'rs-ask-1440');
88
+ await page.click('[data-rs-card] [data-rs-act="setup"]');
89
+ ok('Set up posts ONE setup with the ask and the proposal', !!(await until(() => sent('/api/report-setups/setup').length === 1)) && sent('/api/report-setups/setup')[0].body.ask && sent('/api/report-setups/setup')[0].body.proposal && /pricing/i.test(sent('/api/report-setups/setup')[0].body.proposal.name), JSON.stringify(sent('/api/report-setups/setup')).slice(0, 300));
90
+ ok('the set-up appears marked New with its first draft in Documents', !!(await until(async () => (await page.$$('[data-rs-reg]')).length === 4)) && /New/.test(await text('[data-rs-setups]')) && (await page.$$('[data-rs-doc]')).length === 7);
91
+ ok('nothing else was posted: no copy, no issue', sent('/api/docs/create-copy').length === 0 && sent('/api/report/issue').length === 0);
92
+
93
+ /* ── found: Set up, Not a report with Undo ── */
94
+ await page.click('[data-rs-row="rs2"] [data-rs-act="dismiss"]');
95
+ ok('Not a report hides the row at once and says PMOS will not suggest it again, with Undo', !(await page.$('[data-rs-row="rs2"]')) && /won.t suggest this again/.test(await text('[data-c3-toast]')) && !!(await page.$('[data-c3-toast-action]')));
96
+ await clickToast();
97
+ await sleep(5600);
98
+ ok('Undo brings the row back and no dismiss was ever posted', !!(await page.$('[data-rs-row="rs2"]')) && sent('/api/report-setups/dismiss').length === 0);
99
+ await page.click('[data-rs-row="rs3"] [data-rs-act="dismiss"]');
100
+ ok('no POST during the five seconds', sent('/api/report-setups/dismiss').length === 0);
101
+ ok('after five seconds the dismiss is posted once with the finding id', !!(await until(() => sent('/api/report-setups/dismiss').length === 1, 8000)) && sent('/api/report-setups/dismiss')[0].body.finding === 'rs3');
102
+ await page.click('[data-rs-row="rs1"] [data-rs-act="setup"]');
103
+ ok('Set up on a finding posts the finding and its proposal', !!(await until(() => sent('/api/report-setups/setup').length === 2)) && sent('/api/report-setups/setup')[1].body.finding === 'rs1' && sent('/api/report-setups/setup')[1].body.proposal.sections.length === 5);
104
+ ok('the finding leaves the list and its set-up arrives', !!(await until(async () => !(await page.$('[data-rs-row="rs1"]')) && !!(await page.$('[data-rs-reg="rs1"]')))));
105
+
106
+ /* ── documents ── */
107
+ await page.click('[data-rs-act="docf"][data-k="state"][data-v="issued"]');
108
+ ok('the Issued filter lists only issued documents', (await page.$$('[data-rs-doc] .tag.ok')).length === (await page.$$('[data-rs-doc]')).length && (await page.$$('[data-rs-doc]')).length >= 4);
109
+ await page.click('[data-rs-act="docf"][data-k="state"][data-v="all"]');
110
+ await page.click('[data-rs-doc$="rs1/2026-10-21.md"] [data-rs-act="opendoc"] >> nth=0');
111
+ ok('a draft opens in the document sheet with its format chip and an Issue button', !!(await until(() => page.$('[data-doc-format]'))) && !!(await page.$('[data-doc-issue]')) && /Google Slides/.test(await text('[data-doc-format]')));
112
+ ok('the Google Slides draft reads as slides, with the title card first', !!(await until(() => page.$('.rs-slides .rs-slide'))) && (await page.$$('.rs-slide')).length >= 6 && !!(await page.$('.rs-slide.title')));
113
+ ok('its copy chip says Google Slides: not created', /Google Slides: not created/.test(await text('[data-doc-copychip]')));
114
+ await shot(page, 'rs-draft-1440');
115
+ await page.click('[data-doc-more]');
116
+ const dl = await page.$$eval('[data-doc-download]', (a) => a.map((b) => b.getAttribute('data-doc-download')));
117
+ ok('the more menu offers the Slides downloads (PDF, pptx)', dl.includes('pdf') && dl.includes('pptx'), JSON.stringify(dl));
118
+ ok('Download posts nothing and presses nothing by itself', sent('/api/docs/create-copy').length === 0);
119
+ await page.click('[data-doc-more]');
120
+ /* Create the copy: five seconds and Undo */
121
+ await page.click('[data-doc-copychip]');
122
+ const cb = await until(() => page.$('[data-copy-create]'), 3000);
123
+ ok('the Copies panel offers Create the Google Slides copy for a copy that does not exist', !!cb && /Create the Google Slides copy/.test(await text('[data-copy-create]')));
124
+ if (cb) {
125
+ await cb.click();
126
+ ok('Create the copy posts nothing for five seconds', sent('/api/docs/create-copy').length === 0);
127
+ await until(() => clickToast(), 2000);
128
+ await sleep(5400);
129
+ ok('Undo means no copy was created', sent('/api/docs/create-copy').length === 0);
130
+ } else ok('Create the copy waits and Undo posts nothing', false, 'no create button found');
131
+ await page.keyboard.press('Escape');
132
+
133
+ /* issued, frozen */
134
+ await page.waitForSelector('[data-rs-doc]');
135
+ await page.click('[data-rs-doc$="rs1/2026-10-07.md"] [data-rs-act="opendoc"] >> nth=0');
136
+ ok('an issued document shows Issued and no Issue button', !!(await until(() => page.$('[data-doc-format]'))) && !(await page.$('[data-doc-issue]')));
137
+ await page.click('[data-dmode="edit"]');
138
+ ok('Edit on an issued document says it is frozen and offers Start the next cycle', !!(await until(() => page.$('[data-doc-next]'))) && /Issued reports are frozen/.test(await text('body')));
139
+ await shot(page, 'rs-doc-issued-1440');
140
+ await page.keyboard.press('Escape');
141
+
142
+ /* Issue: the PM's act on the existing route, with the cycle and the sha it read */
143
+ await page.waitForSelector('[data-rs-doc]');
144
+ await page.click('[data-rs-doc$="rs1/2026-10-21.md"] [data-rs-act="opendoc"] >> nth=0');
145
+ await until(() => page.$('[data-doc-issue]'));
146
+ await page.click('[data-doc-issue]');
147
+ const iss = await until(() => sent('/api/report/issue')[0]);
148
+ ok('Issue posts the existing report issue with the cadence, the slot and a sha', !!iss && iss.body.cadence === 'rs1' && iss.body.slot === '2026-10-21' && /^[0-9a-f]{40}$/.test(iss.body.sha || ''), JSON.stringify(iss && iss.body));
149
+ ok('after Issue the document is frozen: no Issue button', !!(await until(async () => !(await page.$('[data-doc-issue]')))));
150
+ await page.keyboard.press('Escape');
151
+
152
+
153
+ /* ── briefing and settings ── */
154
+ await page.goto(BASE + '/c3.html?demo=1');
155
+ ok('the Briefing has a Report found card with Set up, See what PMOS found and Not a report', !!(await until(() => page.$('[data-rs-card] [data-rs-act="b-setup"]'), 12000)) && !!(await page.$('[data-rs-act="b-see"]')) && !!(await page.$('[data-rs-act="b-no"]')) && /Nothing is sent/.test(await text('[data-rs-card]')));
156
+ await page.goto(BASE + '/c3.html?demo=1&tab=settings&section=connectors');
157
+ ok('Settings, Connectors has the Report finder: Weekly or Off, and the sources', !!(await until(() => page.$('[data-rs-finder]'), 12000)) && !!(await spy(page).then(() => 1)) && /Weekly/.test(await text('[data-rs-finder]')) && /Calendar/.test(await text('[data-rs-finder]')));
158
+ await page.click('[data-rs-finder] button:has-text("Off") >> nth=0');
159
+ ok('Off posts the finder cadence', !!(await until(() => sent('/api/report-setups/finder').length >= 1)) && sent('/api/report-setups/finder')[0].body.cadence === 'off');
160
+ await page.goto(BASE + '/c3.html?demo=1&tab=settings&section=sharing');
161
+ await page.waitForSelector('[data-c3set-sched-link]');
162
+ ok('Sharing links to Reports, Set up and no longer lists schedules', /Set up/.test(await text('[data-c3set-sched-link]')) && !/Scheduled shares/.test(await text('body')));
163
+
164
+ /* ── old tab names ── */
165
+ await page.goto(BASE + '/c3.html?demo=1&tab=rep'); await page.waitForSelector('[data-rp3]');
166
+ await page.evaluate(() => window.dispatchEvent(new CustomEvent('pmos:go-tab', { detail: { tab: 'rep', view: 'lib' } })));
167
+ ok('view=lib lands on Charts', !!(await until(async () => await page.getAttribute('[data-rp3-tab="charts"]', 'aria-selected') === 'true')));
168
+ await page.evaluate(() => window.dispatchEvent(new CustomEvent('pmos:go-tab', { detail: { tab: 'rep', view: 'drafts' } })));
169
+ ok('view=drafts lands on Reports', !!(await until(async () => await page.getAttribute('[data-rp3-tab="reports"]', 'aria-selected') === 'true')));
170
+
171
+ /* ── phone ── */
172
+ const ph = await browser.newContext({ viewport: { width: 400, height: 860 }, hasTouch: true, isMobile: true });
173
+ const pp = await ph.newPage(); watch(pp);
174
+ const noScroll = () => pp.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1);
175
+ await pp.goto(BASE + '/c3.html?demo=1&tab=rep'); await pp.waitForSelector('[data-rp3]'); await pp.click('[data-rp3-tab="reports"]'); await pp.waitForSelector('[data-rs-row]');
176
+ ok('400px: the Reports tab has no horizontal scroll', await noScroll());
177
+ await shot(pp, 'reports-400');
178
+ await pp.click('[data-rs-row="rs1"] [data-rs-act="open"], [data-rs-row="rs1"] .tt');
179
+ ok('400px: a proposal sheet has no horizontal scroll', !!(await until(() => pp.$('[data-rs-sheet]'))) && await noScroll());
180
+ await pp.keyboard.press('Escape');
181
+ await pp.click('[data-rs-doc] [data-rs-act="opendoc"] >> nth=0');
182
+ await until(() => pp.$('[data-doc-format]'));
183
+ ok('400px: the document sheet has no horizontal scroll', await noScroll());
184
+ await shot(pp, 'rs-draft-400');
185
+ await ph.close();
186
+
187
+ await sleep(300);
188
+ ok('no script error and no failed request', errors.length === 0, errors.slice(0, 4).join(' | '));
189
+ return finish();
190
+ })();
@@ -7,18 +7,10 @@
7
7
  * file in public/) that installs demo mode and mounts mountReports with the same ctx the shell passes ({ ws, pmGet, drafts }, drafts
8
8
  * doing what repTab does). The suite prints which one it used.
9
9
  *
10
- * tabs Dashboards, Library and Drafts; Dashboards first
11
- * scopes Portfolio draws its seven default reports from the Calendar's builders; Board and Initiative show a chooser, and a
12
- * report shows only the rows of the chosen board or initiative; a scope's add list holds only reports that fit it
13
- * cache the data route is read once per scope (switching back reads nothing)
14
- * edit up and down, Remove with Undo, Add from the report library, Reset to default; each change POSTs { key, reports }
15
- * and a failed POST is toasted while the page keeps the change
16
- * reload a stored layout is there after a reload
17
- * share the frame (Headline, Sections, Needs your call, Sources) for Leadership and for Team; Save as draft POSTs ONE draft;
18
- * the schedule: three options, the next draft, the warning that nothing is sent; Save schedule POSTs { key, plan, audience }
19
- * and the head and Drafts > Scheduled show it; a draft says whether a schedule or you made it
10
+ * tabs Dashboards, Reports and Charts (the old Library and Drafts); Dashboards first; a rule line under the tabs
11
+ * send the dashboard head has "Send this as a report..." and no Share button or schedule line; the sheet posts from-dashboard
20
12
  * library one row per report with data-home, never on the dashboard; a widget carries data-ref-row and opens its case in place
21
- * drafts the Drafts tab shows the report families
13
+ * reports the Reports tab mounts Ask PMOS, Found in your work, Set up and Documents (the full flow is c3-report-setup-test.js)
22
14
  * phone 400px: one column, no horizontal page scroll
23
15
  * errors no script error and no console error
24
16
  */
@@ -55,30 +47,17 @@ const HARNESS_HTML = `<!doctype html><html lang="en" data-pm-page-theme="light">
55
47
  <script type="module">
56
48
  const demo = await import('/pm/c3-demo.js');
57
49
  demo.installDemo(); if (window.__c3demoReady) await window.__c3demoReady;
58
- const { el } = await import('/pm/dom.js');
59
50
  const { ensureC3Css, c3Phone } = await import('/pm/pm-work-view.js');
60
51
  const { pmGet } = await import('/pm/http.js');
61
52
  const { mountReports } = await import('/pm/c3-reports.js');
62
- const { mountReportFamilies } = await import('/pm/report-families-view.js');
63
- const { mountOutbox } = await import('/pm/outbox-panel.js');
64
53
  ensureC3Css(); c3Phone(document.getElementById('c3-root'));
65
54
  const ws = demo.DEMO_WS;
66
- const drafts = (pane) => {
67
- const fam = el('div', { 'data-c3pg': 'families-host' }); const out = el('div', { class: 'brief', 'data-c3pg': 'outbox-host' });
68
- pane.append(fam, out);
69
- pmGet('/api/report-families/facts?ws=' + encodeURIComponent(ws)).then((j) => {
70
- mountReportFamilies(fam, { reportFacts: j && j.ok ? j.facts : {}, plan: null, portfolio: null });
71
- mountOutbox(out, {});
72
- const sync = () => { out.hidden = !fam.querySelector('[data-family="communications"][aria-pressed="true"]'); };
73
- sync(); new MutationObserver(sync).observe(fam, { subtree: true, attributes: true, attributeFilter: ['aria-pressed'] });
74
- });
75
- };
76
- mountReports(document.getElementById('host'), { ws, pmGet, drafts });
55
+ mountReports(document.getElementById('host'), { ws, pmGet });
77
56
  document.getElementById('c3-root').setAttribute('data-c3-demo', '1');
78
57
  </script></body></html>`;
79
58
 
80
59
  const D = '[data-rp3-pane="dash"]';
81
- const ACT = '[data-rp3]'; // Share, Edit dashboard and Reset sit on the tab row at desktop width (c3-quiet-pages), inside the pane on a phone
60
+ const ACT = '[data-rp3]'; // Send, Edit dashboard and Reset sit on the tab row at desktop width (c3-quiet-pages), inside the pane on a phone
82
61
  const order = (page) => page.$$eval(D + ' [data-rp3-w]', (a) => a.map((n) => n.getAttribute('data-rp3-w')));
83
62
  const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'maturity'];
84
63
 
@@ -137,14 +116,15 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
137
116
 
138
117
  /* ── tabs ── */
139
118
  const tabs = await page.$$eval('[data-rp3] [role="tab"]', (a) => a.map((b) => [b.getAttribute('data-rp3-tab'), b.textContent.trim(), b.getAttribute('aria-selected')]));
140
- ok('three tabs: Dashboards, Library, Drafts, with Dashboards selected', tabs.length === 3 && tabs[0][0] === 'dash' && /^Dashboards/.test(tabs[0][1]) && tabs[0][2] === 'true' && /^Library/.test(tabs[1][1]) && /^Drafts/.test(tabs[2][1]), JSON.stringify(tabs));
119
+ ok('three tabs: Dashboards, Reports, Charts, with Dashboards selected', tabs.length === 3 && tabs[0][0] === 'dash' && /^Dashboards/.test(tabs[0][1]) && tabs[0][2] === 'true' && /^Reports/.test(tabs[1][1]) && /^Charts/.test(tabs[2][1]), JSON.stringify(tabs));
120
+ ok('a rule line under the tabs says how Reports is organised', /HOW REPORTS IS ORGANISED/.test(await page.textContent('[data-rp3-rule]')) && /Live/.test(await page.textContent('[data-rp3-rule]')) && /Building blocks/.test(await page.textContent('[data-rp3-rule]')));
141
121
 
142
122
  ok('the tab row is the mockup\'s calbar: tabs on the left, the scope pills on the same row (no intro paragraph)', await until(() => page.evaluate(() => {
143
123
  const bar = document.querySelector('[data-rp3] > .calbar'); if (!bar || !bar.querySelector("[data-rp3-scope]")) return false;
144
124
  const t = bar.querySelector('.rp3-tabs').getBoundingClientRect(); const s = bar.querySelector('[data-rp3-scope]').getBoundingClientRect();
145
125
  return Math.abs(t.top - s.top) < 40 && !document.querySelector('.rp3-intro') && bar.querySelectorAll('.pill').length === 3;
146
126
  })));
147
- ok('the Library tab carries its count as a badge', /7/.test(await page.textContent('[data-rp3-tab="lib"] .bdg')));
127
+ ok('the Charts tab carries its count as a badge', /7/.test(await page.textContent('[data-rp3-tab="charts"] .bdg')));
148
128
  if (mode === 'shell') ok('the headline deck sits above the tabs (shell)', !!(await until(() => page.$('[data-rp3-deck] .deck > *'))) && await page.evaluate(() => document.querySelector('[data-rp3-deck]').getBoundingClientRect().top < document.querySelector('[data-rp3] > .calbar').getBoundingClientRect().top));
149
129
 
150
130
  /* ── portfolio ── */
@@ -248,85 +228,38 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
248
228
  await page.click('[data-rp3-scope="portfolio"]'); await loaded(page, 7);
249
229
  ok('the portfolio keeps its own layout when a board changes', JSON.stringify(await order(page)) === JSON.stringify(DEF_P));
250
230
 
251
- /* ── share ── */
252
- ok('the head says the portfolio is scheduled, draft only (the demo seeds a monthly schedule)', /Scheduled: Monthly, 1st at 08:00, draft only/.test(await page.textContent(D + ' [data-rp3-schedline]')));
231
+ /* ── send this as a report ── */
232
+ ok('the head has Send this as a report and no Share button and no schedule line', !!(await page.$(ACT + ' [data-rp3-send]')) && !(await page.$(ACT + ' [data-rp3-share]')) && !(await page.$(ACT + ' [data-rp3-schedline]')) && /Send this as a report/.test(await page.textContent(ACT + ' [data-rp3-send]')));
253
233
  await spy(page);
254
- await page.click(ACT + ' [data-rp3-share]');
255
- const sheet = await until(() => page.$('[data-rp3-sheet] [data-rp3-frame="Headline"]'));
256
- ok('Share opens a sheet with the report frame', !!sheet);
257
- const frames = await page.$$eval('[data-rp3-sheet] [data-rp3-frame] h2', (a) => a.map((h) => h.textContent.replace(/\d+$/, '')));
258
- ok('the frame has Headline, Sections, Needs your call, Sources', JSON.stringify(frames) === JSON.stringify(['Headline', 'Sections', 'Needs your call', 'Sources']), JSON.stringify(frames));
259
- ok('Leadership: one sentence per report, no numbers', await page.$eval('[data-rp3-sheet] [data-rp3-aud]', (n) => n.getAttribute('data-rp3-aud')) === 'Leadership'
260
- && (await page.$$('[data-rp3-sheet] [data-rp3-frame="Sections"] ul.chgl > li')).length === 7 && !(await page.$('[data-rp3-sheet] [data-rp3-nums]')));
261
- const need = await page.textContent('[data-rp3-sheet] [data-rp3-frame="Needs your call"]');
262
- ok('Needs your call lists what the reports flag', /Conversion is off course/.test(need) && /Checkout v2 is delayed/.test(need), need.slice(0, 200));
263
- await page.click('[data-rp3-sheet] [data-rp3-audbtn="Team"]');
264
- ok('Team adds the numbers behind each sentence', !!(await until(async () => (await page.$$('[data-rp3-sheet] [data-rp3-nums]')).length >= 5)));
265
- ok('the Team numbers come from the records (Conversion now 3.1%)', /Conversion: now 3\.1%, should be 3\.75%, target 4%/.test(await page.textContent('[data-rp3-sheet] [data-rp3-nums="outcomes"]')));
266
- const sched = await page.$$eval('[data-rp3-sheet] [data-rp3-sched] button', (a) => a.map((b) => [b.disabled, b.getAttribute('aria-pressed'), b.textContent.trim()]));
267
- ok('the schedule control has its three options, enabled, the saved one (Monthly) pressed', sched.length === 3 && sched.every((x) => !x[0]) && sched[2][1] === 'true' && sched[2][2] === 'Monthly, 1st at 08:00', JSON.stringify(sched));
268
- ok('the next draft is the 1st at 08:00', /^Next draft: \w{3} 1 \w{3}, 08:00$/.test((await page.textContent('[data-rp3-sheet] [data-rp3-next]')).trim()));
269
- ok('the warning says a schedule makes a ready draft and nothing is sent to the team', /A scheduled send makes a ready draft and notifies you\. Nothing is sent to the team until you connect a channel\./.test(await page.textContent('[data-rp3-sheet] #rp3-sched-why')));
270
- ok('Save schedule is a primary button', !!(await page.$('[data-rp3-sheet] [data-rp3-sched-save]:not([disabled])')));
271
- await page.click('[data-rp3-sheet] [data-rp3-draft]');
272
- ok('Save as draft says one draft is in the outbox and nothing was sent', !!(await until(async () => /one draft in the outbox\. Nothing was sent/.test(await page.textContent('[data-rp3-sheet] [data-rp3-share-msg]')))));
273
- ok('Save as draft is off once the draft is saved', await page.$eval('[data-rp3-sheet] [data-rp3-draft]', (b) => b.disabled));
274
- P = await posts(page, '/api/outbox/draft');
275
- const B = P[0] && P[0].body;
276
- ok('ONE draft POST, to a known channel, for the chosen audience, its claim lines labelled', P.length === 1 && B.channel === 'customer-note' && B.recipient === 'Team' && /^Portfolio for Team/.test(B.body)
277
- && B.body.split('\n').filter((l) => /^- /.test(l)).every((l) => /\[reported\]$/.test(l)), JSON.stringify(B).slice(0, 300));
278
- ok('nothing else was POSTed from the sheet (no send, no schedule)', (await page.evaluate(() => window.__rp3spy.posts.length)) === 1);
279
- await page.click('[data-rp3-sheet] [data-sched="weekly"]');
280
- ok('choosing Weekly shows a Monday as the next draft', !!(await until(async () => /^Next draft: Mon \d+ \w{3}, 08:00$/.test(((await page.textContent('[data-rp3-sheet] [data-rp3-next]').catch(() => '')) || '').trim()))));
281
- ok('choosing is not saving: nothing is POSTed yet', (await page.evaluate(() => window.__rp3spy.posts.length)) === 1);
282
- await page.click('[data-rp3-sheet] [data-rp3-sched-save]');
283
- ok('Save schedule says you get a draft and a notification', !!(await until(async () => /Scheduled: Weekly, Mondays 08:00\. You get a draft and a notification\./.test(await page.textContent('[data-rp3-sheet] [data-rp3-share-msg]')))));
284
- P = await posts(page, '/api/report-dashboards/schedule');
285
- ok('Save schedule POSTs ONE schedule: the key, the plan and the sheet\'s audience', P.length === 1 && P[0].body.key === 'portfolio' && P[0].body.plan === 'weekly' && P[0].body.audience === 'Team', JSON.stringify(P));
286
- ok('the head now says Weekly, draft only', !!(await until(async () => /Scheduled: Weekly, Mondays 08:00, draft only/.test(await page.textContent(D + ' [data-rp3-schedline]')))));
287
- await page.keyboard.press('Escape');
288
- ok('Escape closes the sheet', !!(await until(async () => !(await page.$('[data-rp3-sheet]')))));
234
+ await page.click(ACT + ' [data-rp3-send]');
235
+ ok('the send sheet shows the proposed report with Output, Schedule and Audience, and says nothing is sent', !!(await until(async () => { const t = await page.textContent('[data-rs-send-sheet]').catch(() => ''); return /Proposed report/.test(t) && /Output/.test(t) && /Schedule/.test(t) && /Audience/.test(t) && /Nothing is sent/.test(t); }))
236
+ && !(await page.$('[data-rp3-sched]')));
237
+ await page.click('[data-rs-act="send-go"]');
238
+ ok('Set up posts ONE from-dashboard with the dashboard key, a name, an output, a schedule and an audience', !!(await until(async () => (await posts(page, '/api/report-setups/from-dashboard')).length === 1)) && (await (async () => { const b = (await posts(page, '/api/report-setups/from-dashboard'))[0].body; return b.key === 'portfolio' && b.name && b.output && b.schedule && b.schedule.cadence && b.audience; })()));
239
+ ok('after Set up the Reports tab is open', !!(await until(async () => await page.getAttribute('[data-rp3-tab="reports"]', 'aria-selected') === 'true')));
240
+ await page.click('[data-rp3-tab="dash"]');
289
241
 
290
242
  /* ── library and one home ── */
291
243
  ok('the dashboard has no data-home row; every widget carries data-ref-row', !(await page.$(D + ' [data-home]')) && (await page.$$(D + ' section.rp3-w[data-ref-row]')).length === 7);
292
244
  await page.click(D + ' [data-rp3-about="outcomes"]');
293
245
  ok('a widget opens its case in place (question, source, where used)', !!(await until(async () => { const x = await page.$(D + ' [data-rp3-w="outcomes"] .xp.open'); return x && /Used on: Portfolio/.test(await x.textContent()); })));
294
246
  await page.click(D + ' [data-rp3-golib="outcomes"]');
295
- ok('Open in the library goes to the Library tab', !!(await until(async () => await page.getAttribute('[data-rp3-tab="lib"]', 'aria-selected') === 'true')));
296
- const L = '[data-rp3-pane="lib"]';
247
+ ok('Open in Charts goes to the Charts tab', !!(await until(async () => await page.getAttribute('[data-rp3-tab="charts"]', 'aria-selected') === 'true')));
248
+ const L = '[data-rp3-pane="charts"]';
297
249
  const libRows = await page.$$eval(L + ' [data-home]', (a) => a.map((r) => r.getAttribute('data-home')));
298
250
  ok('the library has one row per report, each its home', libRows.length === 7 && new Set(libRows).size === 7, JSON.stringify(libRows));
299
251
  ok('library rows are not references', !(await page.$(L + ' [data-ref-row]')));
300
252
  const lrow = await page.textContent(L + ' [data-home="maturity"]');
301
- ok('a library row shows the question, the source and where it is used (short: On: Portfolio)', /Where does each pillar stand/.test(lrow) && /Pillar maturity records/.test(lrow) && /On: Portfolio/.test(lrow), lrow.slice(0, 200));
253
+ ok('a library row shows the question, the source and where it is used (Dashboards: Portfolio, Reports: none)', /Where does each pillar stand/.test(lrow) && /Pillar maturity records/.test(lrow) && /Dashboards: Portfolio/.test(lrow) && /Reports: /.test(lrow), lrow.slice(0, 200));
302
254
  ok('the row opened from the widget is open, with its card', !!(await until(() => page.$(L + ' [data-home="outcomes"][aria-expanded="true"] [data-cs="outcomes"]'))));
303
255
  await page.click(L + ' [data-home="bets"]');
304
256
  ok('a library row opens the report\'s card in place', !!(await until(() => page.$(L + ' [data-home="bets"] .xp.open [data-cs="bets"]'))));
305
- ok('a board layout shows in where it is used', /On: .*Board/.test(await page.textContent(L + ' [data-home="synergy"]')));
257
+ ok('a board layout shows in where it is used', /Dashboards: .*Board/.test(await page.textContent(L + ' [data-home="synergy"]')));
306
258
 
307
- /* ── drafts ── */
308
- const DR = '[data-rp3-pane="drafts"]';
309
- ok('the Drafts tab carries a count once drafts are read (the seeded two plus the one just made)', !!(await until(async () => (await page.textContent('[data-rp3-draftcount]').catch(() => '')) === '3')));
310
- await page.click('[data-rp3-tab="drafts"]');
311
- ok('the Drafts tab says nothing here is sent, and that a schedule only makes a draft', !!(await until(async () => /Nothing here is sent\. No sender channel is connected yet, so a schedule only makes a ready draft and notifies you\./.test(await page.textContent(DR + ' .fold.w')))));
312
- ok('the dashboard drafts are listed, with Preview and Discard, newest first', !!(await until(async () => (await page.$$(DR + ' [data-draft-row]')).length === 3)) && (await page.textContent(DR + ' [data-draft-row]')).includes('Portfolio') && (await page.$$(DR + ' [data-draft="preview"]')).length === 3);
313
- const dtext = await page.textContent(DR + ' [data-draft-row]');
314
- ok('a draft row shows its audience and that it was not sent', /For Team/.test(dtext) && /Not sent/.test(dtext), dtext);
315
- const sch = await page.textContent(DR + ' [data-rp3-scheduled]');
316
- ok('Scheduled lists the portfolio schedule, its audience and its next draft', /Portfolio/.test(sch) && /Weekly, Mondays 08:00/.test(sch) && /For Team/.test(sch) && /Next draft Mon \d+ \w{3}, 08:00/.test(sch), sch);
317
- ok('the drafts say who made them: a schedule or you', (await page.$$(DR + ' [data-rp3-madeby="schedule"]')).length === 1 && (await page.$$(DR + ' [data-rp3-madeby="you"]')).length === 2
318
- && /Made by schedule/.test(await page.textContent(DR + ' [data-draft-row="out-demo-rep3-s1"]')));
319
- await page.click(DR + ' [data-draft="preview"]');
320
- ok('Preview opens the draft text in a sheet', !!(await until(async () => /Headline:/.test((await page.textContent('[data-rp3-draft-text]').catch(() => '')) || ''))));
321
- await page.keyboard.press('Escape');
322
- await spy(page);
323
- await page.click(DR + ' [data-draft="discard"]');
324
- ok('Discard takes the draft off the list and POSTs the discard', !!(await until(async () => (await page.$$(DR + ' [data-draft-row]')).length === 2)) && (await posts(page, '/api/outbox/discard')).length === 1);
325
- await page.click('[data-c3-toast-action]');
326
- ok('Undo brings the draft back', !!(await until(async () => (await page.$$(DR + ' [data-draft-row]')).length === 3)));
327
- ok('Drafts carries what the Briefing used to: Handled, What I could stop, Data you can lean on (the demo has them)', !!(await until(() => page.$(DR + ' [data-rp3-more] [data-c3pg="handled"]'))) && !!(await page.$(DR + ' [data-rp3-more] [data-c3pg="trust"]')));
328
- ok('Reports to send holds the report families', !!(await until(() => page.$(DR + ' .card.sect [data-report-families] [data-family]'), 15000)) && /Reports to send/.test(await page.textContent(DR + ' [data-rp3-more] ~ .card.sect > .sh2')));
329
- ok('the dashboards are hidden while Drafts is open', await page.$eval(D, (n) => n.hidden));
259
+ /* ── reports ── */
260
+ await page.click('[data-rp3-tab="reports"]');
261
+ ok('the Reports tab mounts Ask PMOS, Found in your work, Set up and Documents; the old Drafts content is gone', !!(await until(() => page.$('[data-rs-tab] [data-rs-askbox]'))) && !!(await until(() => page.$('[data-rs-found]'))) && !!(await page.$('[data-rs-setups]')) && !!(await page.$('[data-rs-docs]')) && !(await page.$('[data-rp3-draftlist]')) && !(await page.$('[data-report-families]')));
262
+ ok('the dashboards are hidden while Reports is open', await page.$eval(D, (n) => n.hidden));
330
263
  await page.click('[data-rp3-tab="dash"]');
331
264
 
332
265
  /* ── phone ── */
@@ -341,12 +274,12 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
341
274
  await pp.click(ACT + ' [data-rp3-edit]');
342
275
  ok('400px: Initiative scope in edit mode has no horizontal scroll', await noScroll(pp));
343
276
  await pp.click(ACT + ' [data-rp3-edit]');
344
- await pp.click(ACT + ' [data-rp3-share]'); await until(() => pp.$('[data-rp3-sheet] [data-rp3-frame]'));
345
- ok('400px: the share sheet has no horizontal scroll', await noScroll(pp));
277
+ await pp.click(ACT + ' [data-rp3-send]'); await until(() => pp.$('[data-rs-send-sheet]'));
278
+ ok('400px: the send sheet has no horizontal scroll', await noScroll(pp));
346
279
  await pp.keyboard.press('Escape');
347
- await pp.click('[data-rp3-tab="lib"]'); await pp.click('[data-rp3-pane="lib"] [data-home="delivery"]');
348
- await until(() => pp.$('[data-rp3-pane="lib"] [data-home="delivery"] .xp.open [data-cs="delivery"]'));
349
- ok('400px: the library with an open row has no horizontal scroll', await noScroll(pp));
280
+ await pp.click('[data-rp3-tab="charts"]'); await pp.click('[data-rp3-pane="charts"] [data-home="delivery"]');
281
+ await until(() => pp.$('[data-rp3-pane="charts"] [data-home="delivery"] .xp.open [data-cs="delivery"]'));
282
+ ok('400px: Charts with an open row has no horizontal scroll', await noScroll(pp));
350
283
  await ph.close();
351
284
 
352
285
  await sleep(300); // a late error from a closing view would land here
@@ -71,3 +71,17 @@ PMOS never overwrites the document from a copy.
71
71
  Health shows the checks of the document and how many pass.
72
72
 
73
73
  Relations shows a graph of the documents around this one: the documents it uses on the left, its initiative and goals on top, and the documents that use it and its copies on the right. A dashed orange line shows a document that is out of step. Select a document to open it. The lists under the graph name the same documents. From Initiatives › Documents, **Open the full graph in Docs** shows the whole graph.
74
+
75
+ ## Report documents
76
+
77
+ A report that you set up opens in the same sheet. Each cycle of a report is one document. The sheet adds these parts for a report.
78
+
79
+ - **The head.** A chip says **Draft** or **Issued**. A second chip names the report. A third chip names the format: Google Slides, Google Doc, Google Sheets, Confluence or PDF.
80
+ - **Read.** The text is drawn in the format of the report. A Slides report shows slides. A Sheets report shows grids. A Google Doc or a Confluence report shows a page. A PDF report shows A4 pages. The words come from the markdown file. Edit the file, or ask for a change, and the format changes with it.
81
+ - **Issue.** Press **Issue** on a draft when you have sent it. PMOS sends nothing. Issue records that you did.
82
+ - **Download.** Open the menu with the three dots. A Slides report offers PDF and PowerPoint. A Sheets report offers Excel and CSV. A Google Doc report offers PDF and Word. A Confluence report offers PDF and Word. A PDF report offers PDF. For PDF, the sheet opens the print dialog. Choose "Save as PDF" in it.
83
+ - **Copies.** A report has one live copy, in the tool of its format. A PDF has none. Before the copy exists, the panel says "Not created" and offers **Create the Google Slides copy** (or the name of the tool of the report). PMOS waits five seconds before it starts, and **Undo** stops it. When PMOS is ahead of the copy, the button is **Update the copy**. PMOS writes to Google or to Confluence only when you press one of these buttons.
84
+
85
+ ## Issued reports are frozen
86
+
87
+ An issued report cannot be edited. **Edit** says so, and offers **Start the next cycle**. The next cycle is a new draft. If the next draft already exists, **Start the next cycle** opens it. If it does not exist, PMOS drafts it on the day of the schedule.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "Guide: start here"
4
- timestamp: 2026-10-06
4
+ timestamp: 2026-10-12
5
5
  ---
6
6
 
7
7
  # Guide: start here
@@ -71,7 +71,8 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
71
71
  - [Board](board.md): one Kanban for the items of every initiative, its filters, Release, and `pmos board`.
72
72
  - [Integrated work](integrated-work.md): how `pmos verify-work` turns a merged commit into Done, and what "Done · your call · not merged" means.
73
73
  - [Report slides](report-slides.md): how the Reports page shows a report as the PM-OS biweekly slides, Present and Print / PDF, and how your notes become the words on the slides.
74
- - [Reports: dashboards of reports](reports-dashboards.md): the console Reports page, its dashboards at portfolio, board and initiative scope, how to edit and share one as a draft, and the Drafts tab.
74
+ - [Reports: dashboards of reports](reports-dashboards.md): the console Reports page, its dashboards at portfolio, board and initiative scope, how to edit one, how to send one as a report, and the Charts tab.
75
+ - [Set up a report by asking, and the report finder](reports-set-up.md): how to ask PMOS to draft a report you send, how the finder finds reports you already send, and how a cycle becomes a document.
75
76
  - [The document sheet](document-sheet.md): read a document as rich text, edit it, accept the agent's proposals section by section, read the edit log, and clear a stale document.
76
77
  - [Ask the agent from the page](ask-the-agent.md): the five presses that start an agent from the console, what the agent may do, and what stays yours.
77
78
  - [Sessions in console3](console3-sessions.md): the Sessions panel, how to open the terminal of a session on the same page, and how to end a session.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "Reports: dashboards of reports"
4
- description: "How the console Reports page shows dashboards at portfolio, board and initiative scope, how to edit one, how to share one as a draft, how to put one on a schedule, and where the report drafts went."
5
- timestamp: 2026-10-11
4
+ description: "How the console Reports page shows dashboards at portfolio, board and initiative scope, how to edit one, how to send one as a report, and what the Charts tab holds."
5
+ timestamp: 2026-10-12
6
6
  ---
7
7
 
8
8
  # Reports: dashboards of reports
@@ -11,9 +11,13 @@ A report answers one question. A dashboard is a set of reports for one scope. Op
11
11
 
12
12
  ## The three tabs
13
13
 
14
- - **Dashboards.** The reports of one scope, drawn from the repository's records.
15
- - **Library.** Every report the workspace can draw, with the question it answers and where it is used.
16
- - **Drafts.** The dashboard drafts you made with Share, what the Briefing used to show below Needs you, and the report families. See "Drafts" below.
14
+ Each tab has one job. A line under the tabs repeats it: **Live** goes to Dashboards, **Sent** goes to Reports, **Building blocks** goes to Charts.
15
+
16
+ - **Dashboards.** The charts of one scope, drawn from the repository's records. A dashboard is live: it changes when your work changes. The tab shows how many dashboards there are.
17
+ - **Reports.** The documents you send, with their set-ups. See [Set up a report by asking](reports-set-up.md). The tab shows how many drafts wait for you.
18
+ - **Charts.** Every chart the workspace can draw, with the question it answers, the dashboards that use it and the reports that use it. The tab shows how many charts there are.
19
+
20
+ The old tab names still work. A link to Library opens Charts. A link to Drafts opens Reports.
17
21
 
18
22
  The headline deck of the Reports page is above the tabs. It shows only report signals: claims that block sending, ticket keys in a leadership report, a digest that is due, and people who hold an old date. It never repeats the Briefing's cards. When there is no report signal, the deck says that nothing needs you. The scope buttons are on the same row as the tabs.
19
23
 
@@ -50,32 +54,17 @@ The Calendar page no longer carries these. They moved here.
50
54
 
51
55
  The workspace keeps your layout for each scope in its runtime state. It does not write to the repository. **Reset to default** returns a scope to its first layout.
52
56
 
53
- ## Share a dashboard
54
-
55
- Press **Share**. A sheet shows the dashboard as a report: a headline, one section per report, what needs your call, and the sources. Choose **Leadership** for one sentence per report. Choose **Team** to add the numbers behind each sentence.
56
-
57
- **Make a draft now** puts one draft in the outbox. Nothing is sent. With no channel connected, the outbox says so.
58
-
59
- ## Put a dashboard on a schedule
60
-
61
- A schedule makes a ready draft of a dashboard and tells you on the bell. Nothing is sent. No sender channel is connected yet, so you send the draft yourself.
62
-
63
- 1. Press **Share**.
64
- 2. Choose the audience: **Leadership** or **Team**.
65
- 3. Choose **Weekly, Mondays 08:00** or **Monthly, 1st at 08:00**. The sheet shows the next draft.
66
- 4. Press **Save schedule**.
57
+ ## Send a dashboard as a report
67
58
 
68
- The dashboard head then says "Scheduled: ..., draft only". To stop a schedule, choose **Not scheduled** and press **Save schedule**. A dashboard has one schedule. The times are the time on this computer.
59
+ A dashboard is live. A report is a document that you send on a day. To send a dashboard as a report, press **Send this as a report…** in the head of the dashboard.
69
60
 
70
- On each run, the workspace reads the dashboard as it is then, with your layout, and puts one draft in the outbox. The draft text is the same as the text **Make a draft now** makes. The bell shows "a scheduled draft is ready" for 7 days, or until you discard the draft. Press the notice to open **Drafts**.
61
+ 1. A sheet shows the proposed report: its name, what it is built from, the output, the schedule and the audience.
62
+ 2. Change the name. Choose the output: **PDF** or **Google Slides**. Choose the schedule: **Weekly**, **Every 2 weeks** or **Monthly**. Choose the audience: **Leadership** or **Team**.
63
+ 3. Press **Set up**.
71
64
 
72
- If the workspace is not running at the time, the run happens once when it starts again. It does not make one draft for each run it missed. If a run cannot read the dashboard, **Scheduled** shows why, and the next run tries again.
65
+ PMOS shows the first draft date. Each cycle is a snapshot document of the dashboard as it was on that day. The document is in Reports, under Documents, and the set-up is under Set up. The dashboard stays live.
73
66
 
74
- ## Drafts
67
+ PMOS drafts only. It sends nothing. It writes to Google only when you press **Create the copy** in the document.
75
68
 
76
- Open the **Drafts** tab. It has these parts, in order.
69
+ The Share sheet and the schedule control of a dashboard are gone. Each schedule is now the schedule of a report. See and change it in Reports, under Set up.
77
70
 
78
- - **Drafts.** One row for each dashboard draft. A row says **Made by schedule** or **Made by you**. Press **Preview** to read the text. Press **Discard** to remove it from the list. **Undo** in the message brings it back. The log keeps the line.
79
- - **Scheduled.** One row for each schedule: the dashboard, how often, the audience and the next draft.
80
- - **Handled for you**, **What I could stop** and **Data you can lean on.** The Briefing used to show these. They come from the same briefing data. A card with no data does not show.
81
- - **Reports to send.** The four report families, the claims check and the outbox. They work as before.
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Reports: draft, read and issue"
4
4
  description: "How to draft a report, add your notes, issue it, and print or copy it. How the report schedules and the Today cards for reports work."
5
- timestamp: 2026-10-05
5
+ timestamp: 2026-10-12
6
6
  ---
7
7
 
8
8
  # Reports: draft, read and issue
@@ -15,11 +15,16 @@ The workspace keeps each report in a file of the repository, for example `planni
15
15
 
16
16
  Open the Reports tab with the shortcut `g r`, from the command palette, or from the Today card of a report. Reports shows the repository that is open.
17
17
 
18
- The page has three parts:
18
+ The Reports page has three tabs: Dashboards, Reports and Charts. See [Reports: dashboards of reports](reports-dashboards.md) and [Set up a report by asking](reports-set-up.md).
19
19
 
20
- - **Report schedules.** The schedules that draft a report.
21
- - **Reports.** The reports of the repository, newest first.
22
- - **The selected report.** Its text and the actions you can do on it.
20
+ The Reports tab is for the documents that you issue. It has four parts:
21
+
22
+ - **Ask PMOS.** Describe a report. PMOS drafts it.
23
+ - **Found in your work.** The reports PMOS found that you already issue.
24
+ - **Set up.** One row for each report with a schedule.
25
+ - **Documents.** One row for each cycle of each report. A draft or an issued document opens in the [document sheet](document-sheet.md).
26
+
27
+ The parts that this guide describes below, such as Draft now, Edit notes and the report schedules, are the controls of the cadence report. The Reports tab does not show them. The `pmos report` commands in "In a terminal" do the same acts.
23
28
 
24
29
  ## Draft a report
25
30