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.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-report-structure-real-test.js +211 -0
- package/workspace/c3-report-structure-test.js +382 -0
- package/workspace/guide/console-calendar.md +14 -0
- package/workspace/guide/index.md +2 -0
- package/workspace/guide/notes.md +22 -1
- package/workspace/guide/report-design.md +4 -0
- package/workspace/guide/report-structure.md +91 -0
- package/workspace/guide/reports-dashboards.md +1 -1
- package/workspace/guide/reports-set-up.md +2 -0
- package/workspace/lib/charts-store.js +124 -0
- package/workspace/lib/headless.js +17 -3
- package/workspace/lib/meeting-material.js +151 -0
- package/workspace/lib/meeting-replay.js +170 -0
- package/workspace/lib/needs-providers.js +1 -0
- package/workspace/lib/notes-screen.js +128 -8
- package/workspace/lib/report-structure-needs.js +64 -0
- package/workspace/lib/report-structure.js +515 -0
- package/workspace/lib/route-modules.js +3 -0
- package/workspace/lib/routes-meeting-focus.js +5 -3
- package/workspace/lib/routes-meeting-material.js +143 -0
- package/workspace/lib/routes-meeting-replay.js +75 -0
- package/workspace/lib/routes-notes.js +37 -4
- package/workspace/lib/routes-report-setups.js +11 -6
- package/workspace/lib/routes-report-structure.js +619 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-calendar.js +15 -3
- package/workspace/public/pm/c3-demo-material.js +58 -0
- package/workspace/public/pm/c3-demo-rd.js +4 -0
- package/workspace/public/pm/c3-demo.js +5 -1
- package/workspace/public/pm/c3-design-studio.js +7 -3
- package/workspace/public/pm/c3-focus.js +83 -16
- package/workspace/public/pm/c3-listen.css +10 -0
- package/workspace/public/pm/c3-material.css +22 -0
- package/workspace/public/pm/c3-material.js +311 -0
- package/workspace/public/pm/c3-notes.js +12 -4
- package/workspace/public/pm/c3-replay.css +127 -0
- package/workspace/public/pm/c3-replay.js +413 -0
- package/workspace/public/pm/c3-report-setup.js +3 -0
- package/workspace/public/pm/c3-report-structure.css +40 -0
- package/workspace/public/pm/c3-report-structure.js +455 -0
- package/workspace/public/pm/c3-reports.css +2 -0
- package/workspace/public/pm/c3-reports.js +22 -3
- package/workspace/public/pm/c3-shell.js +2 -0
- package/workspace/public/pm/dest-notes.js +4 -2
- package/workspace/public/pm/listen-sheet.js +94 -0
- package/workspace/public/pm/listen-sources.js +82 -0
- package/workspace/public/pm/notes-aside.js +62 -0
- package/workspace/public/pm/notes-screen-capture.js +23 -10
- package/workspace/public/pm/notes-screen-view.js +18 -5
- package/workspace/public/pm/notes-shots.js +55 -0
- package/workspace/public/pm/shell-recording.js +10 -2
- package/workspace/public/pm/view-notes.js +48 -8
- package/workspace/server.js +38 -9
- package/workspace/skills/report-source-check.skill +45 -0
- package/workspace/skills/report-structure-dry.skill +51 -0
- package/workspace/skills/report-structure.skill +61 -0
- package/workspace/skills/report-template.skill +10 -0
- package/workspace/test-lists/meeting-material.json +6 -0
- package/workspace/test-lists/meeting-replay.json +6 -0
- package/workspace/test-lists/mm-capture.json +15 -0
- package/workspace/test-lists/report-structure-backend.json +5 -0
- package/workspace/test-lists/report-structure-ui.json +11 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/* c3-report-structure-test.js — the Structure step of the report studio, UI slice (initiative report-structure; mockup 43-reportstructure;
|
|
4
|
+
* API contract planning/prd/report-structure-contract.md). A real server and a real Chromium drive /c3.html?tab=rep; the routes of the
|
|
5
|
+
* contract (/api/report-structure*, /api/charts) and the report reads are answered by Playwright route interception, so the page is
|
|
6
|
+
* tested against the contract alone, not against the backend (c3-report-structure-real-test.js does that).
|
|
7
|
+
*
|
|
8
|
+
* entry the Ask PMOS answer (Shape it with PMOS sets the report up, then opens Structure), a set-up row (Shape the report, before Design
|
|
9
|
+
* how it looks) and the step bar of the Design studio (1 Structure, 2 Design)
|
|
10
|
+
* turn a first turn shows PMOS working, lands S1 with its questions as answer chips; a chip fills the box; a refine turn lands S2; the
|
|
11
|
+
* strip lists both; S1 can be shown again and Go back to S1 posts a revert
|
|
12
|
+
* plan What it answers; Sources in From PMOS and Outside PMOS with their state tags and actions (Open, Test pull, Check it works, How to
|
|
13
|
+
* connect); Charts tagged New chart / From Charts; a row opens to question, Used in, what PMOS reads, why and the test pull;
|
|
14
|
+
* Outline with chart chips and PMOS notes
|
|
15
|
+
* chart Discuss this chart fills the box; Remove it posts a remove and lands the next version
|
|
16
|
+
* source Add a source offers every From PMOS and Outside PMOS kind; a From PMOS source and a Chrome source are added; the Chrome check
|
|
17
|
+
* shows its steps with Needs you and Check again, and passes on the second check
|
|
18
|
+
* options Other structures: three side by side, the recommended first
|
|
19
|
+
* dry Try it with real data runs one job and shows headline, values, flags, could not get, options
|
|
20
|
+
* accept Use this structure posts the shown version; the setup sheet shows the saved structure with Refine the structure
|
|
21
|
+
* charts the Charts tab lists a data chart beside the built-ins with its source and Used in
|
|
22
|
+
* phone at 400 wide none of it scrolls sideways
|
|
23
|
+
* Set RX_SHOTS=<dir> to save screenshots at 1440 and 400 wide. No script error and no failed request may be raised. */
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const { spawn } = require('child_process');
|
|
27
|
+
const { findBrowser } = require('./lib/find-browser');
|
|
28
|
+
const T = require('./test-run').configure({ name: 'c3-report-structure' });
|
|
29
|
+
|
|
30
|
+
const BASE = T.BASE;
|
|
31
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
32
|
+
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); } };
|
|
33
|
+
const results = [];
|
|
34
|
+
const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + String(extra).slice(0, 400) : ''}`); };
|
|
35
|
+
let srv = null; let browser = null;
|
|
36
|
+
const finish = async (code) => {
|
|
37
|
+
if (browser) await browser.close().catch(() => {});
|
|
38
|
+
if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
|
|
39
|
+
T.killTmux();
|
|
40
|
+
const bad = results.filter((x) => !x.cond).length;
|
|
41
|
+
console.log(`\n${results.length - bad}/${results.length} c3-report-structure assertions passed`);
|
|
42
|
+
process.exit(code !== undefined ? code : (bad ? 1 : 0));
|
|
43
|
+
};
|
|
44
|
+
process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
45
|
+
|
|
46
|
+
/* ── the stub: the contract's routes, as a small state machine ── */
|
|
47
|
+
const SETUP = { id: 'app-monthly', name: 'MySPH app performance, monthly', source: 'work', output: 'doc', status: 'active', schedule: { cadence: 'monthly', dayOfMonth: 3, time: '09:00' }, label: 'Monthly on the 3rd 09:00, draft only', next_draft: '2026-11-03', last_issued: null, cycles: 0, copy: null };
|
|
48
|
+
const PROPOSAL = { name: 'MySPH app performance, monthly', purpose: 'Where to invest next quarter.', audience: 'Leadership', source: 'work', output: 'doc', schedule: { cadence: 'monthly', dayOfMonth: 3, time: '09:00' }, sources: ['Google Analytics'], sections: ['Headline'], learned_from: null, fixes: [] };
|
|
49
|
+
const src = (id, kind, name, o) => ({ id, kind, name, via: '', ref: '', use: '', state: 'reached', group: ['connector', 'link', 'upload', 'person', 'chrome', 'computer'].includes(kind) ? 'outside' : 'pmos', ...o });
|
|
50
|
+
const GA = src('ga', 'connector', 'Google Analytics 4: MySPH web + app', { via: 'Read through your Supermetrics connector', use: 'Test pull Mon 12 Oct 22:31. App streams iOS and Android, Sep 2026.' });
|
|
51
|
+
const BOARD = src('board', 'pmos-level', 'Board: MySPH', { via: 'What the report covers', use: 'Its initiatives, releases, key results and risks.' });
|
|
52
|
+
const DASH = src('dash', 'dashboard', 'Dashboard: MySPH health', { via: '2 of its 6 charts used', use: 'New users is already a chart there.' });
|
|
53
|
+
const PAST = src('past', 'past-report', 'Past report: none yet', { via: 'From the second cycle', use: 'From November, PMOS reads the October report.', state: 'later' });
|
|
54
|
+
const BQ = src('bq', 'connector', 'BigQuery: GA4 export', { via: 'Not connected', use: 'Optional. Only needed for retention older than 14 months.', state: 'not-connected' });
|
|
55
|
+
const CH_NEW = { id: 'cpa', chart: null, name: 'Cost per new user', source: 'ga', from: 'GA4 cost', how: 'Paid spend divided by new users from paid campaigns', compare: 'Previous month', flag: 'Up 15% or more', question: 'What do new users cost us?', query: 'GA4 Data API: metric advertiserAdCost; filter sessionMedium = cpc', why: 'Spend explains the drop in new users.' };
|
|
56
|
+
const CH_OLD = { id: 'new-users', chart: 'new-users', name: 'New users', source: 'ga', from: 'GA4 newUsers', how: 'Month total, app streams only', compare: 'Previous month', flag: 'Down 10% or more', question: 'Are we still bringing people in?', query: 'GA4 Data API: metric newUsers', why: 'The top of the funnel.' };
|
|
57
|
+
const CH_RET = { id: 'ret', chart: null, name: 'D30 retention', source: 'ga', from: 'GA4 cohort exploration', how: 'Share of each monthly cohort still active on day 30', compare: 'The last 3 cohorts', flag: 'Below 25%', question: 'Do they stay?', query: 'GA4 Data API runReport with cohortSpec', why: 'You asked for it in S2.' };
|
|
58
|
+
const SEC = (id, title, o) => ({ id, title, fixed: false, charts: [], notes: '', show: '', ...o });
|
|
59
|
+
const OPTIONS = [
|
|
60
|
+
{ name: 'Goals first', sections: ['Headline', 'One block per Q4 objective', 'Where to invest'], good: 'A team that reviews against OKRs.', misses: 'Numbers outside an OKR disappear.', recommended: false },
|
|
61
|
+
{ name: 'Funnel first', sections: ['Headline', 'New users, activation, subscriptions', 'Where to invest'], good: 'Leadership deciding where to invest.', misses: 'Less detail per feature.', recommended: true },
|
|
62
|
+
{ name: 'Features first', sections: ['Headline', 'One block per feature', 'What to build next'], good: 'A product team deciding what to fix next.', misses: 'Leadership has to add the money side.', recommended: false },
|
|
63
|
+
];
|
|
64
|
+
const plan1 = () => ({ purpose: { readers: 'Leadership: Gita Rahma and the product directors', decision: 'Where to invest next quarter', scope: 'MySPH app, iOS and Android', cadence: 'monthly', questions: ['Where do users drop between installing and paying?', 'What should we fund next quarter?'] },
|
|
65
|
+
sources: [BOARD, DASH, PAST, GA, BQ].map((s) => ({ ...s })), charts: [CH_OLD, CH_NEW].map((c) => ({ ...c })),
|
|
66
|
+
outline: [SEC('headline', 'Headline', { fixed: true, notes: 'The one thing leadership must know this month', show: 'Two sentences' }), SEC('funnel', 'The funnel this month', { charts: ['new-users', 'cpa'], notes: 'Where do users drop?', show: 'Funnel chart' }),
|
|
67
|
+
SEC('needs', 'Needs your call', { fixed: true, notes: 'Asks to leadership' }), SEC('sources', 'Sources', { fixed: true, notes: 'Sources reached and not reached' })], options: OPTIONS });
|
|
68
|
+
const Q1 = [{ q: 'Who reads it, and what do they decide with it?', chips: ['Leadership: where to invest next quarter', 'Product team: what to fix next', 'Both'] }, { q: 'Anything that must be in it?', chips: ['Revenue', 'Releases this month', 'Nothing more'] }];
|
|
69
|
+
|
|
70
|
+
function makeStub() {
|
|
71
|
+
const S = { versions: [], plans: {}, log: [], current: null, saved: null, running: null, ticks: 0, dry: null, dryResult: null, checks: {}, posts: [], gets: [], chartsRead: 0, polls: 0, setupMade: 0, designStep: 0 };
|
|
72
|
+
const now = () => '2026-10-12T22:' + String(10 + S.versions.length * 3 + S.log.length).padStart(2, '0') + ':00.000Z';
|
|
73
|
+
const land = (turn, plan, answer, extra) => { const n = S.versions.length + 1; S.versions.push({ n, at: now(), turn }); S.plans[n] = plan; S.current = n; S.log.push({ who: 'pmos', text: answer, at: now(), n, questions: [], options: [], ...(extra || {}) }); };
|
|
74
|
+
const view = () => {
|
|
75
|
+
if (S.running) { S.ticks++; if (S.ticks >= 2) { const r = S.running; S.running = null; r.done(); } }
|
|
76
|
+
return { ok: true, setup: { id: SETUP.id, name: SETUP.name, cadence: 'monthly' }, structure: { running: !!S.running, job: S.running ? { kind: S.running.kind } : null, current: S.current, saved: S.saved, versions: S.versions.slice(), log: S.log.slice(),
|
|
77
|
+
plan: S.current ? S.plans[S.current] : null, dry: S.dry, checks: S.checks, errors: [] } };
|
|
78
|
+
};
|
|
79
|
+
const chromeChecks = (pass) => ({ source: 'looker', kind: 'chrome', at: now(), running: false, ok: pass, steps: [
|
|
80
|
+
{ id: 'mac', label: 'PMOS runs on this Mac', state: 'works', reason: 'The read runs on your MacBook.' }, { id: 'session', label: 'A session can start with Claude in Chrome', state: 'works', reason: 'PMOS started a test session.' },
|
|
81
|
+
{ id: 'extension', label: 'Claude in Chrome answers', state: 'works', reason: 'The extension answered.' },
|
|
82
|
+
{ id: 'signed-in', label: 'You are signed in to Looker Studio', state: pass ? 'works' : 'needs-you', reason: pass ? 'Opened the report as you.' : 'Chrome showed the Google sign-in page. Sign in once in Chrome yourself, then check again.' },
|
|
83
|
+
{ id: 'read', label: 'PMOS can read the numbers', state: pass ? 'works' : 'waiting', reason: pass ? 'Read the table "Spend by campaign".' : 'Waits for the step above.' }] });
|
|
84
|
+
const fixedPlan = (n) => JSON.parse(JSON.stringify(S.plans[n]));
|
|
85
|
+
const route = async (route) => {
|
|
86
|
+
const req = route.request(); const u = new URL(req.url()); const p = u.pathname; const m = req.method();
|
|
87
|
+
const json = (b, status) => route.fulfill({ status: status || 200, contentType: 'application/json', body: JSON.stringify(b) });
|
|
88
|
+
const body = m === 'POST' ? JSON.parse(req.postData() || '{}') : null;
|
|
89
|
+
if (m === 'POST') S.posts.push({ path: p, body });
|
|
90
|
+
if (m === 'GET' && p === '/api/report-structure') { S.polls++; return json(view()); }
|
|
91
|
+
if (m === 'GET' && p === '/api/report-structure/version') return json({ ok: true, plan: S.plans[Number(u.searchParams.get('n'))] });
|
|
92
|
+
if (m === 'GET' && p === '/api/report-structure/dry') return json({ ok: true, dry: S.dryResult });
|
|
93
|
+
if (m === 'GET' && p === '/api/charts') { S.chartsRead++; return json({ ok: true, charts: [{ id: 'new-users', name: 'New users', question: 'Are we still bringing people in?', source: { kind: 'connector', name: 'Google Analytics 4', via: 'Supermetrics' }, from: 'GA4 newUsers', how: 'Month total', compare: 'Previous month', flag: 'Down 10% or more', query: 'GA4 Data API: metric newUsers', why: 'The top of the funnel.', users: [{ kind: 'dashboard', id: 'health', name: 'MySPH health' }, { kind: 'report', id: SETUP.id, name: SETUP.name }] }] }); }
|
|
94
|
+
if (m === 'POST' && p === '/api/report-structure/turn') {
|
|
95
|
+
if (S.running) return json({ ok: false, error: 'a report-structure job is already running' }, 409);
|
|
96
|
+
S.ticks = 0; S.log.push({ who: 'pm', text: body.text, at: now() });
|
|
97
|
+
let done;
|
|
98
|
+
if (!S.versions.length && !S.asked) done = () => { S.asked = true; S.log.push({ who: 'pmos', text: 'Before I propose a structure, two questions.', at: now(), questions: Q1, options: [] }); };
|
|
99
|
+
else if (!S.versions.length) done = () => land('From your answers', plan1(), 'Three ways to shape it. I recommend Funnel first. I drafted it as S1.', { options: OPTIONS });
|
|
100
|
+
else if (/^Test pull/.test(body.text)) done = () => { S.log.push({ who: 'pmos', text: 'Read it: both charts returned a number.', at: now(), questions: [], options: [] }); };
|
|
101
|
+
else done = () => { const pl = fixedPlan(S.current); pl.charts.push({ ...CH_RET }); pl.outline.splice(2, 0, SEC('ret', 'Retention by cohort', { charts: ['ret'], notes: 'Do the people we bring in stay?', show: 'Cohort table' })); land('Retention added', pl, 'S2: D30 retention added.'); };
|
|
102
|
+
S.running = { kind: 'report-structure', done };
|
|
103
|
+
return json({ ok: true, running: true });
|
|
104
|
+
}
|
|
105
|
+
if (m === 'POST' && p === '/api/report-structure/revert') { S.current = body.n; return json({ ok: true, structure: view().structure }); }
|
|
106
|
+
if (m === 'POST' && p === '/api/report-structure/remove') { const pl = fixedPlan(S.current); if (body.kind === 'chart') { pl.charts = pl.charts.filter((c) => c.id !== body.ref); pl.outline.forEach((o) => { o.charts = o.charts.filter((c) => c !== body.ref); }); } else pl.sources = pl.sources.filter((s) => s.id !== body.ref); land('Remove ' + body.ref, pl, 'Removed ' + body.ref + '.'); return json({ ok: true, structure: view().structure }); }
|
|
107
|
+
if (m === 'POST' && p === '/api/report-structure/source') {
|
|
108
|
+
const pl = fixedPlan(S.current); const s = body.source; const grp = ['connector', 'link', 'upload', 'person', 'chrome', 'computer'].includes(s.kind) ? 'outside' : 'pmos';
|
|
109
|
+
pl.sources.push({ via: '', ref: '', ...s, group: grp, state: s.kind === 'chrome' ? 'needs-you' : 'reached' });
|
|
110
|
+
land('Add ' + s.name, pl, s.name + ' added.');
|
|
111
|
+
if (s.kind === 'chrome') S.checks[s.id] = { ...chromeChecks(false), source: s.id };
|
|
112
|
+
return json({ ok: true, structure: view().structure });
|
|
113
|
+
}
|
|
114
|
+
if (m === 'POST' && p === '/api/report-structure/check') { S.checks[body.source] = { ...chromeChecks(true), source: body.source }; const pl = S.plans[S.current]; const s = pl.sources.find((x) => x.id === body.source); if (s) s.state = 'reached'; return json({ ok: true, check: S.checks[body.source] }); }
|
|
115
|
+
if (m === 'POST' && p === '/api/report-structure/dry') { S.ticks = 0; S.running = { kind: 'report-structure-dry', done: () => { S.dry = { run: 'r1', n: body.n || S.current, at: now(), ok: true };
|
|
116
|
+
S.dryResult = { n: S.dry.n, period: '2026-09', headline: 'New users fell 8.9% while cost per new user rose 19%.', values: [{ chart: 'new-users', value: '48,210', was: '52,930 in Aug', delta: '-8.9%', flag: true, note: 'down 10% or more is the rule' }, { chart: 'cpa', value: 'IDR 21,900', was: 'IDR 18,400 in Aug', delta: '+19%', flag: false, note: '' }], missing: [{ source: 'bq', reason: 'BigQuery is not connected.' }], options: ['Fix the first 30 days', 'Restart paid acquisition'] }; } }; return json({ ok: true, running: true }); }
|
|
117
|
+
if (m === 'POST' && p === '/api/report-structure/accept') { S.saved = body.n; return json({ ok: true, structure: view().structure, brief: 'structure/brief.md', charts: ['cpa'] }); }
|
|
118
|
+
if (m === 'GET' && p === '/api/report-setups') return json({ ok: true, setups: S.setupMade ? [SETUP] : [SETUP], findings: [], asks: S.askShown ? [{ id: 'ask1', text: 'Monthly product performance for leadership, numbers from Google Analytics', state: 'ready', proposal: PROPOSAL }] : [], finder: { cadence: 'off' } });
|
|
119
|
+
if (m === 'POST' && p === '/api/report-setups/setup') { S.setupMade++; return json({ ok: true, setup: SETUP, drafted: null }); }
|
|
120
|
+
if (m === 'GET' && p === '/api/report-documents') return json({ ok: true, docs: [] });
|
|
121
|
+
if (m === 'GET' && p === '/api/report-dashboards') return json({ ok: true, library: [{ id: 'north', title: 'North star', question: 'Are we moving?', source: 'PMOS ledgers' }], layouts: {}, defaults: { board: [], initiative: [] }, boards: [], initiatives: [], schedules: [] });
|
|
122
|
+
if (m === 'GET' && p === '/api/report-design') return json({ ok: true, setup: { id: SETUP.id, name: SETUP.name }, design: { running: false, current: null, saved: null, versions: [], log: [], samples: [], description: '', format: 'deck' } });
|
|
123
|
+
return route.continue();
|
|
124
|
+
};
|
|
125
|
+
return { S, route };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
(async () => {
|
|
129
|
+
const exe = findBrowser();
|
|
130
|
+
if (!exe) { console.log('· no Chrome/Chromium on this machine, c3-report-structure suite skipped'); process.exit(0); }
|
|
131
|
+
let chromium;
|
|
132
|
+
try { ({ chromium } = require('playwright-core')); } catch { console.log('· playwright-core not installed, c3-report-structure suite skipped'); process.exit(0); }
|
|
133
|
+
srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
|
|
134
|
+
await T.waitForServer(BASE);
|
|
135
|
+
browser = await chromium.launch({ executablePath: exe });
|
|
136
|
+
require('./lib/c3-full-console').fullConsole(browser); // the desktop layout at phone width (c3-phone-companion)
|
|
137
|
+
const shots = process.env.RX_SHOTS || ''; if (shots) fs.mkdirSync(shots, { recursive: true });
|
|
138
|
+
const shot = async (pg, n) => { if (shots) await pg.screenshot({ path: path.join(shots, n + '.png') }); };
|
|
139
|
+
|
|
140
|
+
const errors = [];
|
|
141
|
+
const stub = makeStub(); const S = stub.S;
|
|
142
|
+
const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
|
143
|
+
await ctx.route(/\/api\/(report-structure|charts|report-setups|report-documents|report-dashboards|report-design)/, stub.route);
|
|
144
|
+
const page = await ctx.newPage();
|
|
145
|
+
page.on('pageerror', (e) => errors.push('pageerror: ' + String((e && e.message) || e)));
|
|
146
|
+
page.on('console', (m) => { if (m.type() === 'error' && !/favicon|Failed to load resource/.test(m.text())) errors.push('console: ' + m.text()); });
|
|
147
|
+
page.on('response', (r) => { if (r.status() >= 400 && !/\/api\/(scan\/(?:today|portfolio)|settings|report-structure)\b/.test(r.url()) && !(r.status() === 409)) errors.push('http ' + r.status() + ' ' + r.url()); });
|
|
148
|
+
page.on('dialog', (d) => d.dismiss().catch(() => {}));
|
|
149
|
+
const text = (sel) => page.textContent(sel).catch(() => '');
|
|
150
|
+
const sent = (p) => S.posts.filter((x) => x.path === p);
|
|
151
|
+
const closeAll = async () => { for (let i = 0; i < 3 && await page.$('.sheet'); i++) { await page.keyboard.press('Escape'); await sleep(250); } };
|
|
152
|
+
const goReports = async () => { await page.goto(BASE + '/c3.html?tab=rep'); await page.waitForSelector('[data-rp3]'); await page.click('[data-rp3-tab="reports"]'); await page.waitForSelector('[data-rs-reg="' + SETUP.id + '"]', { timeout: 15000 }); };
|
|
153
|
+
const idle = () => page.waitForFunction(() => { const b = document.querySelector('[data-st-act="send"]'); return b && !b.disabled && !document.querySelector('[data-st-running]'); }, null, { timeout: 15000 });
|
|
154
|
+
const noOverflow = () => page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1);
|
|
155
|
+
|
|
156
|
+
/* ── entry points ── */
|
|
157
|
+
S.askShown = true;
|
|
158
|
+
await goReports();
|
|
159
|
+
ok('the set-up row has Shape the report before Design how it looks', await page.$$eval('[data-rs-reg="' + SETUP.id + '"] .acts button', (a) => { const t = a.map((b) => b.textContent.trim()); return t.indexOf('Shape the report') > -1 && t.indexOf('Shape the report') < t.indexOf('Design how it looks'); }));
|
|
160
|
+
await page.waitForSelector('[data-rs-card="ask1"]');
|
|
161
|
+
ok('an Ask PMOS answer has Shape it with PMOS', !!(await page.$('[data-rs-card="ask1"] [data-rs-act="shape"]')));
|
|
162
|
+
await page.click('[data-rs-card="ask1"] [data-rs-act="shape"]');
|
|
163
|
+
await page.waitForSelector('[data-st-studio]');
|
|
164
|
+
ok('Shape it with PMOS sets the report up, then opens the Structure step', sent('/api/report-setups/setup').length === 1 && sent('/api/report-setups/setup')[0].body.ask === 'ask1' && /Not saved yet/.test(await text('[data-st-studio]')));
|
|
165
|
+
ok('the step bar says 1 Structure, 2 Design with Structure pressed', await page.getAttribute('[data-st-step-btn="structure"]', 'aria-pressed') === 'true' && /2 Design/.test(await text('[data-st-steps]')));
|
|
166
|
+
ok('with no version yet the studio asks what the report is for and the Use button waits', !!(await page.$('[data-st-studio] .fold')) && await page.$eval('[data-st-act="save"]', (b) => b.disabled));
|
|
167
|
+
await closeAll();
|
|
168
|
+
S.askShown = false;
|
|
169
|
+
await goReports();
|
|
170
|
+
await page.click('[data-rs-reg="' + SETUP.id + '"] [data-rs-act="shape"]');
|
|
171
|
+
await page.waitForSelector('[data-st-studio]');
|
|
172
|
+
ok('Shape the report on the row opens the same studio', /Not saved yet/.test(await text('[data-st-studio]')));
|
|
173
|
+
await closeAll();
|
|
174
|
+
await page.click('[data-rs-reg="' + SETUP.id + '"] [data-rs-act="design"]');
|
|
175
|
+
await page.waitForSelector('[data-rd-studio]');
|
|
176
|
+
ok('the Design studio shows the step bar with 2 Design pressed', await page.getAttribute('[data-rd-step="design"]', 'aria-pressed') === 'true' && await page.getAttribute('[data-rd-step="structure"]', 'aria-pressed') === 'false');
|
|
177
|
+
await page.click('[data-rd-step="structure"]');
|
|
178
|
+
await page.waitForSelector('[data-st-studio]');
|
|
179
|
+
ok('1 Structure in the Design studio opens the Structure step', !!(await page.$('[data-st-steps]')));
|
|
180
|
+
await page.click('[data-st-step-btn="design"]');
|
|
181
|
+
await page.waitForSelector('[data-rd-studio]');
|
|
182
|
+
ok('2 Design in the Structure step opens the Design studio', true);
|
|
183
|
+
await page.click('[data-rd-step="structure"]');
|
|
184
|
+
await page.waitForSelector('[data-st-studio]');
|
|
185
|
+
|
|
186
|
+
/* ── a first turn: PMOS asks, answer chips ── */
|
|
187
|
+
await page.fill('[data-st-in]', 'I need a monthly product performance report for leadership. Get the numbers from Google Analytics.');
|
|
188
|
+
await page.click('[data-st-act="send"]');
|
|
189
|
+
ok('a turn posts the words as ONE turn', !!(await until(() => sent('/api/report-structure/turn').length === 1)) && /monthly product performance/.test(sent('/api/report-structure/turn')[0].body.text));
|
|
190
|
+
ok('while the job runs the conversation says PMOS is working and Make the next version waits', !!(await until(() => page.$('[data-st-running]'))));
|
|
191
|
+
await page.waitForSelector('[data-st-question]', { timeout: 15000 });
|
|
192
|
+
ok('PMOS asks its questions with answer chips', (await page.$$('[data-st-question]')).length === 2 && (await page.$$('[data-st-question] [data-st-act="chip"]')).length === 6);
|
|
193
|
+
await page.click('[data-st-act="chip"][data-v="Leadership: where to invest next quarter"]');
|
|
194
|
+
ok('a chip puts its answer in the box', (await page.inputValue('[data-st-in]')) === 'Leadership: where to invest next quarter.');
|
|
195
|
+
await page.click('[data-st-act="chip"][data-v="Revenue"]');
|
|
196
|
+
ok('a second chip adds to the box', /next quarter\. Revenue\.$/.test(await page.inputValue('[data-st-in]')));
|
|
197
|
+
await page.click('[data-st-act="send"]');
|
|
198
|
+
await page.waitForSelector('[data-st-v="1"]', { timeout: 15000 });
|
|
199
|
+
ok('the second turn lands S1', (await page.$$('[data-st-v]')).length === 1 && await page.getAttribute('[data-st-v="1"]', 'aria-pressed') === 'true');
|
|
200
|
+
await page.waitForSelector('[data-st-purpose]');
|
|
201
|
+
ok('the plan shows What it answers with readers, decision, scope and the questions', /Leadership: Gita Rahma/.test(await text('[data-st-purpose]')) && /Where to invest next quarter/.test(await text('[data-st-purpose]')) && /Where do users drop/.test(await text('[data-st-purpose]')));
|
|
202
|
+
|
|
203
|
+
/* ── sources ── */
|
|
204
|
+
const grp = (g) => page.$$eval('[data-st-group="' + g + '"] + ul > li', (a) => a.map((li) => li.getAttribute('data-st-src')));
|
|
205
|
+
ok('Sources are grouped: From PMOS and Outside PMOS', JSON.stringify(await grp('pmos')) === '["board","dash","past"]' && JSON.stringify(await grp('outside')) === '["ga","bq"]', JSON.stringify([await grp('pmos'), await grp('outside')]));
|
|
206
|
+
ok('each source shows its state', await page.getAttribute('[data-st-src="ga"] [data-st-state]', 'data-st-state') === 'reached' && /Not connected/.test(await text('[data-st-src="bq"] [data-st-state]')) && /First cycle: nothing to read/.test(await text('[data-st-src="past"] [data-st-state]')));
|
|
207
|
+
ok('the actions fit the source: Open, Test pull, How to connect', /Open/.test(await text('[data-st-src="board"] .acts')) && /Test pull/.test(await text('[data-st-src="ga"] .acts')) && /How to connect/.test(await text('[data-st-src="bq"] .acts')));
|
|
208
|
+
ok('a source says how many charts use it', /2 charts/.test(await text('[data-st-src="ga"]')) && !/ chart/.test(await text('[data-st-src="board"] .chips')));
|
|
209
|
+
await page.click('[data-st-src="ga"] [data-st-act="test"]');
|
|
210
|
+
ok('Test pull asks PMOS to read that source (one turn)', !!(await until(() => sent('/api/report-structure/turn').length === 3)) && /Test pull from Google Analytics 4/.test(sent('/api/report-structure/turn')[2].body.text));
|
|
211
|
+
await idle();
|
|
212
|
+
await page.click('[data-st-src="bq"] [data-st-act="connect"]');
|
|
213
|
+
ok('How to connect says where to connect it', /Settings, Connectors/.test(await text('[data-c3-toast]')));
|
|
214
|
+
|
|
215
|
+
/* ── charts ── */
|
|
216
|
+
ok('Charts are tagged New chart and From Charts', await text('[data-st-chart="cpa"] [data-st-tag]') === 'New chart' && await text('[data-st-chart="new-users"] [data-st-tag]') === 'From Charts' && /1 new, 1 from Charts/.test(await text('[data-st-metrics]')));
|
|
217
|
+
await page.click('[data-st-chart="new-users"] [data-st-act="chart"]');
|
|
218
|
+
await page.waitForSelector('[data-st-detail="new-users"]');
|
|
219
|
+
ok('a chart row opens to question, used in, what PMOS reads and why', /Are we still bringing people in/.test(await text('[data-st-q]')) && /GA4 Data API: metric newUsers/.test(await text('[data-st-query]')) && /top of the funnel/.test(await text('[data-st-why]')));
|
|
220
|
+
ok('Used in names the dashboard and report that use a chart from Charts', !!(await until(async () => /dashboard MySPH health/.test(await text('[data-st-used]')) && /this report/.test(await text('[data-st-used]')))), await text('[data-st-used]'));
|
|
221
|
+
ok('a chart from Charts has Open in Charts, a new chart does not', !!(await page.$('[data-st-detail="new-users"] [data-st-act="opencharts"]')));
|
|
222
|
+
await page.click('[data-st-chart="cpa"] [data-st-act="chart"]');
|
|
223
|
+
ok('only one row is open and a new chart says it is saved in Charts when you use the structure', !!(await page.$('[data-st-detail="cpa"]')) && !(await page.$('[data-st-detail="new-users"]')) && /Saved in Charts when you use this structure/.test(await text('[data-st-used]')) && !(await page.$('[data-st-detail="cpa"] [data-st-act="opencharts"]')));
|
|
224
|
+
ok('the test pull says it is not tried yet', /Not tried on this version/.test(await text('[data-st-testpull]')));
|
|
225
|
+
await shot(page, 'structure-logic-1440');
|
|
226
|
+
await page.click('[data-st-detail="cpa"] [data-st-act="discuss"]');
|
|
227
|
+
ok('Discuss this chart fills the box with the chart name and focuses it', (await page.inputValue('[data-st-in]')) === 'About Cost per new user: ' && await page.evaluate(() => document.activeElement && document.activeElement.hasAttribute('data-st-in')));
|
|
228
|
+
|
|
229
|
+
/* ── the outline ── */
|
|
230
|
+
ok('the Outline lists the sections in order with chart chips and PMOS notes', JSON.stringify(await page.$$eval('[data-st-section]', (a) => a.map((x) => x.getAttribute('data-st-section')))) === '["headline","funnel","needs","sources"]'
|
|
231
|
+
&& /New users/.test(await text('[data-st-section="funnel"]')) && /PMOS notes/.test(await text('[data-st-section="funnel"]')) && !/PMOS notes/.test(await text('[data-st-section="headline"]')) && /In every report/.test(await text('[data-st-section="headline"]')));
|
|
232
|
+
|
|
233
|
+
/* ── a refine turn: S2 ── */
|
|
234
|
+
await idle();
|
|
235
|
+
await page.fill('[data-st-in]', 'Add retention by cohort.');
|
|
236
|
+
await page.click('[data-st-act="send"]');
|
|
237
|
+
await page.waitForSelector('[data-st-v="2"]', { timeout: 15000 });
|
|
238
|
+
ok('a refine turn lands S2 and the strip lists S1 and S2', (await page.$$('[data-st-v]')).length === 2 && await page.getAttribute('[data-st-v="2"]', 'aria-pressed') === 'true');
|
|
239
|
+
await page.waitForSelector('[data-st-chart="ret"]');
|
|
240
|
+
ok('S2 has the retention chart and a section that says New in S2', !!(await page.$('[data-st-chart="ret"]')) && /New in S2/.test(await text('[data-st-section="ret"]')) && !/New in S2/.test(await text('[data-st-section="funnel"]')));
|
|
241
|
+
await shot(page, 'structure-1440');
|
|
242
|
+
await page.$eval('[data-st-sources]', (n) => n.scrollIntoView({ block: 'start' })); await sleep(200); await shot(page, 'structure-sources-1440');
|
|
243
|
+
ok('the conversation names each version with a link', (await page.$$('[data-st-log] [data-st-act="ver"]')).length === 2);
|
|
244
|
+
await page.click('[data-st-v="1"]');
|
|
245
|
+
await until(async () => await page.getAttribute('[data-st-v="1"]', 'aria-pressed') === 'true' && !(await page.$('[data-st-chart="ret"]')));
|
|
246
|
+
ok('S1 shows again without the retention chart and says it is an older version', !(await page.$('[data-st-chart="ret"]')) && /older version/.test(await text('[data-st-vcap]')));
|
|
247
|
+
await page.click('[data-st-act="revert"]');
|
|
248
|
+
ok('Go back to S1 posts a revert for n 1', !!(await until(() => sent('/api/report-structure/revert').length === 1)) && sent('/api/report-structure/revert')[0].body.n === 1);
|
|
249
|
+
await until(async () => !(await page.$('[data-st-act="revert"]')));
|
|
250
|
+
await page.click('[data-st-v="2"]');
|
|
251
|
+
await page.waitForSelector('[data-st-chart="ret"]');
|
|
252
|
+
await page.click('[data-st-act="revert"]');
|
|
253
|
+
ok('going back to S2 the same way makes it current again', !!(await until(() => sent('/api/report-structure/revert').length === 2)) && sent('/api/report-structure/revert')[1].body.n === 2);
|
|
254
|
+
await until(async () => !(await page.$('[data-st-act="revert"]')));
|
|
255
|
+
|
|
256
|
+
/* ── Remove it ── */
|
|
257
|
+
await page.click('[data-st-chart="ret"] [data-st-act="chart"]');
|
|
258
|
+
await page.click('[data-st-detail="ret"] [data-st-act="drop"]');
|
|
259
|
+
ok('Remove it posts a remove of that chart', !!(await until(() => sent('/api/report-structure/remove').length === 1)) && sent('/api/report-structure/remove')[0].body.kind === 'chart' && sent('/api/report-structure/remove')[0].body.ref === 'ret');
|
|
260
|
+
await page.waitForSelector('[data-st-v="3"]', { timeout: 10000 });
|
|
261
|
+
ok('removing makes the next version, S3, without the chart', !(await page.$('[data-st-chart="ret"]')) && (await page.$$('[data-st-v]')).length === 3);
|
|
262
|
+
|
|
263
|
+
/* ── Add a source ── */
|
|
264
|
+
await page.click('[data-st-act="panel"][aria-expanded]');
|
|
265
|
+
await page.waitForSelector('[data-st-panel]');
|
|
266
|
+
const optKinds = await page.$$eval('[data-st-panel] [data-st-act="opt"]', (a) => a.map((b) => b.getAttribute('data-v')));
|
|
267
|
+
ok('Add a source offers every From PMOS and Outside PMOS kind', JSON.stringify(optKinds) === JSON.stringify(['pmos-level', 'dashboard', 'chart', 'document', 'past-report', 'connector', 'link', 'upload', 'person', 'chrome', 'computer']), optKinds.join(','));
|
|
268
|
+
ok('the panel has the two groups and says PMOS only reads', /From PMOS/.test(await text('[data-st-panel]')) && /Outside PMOS/.test(await text('[data-st-panel]')) && /PMOS only reads/.test(await text('[data-st-panel]')));
|
|
269
|
+
await shot(page, 'structure-pmos-1440');
|
|
270
|
+
await page.click('[data-st-panel] [data-st-act="opt"][data-v="document"]');
|
|
271
|
+
await page.waitForSelector('[data-st-form="document"]');
|
|
272
|
+
await page.click('[data-st-form="document"] [data-st-act="add"]');
|
|
273
|
+
ok('a form needs its name before it adds', sent('/api/report-structure/source').length === 0);
|
|
274
|
+
await page.fill('[data-st-form="document"] [data-st-field="name"]', 'MySPH strategy 2026');
|
|
275
|
+
await page.click('[data-st-form="document"] [data-st-act="add"]');
|
|
276
|
+
ok('a From PMOS source (a document) posts a source of that kind', !!(await until(() => sent('/api/report-structure/source').length === 1)) && sent('/api/report-structure/source')[0].body.source.kind === 'document' && sent('/api/report-structure/source')[0].body.source.name === 'MySPH strategy 2026' && !('state' in sent('/api/report-structure/source')[0].body.source));
|
|
277
|
+
await page.waitForSelector('[data-st-v="4"]', { timeout: 10000 });
|
|
278
|
+
ok('it joins the From PMOS group in S4', JSON.stringify(await grp('pmos')) === '["board","dash","past","mysph-strategy-2026"]', JSON.stringify(await grp('pmos')));
|
|
279
|
+
await page.click('[data-st-act="panel"][aria-expanded]');
|
|
280
|
+
await page.click('[data-st-panel] [data-st-act="opt"][data-v="chrome"]');
|
|
281
|
+
await page.waitForSelector('[data-st-form="chrome"]');
|
|
282
|
+
await page.fill('[data-st-form="chrome"] [data-st-field="name"]', 'Looker Studio: Paid acquisition');
|
|
283
|
+
await page.fill('[data-st-form="chrome"] [data-st-field="ref"]', 'not a link');
|
|
284
|
+
await page.click('[data-st-form="chrome"] [data-st-act="add"]');
|
|
285
|
+
ok('a Chrome page needs a link that starts with https://', /starts with https/.test(await text('[data-c3-toast]')) && !S.posts.some((x) => x.body && x.body.source && x.body.source.kind === 'chrome'));
|
|
286
|
+
await page.fill('[data-st-form="chrome"] [data-st-field="ref"]', 'https://lookerstudio.google.com/reporting/abc');
|
|
287
|
+
await page.click('[data-st-form="chrome"] [data-st-act="add"]');
|
|
288
|
+
await page.waitForSelector('[data-st-check]', { timeout: 10000 });
|
|
289
|
+
const lid = sent('/api/report-structure/source').pop().body.source.id;
|
|
290
|
+
ok('a Chrome source shows its check step by step', (await page.$$('[data-st-check] [data-st-step]')).length === 5 && await page.getAttribute('[data-st-check] [data-st-step="signed-in"]', 'data-st-step-state') === 'needs-you');
|
|
291
|
+
ok('a failing step says Needs you with its reason; the steps after it Wait', /Needs you/.test(await text('[data-st-step="signed-in"]')) && /Sign in once in Chrome/.test(await text('[data-st-step="signed-in"]')) && /Waiting/.test(await text('[data-st-step="read"]')) && /Works/.test(await text('[data-st-step="mac"]')));
|
|
292
|
+
ok('the check says one step needs you, what the session may do, and the 4 hour wait', /1 step needs you/.test(await text('[data-st-check-state]')) && /Only the browser tools/.test(await text('[data-st-check]')) && /up to 4 hours/.test(await text('[data-st-check]')));
|
|
293
|
+
ok('the source row is in Outside PMOS with Needs you and Check it works', /Needs you/.test(await text('[data-st-src="' + lid + '"] [data-st-state]')) && /Check it works/.test(await text('[data-st-src="' + lid + '"] .acts')) && /Re-checked 1 hour before each draft/.test(await text('[data-st-src="' + lid + '"]')));
|
|
294
|
+
await shot(page, 'structure-browser-1440');
|
|
295
|
+
ok('Check again is offered', /Check again/.test(await text('[data-st-check] [data-st-act="chkagain"]')));
|
|
296
|
+
await page.click('[data-st-check] [data-st-act="chkagain"]');
|
|
297
|
+
ok('Check again posts a check for that source', !!(await until(() => sent('/api/report-structure/check').length === 1)) && sent('/api/report-structure/check')[0].body.source === lid);
|
|
298
|
+
await until(async () => /All steps work/.test(await text('[data-st-check-state]')));
|
|
299
|
+
ok('the second check passes: every step Works and the source is Reached', /All steps work/.test(await text('[data-st-check-state]')) && (await page.$$eval('[data-st-check] [data-st-step-state="works"]', (a) => a.length)) === 5 && /Reached/.test(await text('[data-st-src="' + lid + '"] [data-st-state]')));
|
|
300
|
+
await shot(page, 'structure-browser-ok-1440');
|
|
301
|
+
await page.click('[data-st-src="' + lid + '"] [data-st-act="chkopen"]');
|
|
302
|
+
ok('Check it works on the row posts a check', !!(await until(() => sent('/api/report-structure/check').length === 2)));
|
|
303
|
+
|
|
304
|
+
/* ── Other structures ── */
|
|
305
|
+
await page.click('[data-st-view="options"]');
|
|
306
|
+
await page.waitForSelector('[data-st-options]');
|
|
307
|
+
ok('Other structures shows three side by side with the recommended first', JSON.stringify(await page.$$eval('[data-st-option]', (a) => a.map((x) => x.getAttribute('data-st-option')))) === '["Funnel first","Goals first","Features first"]' && /Recommended/.test(await text('[data-st-option="Funnel first"]')));
|
|
308
|
+
ok('each option has its sections, good for and misses', /GOOD FOR/.test(await text('[data-st-option="Goals first"]')) && /MISSES/.test(await text('[data-st-option="Goals first"]')) && (await page.$$('[data-st-option="Goals first"] li')).length === 3);
|
|
309
|
+
const cols = await page.$$eval('[data-st-option]', (a) => a.map((x) => Math.round(x.getBoundingClientRect().top)));
|
|
310
|
+
ok('the three options sit in one row at 1440 wide', cols.every((t) => Math.abs(t - cols[0]) < 4), cols.join(','));
|
|
311
|
+
await shot(page, 'structure-options-1440');
|
|
312
|
+
await page.click('[data-st-option="Goals first"] [data-st-act="choose"]');
|
|
313
|
+
ok('Use this instead puts the choice in the box and goes back to the plan', (await page.inputValue('[data-st-in]')) === 'Use the "Goals first" structure instead.' && await page.getAttribute('[data-st-view="plan"]', 'aria-pressed') === 'true');
|
|
314
|
+
await page.fill('[data-st-in]', '');
|
|
315
|
+
|
|
316
|
+
/* ── Try it with real data ── */
|
|
317
|
+
await page.click('[data-st-view="dry"]');
|
|
318
|
+
await page.waitForSelector('[data-st-nodry]');
|
|
319
|
+
ok('Try it with real data explains that it reads only and saves nothing', /saves nothing/.test(await text('[data-st-nodry]')));
|
|
320
|
+
await page.click('[data-st-act="dryrun"]');
|
|
321
|
+
ok('the run posts ONE dry job for the version on screen', !!(await until(() => sent('/api/report-structure/dry').length === 1)) && sent('/api/report-structure/dry')[0].body.n === 5);
|
|
322
|
+
ok('while it runs the view says so and nothing is saved', !!(await until(() => page.$('[data-st-dryrunning]'))));
|
|
323
|
+
await page.waitForSelector('[data-st-dry-headline]', { timeout: 15000 });
|
|
324
|
+
ok('the result shows the headline PMOS would write', /New users fell 8\.9%/.test(await text('[data-st-dry-headline]')));
|
|
325
|
+
ok('the values show each chart with its comparison', /New users/.test(await text('[data-st-dry-values]')) && /48,210/.test(await text('[data-st-dry-values]')) && /52,930 in Aug/.test(await text('[data-st-dry-values]')) && /\+19%/.test(await text('[data-st-dry-values]')));
|
|
326
|
+
ok('a flagged value is flagged and listed under Flags', /Flagged/.test(await text('[data-st-dry-values]')) && /New users/.test(await text('[data-st-dry-flags]')));
|
|
327
|
+
ok('what it could not get names the source and why', /BigQuery/.test(await text('[data-st-dry-missing]')) && /not connected/.test(await text('[data-st-dry-missing]')));
|
|
328
|
+
ok('the options PMOS would propose show, with the note that the PM decides', /Fix the first 30 days/.test(await text('[data-st-dry-options]')) && /PMOS never decides them/.test(await text('[data-st-dry-options]')));
|
|
329
|
+
await shot(page, 'structure-dry-1440');
|
|
330
|
+
await page.click('[data-st-view="plan"]');
|
|
331
|
+
await page.click('[data-st-chart="cpa"] [data-st-act="chart"]');
|
|
332
|
+
ok('a chart row now shows its test pull value', /IDR 21,900/.test(await text('[data-st-testpull]')) && /\+19%/.test(await text('[data-st-testpull]')));
|
|
333
|
+
|
|
334
|
+
/* ── Use this structure ── */
|
|
335
|
+
await page.click('[data-st-act="save"]');
|
|
336
|
+
ok('Use this structure posts the version on screen', !!(await until(() => sent('/api/report-structure/accept').length === 1)) && sent('/api/report-structure/accept')[0].body.n === 5);
|
|
337
|
+
await page.waitForSelector('[data-rd-brief]', { timeout: 10000 });
|
|
338
|
+
await page.waitForSelector('[data-st-saved-wrap]');
|
|
339
|
+
ok('the report setup sheet shows the saved structure beside the Design', /Structure S5/.test(await text('[data-st-saved-tag]')) && !!(await page.$('[data-rd-design]')));
|
|
340
|
+
ok('the saved structure lists what it answers, the sources, the charts and the outline', /Where to invest next quarter/.test(await text('[data-st-answers]')) && /MySPH strategy 2026/.test(await text('[data-st-brief]')) && /Cost per new user/.test(await text('[data-st-brief]')) && /Needs your call/.test(await text('[data-st-brief]')));
|
|
341
|
+
ok('it offers Refine the structure and Try it with real data', /Refine the structure/.test(await text('[data-st-brief]')) && /Try it with real data/.test(await text('[data-st-brief]')));
|
|
342
|
+
ok('it says a change applies from the next cycle and the Chrome source is re-checked', /applies from the next cycle/.test(await text('[data-st-saved-wrap]')) && /Looker Studio: Paid acquisition/.test(await text('[data-st-saved-wrap]')) && /waits up to 4 hours/.test(await text('[data-st-saved-wrap]')));
|
|
343
|
+
await shot(page, 'structure-saved-1440');
|
|
344
|
+
await page.click('[data-st-brief] [data-st-act="opendry"]');
|
|
345
|
+
await page.waitForSelector('[data-st-studio]');
|
|
346
|
+
ok('Try it with real data in the setup sheet opens the studio on that view', await page.getAttribute('[data-st-view="dry"]', 'aria-pressed') === 'true' && /Saved structure: S5/.test(await text('[data-st-studio]')));
|
|
347
|
+
await closeAll();
|
|
348
|
+
|
|
349
|
+
/* ── the Charts tab ── */
|
|
350
|
+
await goReports();
|
|
351
|
+
await page.click('[data-rp3-tab="charts"]');
|
|
352
|
+
await page.waitForSelector('[data-rp3-data-chart="new-users"]');
|
|
353
|
+
ok('the Charts tab lists a data chart beside the built-in ones', !!(await page.$('[data-rp3-lib] [data-home="north"]')) && !!(await page.$('[data-rp3-lib] [data-rp3-data-chart="new-users"]')) && /Data chart/.test(await text('[data-rp3-data-chart="new-users"]')));
|
|
354
|
+
ok('the data chart shows its source and Used in', /Google Analytics 4 through Supermetrics/.test(await text('[data-rp3-source="new-users"]')) && /Used in: .*MySPH health.*MySPH app performance, monthly/.test(await text('[data-rp3-data-chart="new-users"] [data-rp3-used]')));
|
|
355
|
+
ok('the Charts tab counts both kinds', /2/.test(await text('[data-rp3-tab="charts"]')));
|
|
356
|
+
await page.click('[data-rp3-data-chart="new-users"] .tt').catch(() => {});
|
|
357
|
+
await shot(page, 'structure-charts-tab-1440');
|
|
358
|
+
|
|
359
|
+
/* ── the saved structure and the studio at 400 wide ── */
|
|
360
|
+
await page.setViewportSize({ width: 400, height: 900 });
|
|
361
|
+
await goReports();
|
|
362
|
+
ok('at 400 wide the Reports tab does not scroll sideways', await noOverflow());
|
|
363
|
+
await page.click('[data-rs-reg="' + SETUP.id + '"] [data-rs-act="shape"]');
|
|
364
|
+
await page.waitForSelector('[data-st-chart]');
|
|
365
|
+
ok('at 400 wide the Structure step does not scroll sideways', await noOverflow());
|
|
366
|
+
await shot(page, 'structure-400');
|
|
367
|
+
await page.click('[data-st-chart="cpa"] [data-st-act="chart"]');
|
|
368
|
+
await page.click('[data-st-act="panel"][aria-expanded]');
|
|
369
|
+
await page.waitForSelector('[data-st-panel]');
|
|
370
|
+
ok('at 400 wide the open chart and the Add a source panel do not scroll the page sideways', await noOverflow());
|
|
371
|
+
await shot(page, 'structure-pmos-400');
|
|
372
|
+
await page.click('[data-st-view="options"]');
|
|
373
|
+
ok('at 400 wide Other structures stack and do not scroll sideways', await noOverflow());
|
|
374
|
+
await shot(page, 'structure-options-400');
|
|
375
|
+
await page.click('[data-st-view="dry"]');
|
|
376
|
+
ok('at 400 wide the dry run does not scroll sideways', await noOverflow());
|
|
377
|
+
await shot(page, 'structure-dry-400');
|
|
378
|
+
await page.setViewportSize({ width: 1440, height: 1000 });
|
|
379
|
+
|
|
380
|
+
ok('no script error and no failed request', errors.length === 0, errors.join(' | '));
|
|
381
|
+
await finish();
|
|
382
|
+
})().catch((e) => { console.error(e); finish(1); });
|
|
@@ -156,6 +156,20 @@ To fix the topic, press **Topics**. Press **Not this topic** and pick the topic
|
|
|
156
156
|
|
|
157
157
|
The companion starts only while the meeting is recorded. It reads the new lines of the transcript at most every 40 seconds. It writes the sentence, the chips, the topic and the notes. If the answer is not usable, PMOS drops it and shows nothing. To stop the companion, set `PMOS_FOCUS_AGENT` to `off`. The page then shows the transcript and a quiet line, and it adds no notes. In the last 5 minutes, and after the meeting, the page shows the captured items under the material. You confirm them in the note after the meeting. Nothing goes to other people.
|
|
158
158
|
|
|
159
|
+
### Meeting material
|
|
160
|
+
|
|
161
|
+
Link the documents, reports and decks that you want to show or present in a meeting. The card of a meeting that has not ended has a **Material** part. It lists each linked item with its kind, the topic it is for, where it came from (the invite, the morning brief, or you), and whether it is for this meeting or for every meeting in the series. Each item has **Present** or **Open**, and a remove button (x). Items from the invite and the morning brief have no remove button. PMOS keeps your items in its own record (`meeting-material.jsonl` in the runtime folder). It does not change your calendar invite. Removing an item does not change the document.
|
|
162
|
+
|
|
163
|
+
Press **Add material** to open the sheet:
|
|
164
|
+
|
|
165
|
+
- **Suggested for this meeting**: the documents of the level of the meeting (the initiative it is about) and the newest reports and decks, with the reason for each.
|
|
166
|
+
- **Find in PMOS**: type two letters or more to search the titles of documents and report decks.
|
|
167
|
+
- **Or paste a link**: a Google Slides, Google Docs or Confluence link, or any https address. A web page opens in a new tab. PMOS does not read it.
|
|
168
|
+
|
|
169
|
+
After you pick an item, choose **For** (this meeting, or every meeting in the series), **Topic** (one of the topics of the meeting, or the whole meeting) and **You** (**Present it** or **Only read it**, for a PMOS deck or a Google Slides link).
|
|
170
|
+
|
|
171
|
+
In **Focus**, the material shows next to the links of the invite. An item for a topic comes up on the stage when the room is on that topic. When the room moves to another topic, the stage goes back to the first link. Press an item in the **Material** switch to keep it on the stage. A PMOS deck or report opens in PMOS. A PMOS deck has **Present**, which opens the presenter view with your speaker notes, and **Audience view**, which shows the slides only, full screen. Press it before you share your screen in the call. When nothing is linked, the Material part of Focus has **Add material**.
|
|
172
|
+
|
|
159
173
|
### Meeting focus over Google Meet
|
|
160
174
|
|
|
161
175
|
The PMOS Chrome extension shows a small panel over a Google Meet call, so you do not need to switch to the PMOS tab. The panel is the Meeting focus page in a compact form. It has a head with the topic the room is on (or "Starts in" before the start) and the time left, the **What's being said** line, the conversation with PMOS, and **Ask PMOS quietly**. Only you see it. It is not part of the screen you share. The extension reads the address and the title of the Meet tab only. It does not read the call's audio or captions, and it talks only to the PMOS on your computer.
|
package/workspace/guide/index.md
CHANGED
|
@@ -74,6 +74,8 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
74
74
|
- [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.
|
|
75
75
|
- [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.
|
|
76
76
|
- [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.
|
|
77
|
+
- [Shape what a report says: the Structure step](report-structure.md): how to decide with PMOS what a report answers, where each number comes from and how it is worked out, add a page in Chrome or an app as a source, and try the plan on real data.
|
|
78
|
+
- [Shape what a report says: the Structure step](report-structure.md): how to decide with PMOS what a report answers, where each number comes from and how it is worked out, add a page in Chrome or an app as a source, and try the plan on real data.
|
|
77
79
|
- [Design how a report looks, and read it as a deck](report-design.md): how to design a report in the design studio, read each cycle as a deck or an interactive document, present it, and use speaker notes and notes on the slide.
|
|
78
80
|
- [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.
|
|
79
81
|
- [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.
|
package/workspace/guide/notes.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: notes"
|
|
4
4
|
description: "How to take notes in a meeting: one note top to bottom, typed notes as timed lines, key points written each minute and labelled by Jev, delete a note; final notes written after Stop from the whole recording, with speakers and a quote for each item, decisions with their why and who, next steps with an open question, a discussion paragraph, and Wispr Flow's notes merged in and fetched by hand when the fetch fails; listen to your microphone and the meeting on this machine, see decisions, actions and questions caught as they are said, check them against the repository for conflicts, capture highlights during the meeting, create tasks from next steps, correct and tag lines, see what changed in your notes, replay the meeting, see screenshots of the shared screen beside the transcript, link a note to its meeting and its initiative, and get a summary."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-14
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: notes
|
|
@@ -201,6 +201,18 @@ The replay of a new note is kept at 24 kHz, so it keeps the high sounds of a voi
|
|
|
201
201
|
|
|
202
202
|
The replay is your microphone and the meeting added together. When both are loud at the same moment, the workspace lowers the peaks softly, so the sound does not distort. A quiet voice is not changed. The transcript is made from each source on its own, so this does not change what is written.
|
|
203
203
|
|
|
204
|
+
### The meeting replay page
|
|
205
|
+
|
|
206
|
+
Press **Replay** beside the player (in Calendar › Notes, or on the meeting's card once the meeting has ended) to open the meeting replay: the whole meeting on one full-screen page, as Focus showed it, played back. Press **Stop and open the replay** in Focus to end the meeting and open it at once.
|
|
207
|
+
|
|
208
|
+
- The **topic track** is the bar to scrub. Each stretch of talk is a numbered stretch of the bar. A mark above it is a decision, a question or a next step. A mark is amber when it needs you: a next step with no owner, a question still open. A small dot below it is a note PMOS made for you during the meeting.
|
|
209
|
+
- **Play** or **Pause**, **Back 10 s**, and **1×**, **1.5×** or **2×** set how it plays. A press on a stretch, a mark, a key point, a topic, a screen shot or a line plays from there.
|
|
210
|
+
- The **stage** shows the screen shot that was on screen at the playing time. Press it to see it large. A shot that looked like another app was set aside: it is not in the replay, and **N set aside** opens them so you can keep or delete each one.
|
|
211
|
+
- **Key points** lists the decisions, next steps and questions with the time each was said. A key point whose line is not found has no time and no play button. **Confirm all** makes a task of each next step on the note's initiative.
|
|
212
|
+
- **Topics** shows the minutes each topic took and the first time it came up. **Conversation** shows the lines and PMOS's notes in time order. The line being played is marked and kept in view. Choose **All**, **Key points** or **PMOS**, or search for a word.
|
|
213
|
+
|
|
214
|
+
The page reads only records on this machine: the note, the file Focus kept for the meeting, and the screen shots.
|
|
215
|
+
|
|
204
216
|
### Clean up the replay sound
|
|
205
217
|
|
|
206
218
|
The replay can play a cleaned copy of the sound. The copy has less rumble and hiss, and an even loudness. It needs **ffmpeg** on your machine. To install it, run `brew install ffmpeg` in a terminal.
|
|
@@ -226,6 +238,15 @@ When you share the meeting's screen, the workspace also keeps screenshots of it,
|
|
|
226
238
|
- In the transcript, a line said while a new screenshot came up has a small picture beside it. Press it to see the screenshot.
|
|
227
239
|
- When **tesseract** is on this machine, the workspace reads the text on each screenshot, on this machine, and shows it under the large picture. To install it, run `brew install tesseract` in a terminal.
|
|
228
240
|
|
|
241
|
+
### Shots of the meeting only
|
|
242
|
+
|
|
243
|
+
When you press **Listen**, a sheet asks **Where is the meeting?** The choice is remembered for the next time.
|
|
244
|
+
|
|
245
|
+
- **Zoom app** or **Another app**: the browser asks twice. The first share is **Entire screen** with system audio; PMOS uses its sound only, never its picture. Then a second sheet asks you to **Pick the meeting window**; the shots come from that window only, and it stays captured while you work in other apps. Press **Sound only, no shots** to skip the second share.
|
|
246
|
+
- **Google Meet in Chrome**: one share of the Meet tab, with its sound, gives the sound and the shots.
|
|
247
|
+
- **Pause shots**: the chip next to **REC** in Focus, and the eye icon on the recording chip in the top bar, stop the pictures and keep the sound. Press it again to take shots again.
|
|
248
|
+
- **Set aside.** If the picture is the whole screen and tesseract is on this machine, PMOS reads each shot. A shot whose text looks like another app (Slack or a mailbox, for example) and not like the call is set aside: it is not in the note, the replay or anything the agent reads. The note shows **N set aside**. Press it to **Keep it**, **Delete** or **Delete all**. Set-aside shots are deleted on their own after 7 days. Without tesseract, every shot is kept.
|
|
249
|
+
|
|
229
250
|
To stop taking screenshots, clear **Capture the shared screen** in the settings. The sound of the shared screen is still used. To stop sharing only the picture during a meeting, stop the share in the browser's sharing bar; the screenshots stop. To delete one note's screenshots, press **Delete this note's screenshots** in the settings, or **Delete screenshots** in the note's **More** menu. The notes, the transcript and the audio stay. Deleting a note, or marking it private, deletes its screenshots.
|
|
230
251
|
|
|
231
252
|
## Capture highlights during the meeting
|
|
@@ -13,6 +13,10 @@ There is no template to fill in. You describe the report, add samples, and refin
|
|
|
13
13
|
|
|
14
14
|
## Open the design studio
|
|
15
15
|
|
|
16
|
+
Design is step 2 of the report studio. Step 1 is Structure, which decides what the report says (see [Shape what a report says](report-structure.md)). Press **1 Structure** at the top of the studio to go to it.
|
|
17
|
+
|
|
18
|
+
Design is step 2 of the report studio. Step 1 is Structure, which decides what the report says (see [Shape what a report says](report-structure.md)). Press **1 Structure** at the top of the studio to go to it.
|
|
19
|
+
|
|
16
20
|
Use one of these two buttons:
|
|
17
21
|
|
|
18
22
|
- In the **Set up** list of the Reports tab, press **Design how it looks** on the row of the report.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Guide
|
|
3
|
+
title: "Shape what a report says: the Structure step"
|
|
4
|
+
description: "How to decide with PMOS what a report answers, where each number comes from and how it is worked out, in the Structure step of the report studio, and how to try the plan on real data before you design it."
|
|
5
|
+
timestamp: 2026-10-14
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Shape what a report says
|
|
9
|
+
|
|
10
|
+
The report studio has two steps: **1 Structure** and **2 Design**. Structure decides what the report says. Design decides how it looks. Do Structure first.
|
|
11
|
+
|
|
12
|
+
In Structure you talk with PMOS. You say what the report is for. PMOS asks what it needs and writes a report plan. The plan shows what the report answers, where each number comes from, how each number is worked out, and the order of the sections.
|
|
13
|
+
|
|
14
|
+
## Open the Structure step
|
|
15
|
+
|
|
16
|
+
Use one of these:
|
|
17
|
+
|
|
18
|
+
- In **Ask PMOS** on the Reports tab, press **Shape it with PMOS** on the answer. PMOS sets the report up and opens Structure.
|
|
19
|
+
- In the **Set up** list, press **Shape the report** on the row of the report.
|
|
20
|
+
- In the Design studio, press **1 Structure** at the top.
|
|
21
|
+
|
|
22
|
+
To go to the Design studio from Structure, press **2 Design**.
|
|
23
|
+
|
|
24
|
+
## Talk it through
|
|
25
|
+
|
|
26
|
+
1. Write what the report is for, who reads it and what they decide with it. Press **Make the first version**.
|
|
27
|
+
2. PMOS can ask questions first. Each question has answer chips. Press a chip to put the answer in the box. You can press more than one, and edit the words. Press **Make the next version**.
|
|
28
|
+
3. PMOS makes **S1**. It can also propose up to three structures. Press **See the three structures side by side** to read them.
|
|
29
|
+
4. Write what to change and press **Make the next version**. A turn that changes the plan makes a new version: S2, S3 and so on. A turn that only asks a question makes no new version.
|
|
30
|
+
|
|
31
|
+
A turn takes a minute or two. You can close the studio. The version is ready when you open it again.
|
|
32
|
+
|
|
33
|
+
## Read the plan
|
|
34
|
+
|
|
35
|
+
The strip at the top right lists the versions. Press a version to read it. On an older version, press **Go back to** that version to build on it again. The later versions stay in the strip.
|
|
36
|
+
|
|
37
|
+
The plan has these parts:
|
|
38
|
+
|
|
39
|
+
- **What it answers.** The readers, the decision, the scope, how often, and the questions the report answers.
|
|
40
|
+
- **Sources.** Where the numbers come from. They are in two groups.
|
|
41
|
+
- **From PMOS.** A portfolio, board or initiative, a dashboard, a chart, a document, or a past report. A document gives PMOS context for its notes, never numbers. A past report gives last cycle's numbers. In the first cycle there is nothing to read.
|
|
42
|
+
- **Outside PMOS.** A connector you have (for example Google Analytics through Supermetrics), a link, a file you upload each cycle, a person who gives a number each cycle, a page read in Chrome, or an app on your Mac.
|
|
43
|
+
- Each source shows its state: **Reached**, **Not connected**, **First cycle: nothing to read** or **Needs you**. It also shows how it is read and how many charts use it. Press **Test pull** to ask PMOS to read it now. Press **Check it works** on a Chrome page or an app on your Mac. Press **How to connect** for a connector that is not connected. Press **Open** for a source in PMOS.
|
|
44
|
+
- **Charts.** Every number is a chart: one question, its source, how it is worked out, what it is compared with, and when it is flagged. A chart that PMOS proposes is tagged **New chart**. A chart that already exists in Reports is tagged **From Charts**. Press a chart to see the question, where it is used, exactly what PMOS reads, why PMOS proposed it and its test value. Press **Discuss this chart** to put its name in the box. Press **Open in Charts** to see a chart from Charts. Press **Remove it** to make the next version without the chart.
|
|
45
|
+
- **Outline.** The sections in order. Each section lists its charts and says if PMOS writes notes on them. **Headline**, **Needs your call** and **Sources** are in every report. You decide everything between them.
|
|
46
|
+
|
|
47
|
+
Press **Other structures** to read the structures PMOS proposed side by side. The one PMOS recommends is first. Press **Use this instead** to put the choice in the box, then press **Make the next version**.
|
|
48
|
+
|
|
49
|
+
## Add a source
|
|
50
|
+
|
|
51
|
+
Press **Add a source** under the conversation. Choose the kind, write its name and press **Add**. A source from outside PMOS that needs a link asks for one. PMOS reads every source and never writes to one. A connector that your Claude settings do not allow stays closed.
|
|
52
|
+
|
|
53
|
+
## Add a page in Chrome or an app on your Mac
|
|
54
|
+
|
|
55
|
+
Some numbers are in a page that has no connector, or in an app. PMOS can read a page with Claude in Chrome, or an app with computer use. Both run on your Mac, with your own sign-in.
|
|
56
|
+
|
|
57
|
+
When you add one, PMOS checks it step by step. Each step says **Works**, **Needs you** or **Waiting**, with a plain reason:
|
|
58
|
+
|
|
59
|
+
1. PMOS runs on this Mac.
|
|
60
|
+
2. A session can start with Claude in Chrome (or with computer use).
|
|
61
|
+
3. Claude in Chrome answers (or macOS allows the screen and the mouse and keyboard).
|
|
62
|
+
4. You are signed in.
|
|
63
|
+
5. PMOS can read the numbers.
|
|
64
|
+
|
|
65
|
+
If a step says **Needs you**, do what it says, then press **Check again**. PMOS never types a password. The check session can only open the page, read it and scroll. It cannot submit a form or change a setting, and it closes its tab. Computer use runs only when your own Claude Code settings allow it. If they do not, the check says so.
|
|
66
|
+
|
|
67
|
+
When a draft is due, PMOS checks that source again. If the check fails, Work shows what to do. The draft waits up to 4 hours. After that it is drafted without that source and says so under Sources. PMOS never guesses a number.
|
|
68
|
+
|
|
69
|
+
## Try it with real data
|
|
70
|
+
|
|
71
|
+
Press **Try it with real data** to run the plan on the last complete period, before any design. PMOS shows:
|
|
72
|
+
|
|
73
|
+
- the headline it would write,
|
|
74
|
+
- the value of each chart, with what it is compared with,
|
|
75
|
+
- the flags,
|
|
76
|
+
- what it could not get, and why,
|
|
77
|
+
- the options it would propose where the outline asks for a choice.
|
|
78
|
+
|
|
79
|
+
This only reads. It saves nothing in the report and sends nothing.
|
|
80
|
+
|
|
81
|
+
## Save the structure
|
|
82
|
+
|
|
83
|
+
Press **Use this structure** to save the version on screen. PMOS saves each new chart in **Reports**, **Charts**. A chart from Charts is not copied: if you change it there, it changes everywhere it is used.
|
|
84
|
+
|
|
85
|
+
The report setup sheet then shows the saved structure with **Refine the structure** and **Try it with real data**. Each next draft follows it: the sections in order, each number worked out by the saved logic, and the sources it reached and did not reach listed under **Sources**. A change to the structure applies from the next cycle. A cycle that was already issued keeps the structure it was drafted with.
|
|
86
|
+
|
|
87
|
+
Then press **Design how it looks** to do step 2.
|
|
88
|
+
|
|
89
|
+
Nothing is sent from Structure.
|
|
90
|
+
|
|
91
|
+
See also [Design how a report looks](report-design.md), [Set up a report by asking](reports-set-up.md) and [Reports: dashboards of reports](reports-dashboards.md).
|