create-pmos 0.134.0 → 0.135.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/bin/notes-replay.js +235 -0
- package/workspace/c3-briefing-test.js +130 -0
- package/workspace/c3-foundation-test.js +159 -0
- package/workspace/c3-ib-items-test.js +105 -0
- package/workspace/c3-ib-more-test.js +120 -0
- package/workspace/c3-ib-triage-backlog-test.js +119 -0
- package/workspace/c3-ini-harness.js +66 -0
- package/workspace/c3-initiatives-test.js +140 -0
- package/workspace/c3-one-home-test.js +14 -1
- package/workspace/c3-reports-test.js +57 -18
- package/workspace/c3-settings-test.js +139 -0
- package/workspace/c3-work-test.js +42 -10
- package/workspace/fixtures/one-home-baseline.json +1 -9
- package/workspace/guide/briefing-cards.md +15 -3
- package/workspace/guide/briefing.md +12 -8
- package/workspace/guide/calendar-popup.md +2 -2
- package/workspace/guide/console-calendar.md +39 -10
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +65 -0
- package/workspace/guide/jira.md +3 -1
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/my-work.md +5 -3
- package/workspace/guide/notes.md +16 -1
- package/workspace/guide/reports-dashboards.md +29 -5
- package/workspace/guide/settings.md +68 -0
- package/workspace/guide/welcome.md +10 -10
- package/workspace/lib/calendar-cli.js +3 -2
- package/workspace/lib/calendar-layers.js +21 -10
- package/workspace/lib/calendar-tidy.js +230 -0
- package/workspace/lib/engine/boards.js +542 -0
- package/workspace/lib/engine/breakdown-ops.js +180 -0
- package/workspace/lib/engine/calendar-import.js +84 -0
- package/workspace/lib/engine/calendar-model.js +185 -0
- package/workspace/lib/engine/card-state.js +92 -0
- package/workspace/lib/engine/derived/derive.js +92 -0
- package/workspace/lib/engine/derived/plan.js +3 -0
- package/workspace/lib/engine/derived/trust.js +3 -0
- package/workspace/lib/engine/doc-health.js +142 -0
- package/workspace/lib/engine/doc-sheet.js +75 -0
- package/workspace/lib/engine/intake.js +78 -0
- package/workspace/lib/engine/outbox.js +86 -0
- package/workspace/lib/engine/plan-forecast.js +342 -0
- package/workspace/lib/engine/promises.js +29 -0
- package/workspace/lib/engine/records/index.js +114 -0
- package/workspace/lib/engine/releases.js +96 -0
- package/workspace/lib/engine/replies.js +99 -0
- package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
- package/workspace/lib/engine/report-families.js +175 -0
- package/workspace/lib/engine/roadmap-ops.js +150 -0
- package/workspace/lib/engine/sessions-list.js +27 -0
- package/workspace/lib/engine/signal-rules.js +95 -0
- package/workspace/lib/engine/signals/cards.js +57 -0
- package/workspace/lib/engine/signals/detectors.js +370 -0
- package/workspace/lib/engine/signals/facts-cli.js +8 -0
- package/workspace/lib/engine/signals/facts.js +197 -0
- package/workspace/lib/engine/signals/index.js +33 -0
- package/workspace/lib/engine/signals/kinds.js +110 -0
- package/workspace/lib/engine/signals/layout-paths.js +44 -0
- package/workspace/lib/engine/signals/options.js +24 -0
- package/workspace/lib/engine/signals/score.js +72 -0
- package/workspace/lib/engine/signals/select.js +47 -0
- package/workspace/lib/engine/step-planner.js +65 -0
- package/workspace/lib/engine/trust/claim-rules.json +93 -0
- package/workspace/lib/engine/trust/import-calendar.js +21 -0
- package/workspace/lib/engine/trust/import-guard.js +36 -0
- package/workspace/lib/engine/trust/ledger.js +53 -0
- package/workspace/lib/engine/trust/outbox.js +45 -0
- package/workspace/lib/engine/trust/plan.js +5 -0
- package/workspace/lib/engine/trust/trust.js +21 -0
- package/workspace/lib/engine/truth-table.js +57 -0
- package/workspace/lib/engine/workspace-lib.js +24 -0
- package/workspace/lib/factory-join.js +1 -1
- package/workspace/lib/facts-async.js +4 -4
- package/workspace/lib/headless.js +95 -0
- package/workspace/lib/import-overlay.js +2 -1
- package/workspace/lib/jira-status.js +30 -3
- package/workspace/lib/notes-capture.js +9 -1
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +10 -3
- package/workspace/lib/outbox-store.js +10 -6
- package/workspace/lib/pmos-verbs.js +3 -1
- package/workspace/lib/proactive-cli.js +7 -0
- package/workspace/lib/proactive.js +15 -2
- package/workspace/lib/repo-script.js +36 -0
- package/workspace/lib/report-dashboards.js +5 -2
- package/workspace/lib/report-frame.js +122 -0
- package/workspace/lib/report-schedules.js +113 -0
- package/workspace/lib/route-modules.js +9 -2
- package/workspace/lib/routes-boards.js +734 -0
- package/workspace/lib/routes-brief-page.js +13 -4
- package/workspace/lib/routes-briefing.js +22 -11
- package/workspace/lib/routes-c3-init.js +4 -3
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +96 -13
- package/workspace/lib/routes-c3-strategy.js +4 -3
- package/workspace/lib/routes-c3-timeline.js +3 -2
- package/workspace/lib/routes-calendar-import.js +3 -2
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-doc-health.js +2 -1
- package/workspace/lib/routes-doc-sheet.js +2 -1
- package/workspace/lib/routes-forecast.js +2 -1
- package/workspace/lib/routes-hooks.js +3 -2
- package/workspace/lib/routes-initiatives.js +2 -1
- package/workspace/lib/routes-intake.js +2 -1
- package/workspace/lib/routes-notes.js +55 -9
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +3 -2
- package/workspace/lib/routes-releases.js +2 -1
- package/workspace/lib/routes-report-facts.js +4 -3
- package/workspace/lib/routes-report-families.js +2 -1
- package/workspace/lib/routes-roadmap-ops.js +5 -4
- package/workspace/lib/routes-runs.js +6 -5
- package/workspace/lib/routes-settings.js +51 -0
- package/workspace/lib/routes-signals.js +3 -2
- package/workspace/lib/routes-stage-tools.js +220 -0
- package/workspace/lib/run-answers.js +2 -1
- package/workspace/lib/settings-store.js +226 -0
- package/workspace/lib/stage-tools.js +208 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
- package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
- package/workspace/lib/work-evidence.js +73 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +63 -17
- package/workspace/public/pm/briefing-model.js +36 -7
- package/workspace/public/pm/c3-ambient.js +100 -0
- package/workspace/public/pm/c3-brief-needs.js +26 -6
- package/workspace/public/pm/c3-brief-pulse.js +12 -4
- package/workspace/public/pm/c3-brief-today.js +56 -21
- package/workspace/public/pm/c3-brief.js +2 -1
- package/workspace/public/pm/c3-calendar.js +161 -42
- package/workspace/public/pm/c3-calpop.js +5 -5
- package/workspace/public/pm/c3-demo-boards.js +649 -0
- package/workspace/public/pm/c3-demo-brief-page.js +1 -1
- package/workspace/public/pm/c3-demo-cal4.js +3 -2
- package/workspace/public/pm/c3-demo-cal5.js +52 -0
- package/workspace/public/pm/c3-demo-data.js +17 -27
- package/workspace/public/pm/c3-demo-rep3.js +46 -5
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-dock.js +1 -1
- package/workspace/public/pm/c3-ib-docs.js +80 -0
- package/workspace/public/pm/c3-ib-initiatives.js +134 -0
- package/workspace/public/pm/c3-ib-items.js +201 -0
- package/workspace/public/pm/c3-ib-releases.js +51 -0
- package/workspace/public/pm/c3-ib-sprints.js +55 -0
- package/workspace/public/pm/c3-ib-triage.js +178 -0
- package/workspace/public/pm/c3-ii-tabs.js +240 -0
- package/workspace/public/pm/c3-ini-records.js +197 -0
- package/workspace/public/pm/c3-ini-timeline.js +278 -0
- package/workspace/public/pm/c3-ini-ui.js +229 -0
- package/workspace/public/pm/c3-ini-work.js +139 -0
- package/workspace/public/pm/c3-initiatives.css +218 -0
- package/workspace/public/pm/c3-initiatives.js +741 -0
- package/workspace/public/pm/c3-plan.js +2 -2
- package/workspace/public/pm/c3-reports.css +47 -47
- package/workspace/public/pm/c3-reports.js +223 -185
- package/workspace/public/pm/c3-settings.css +74 -0
- package/workspace/public/pm/c3-settings.js +703 -0
- package/workspace/public/pm/c3-shell.js +102 -36
- package/workspace/public/pm/c3-terminal-icons.js +2 -2
- package/workspace/public/pm/c3-terminal.js +2 -2
- package/workspace/public/pm/calendar-views.js +1 -1
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +99 -23
- package/workspace/public/pm/dest-notes.js +26 -4
- package/workspace/public/pm/notes-screen-capture.js +165 -0
- package/workspace/public/pm/notes-screen-view.js +98 -0
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/pm-work-view.js +233 -146
- package/workspace/public/pm/shell-bell.js +3 -2
- package/workspace/public/pm/shell-recording.js +29 -8
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +46 -9
- package/workspace/public/pm/welcome.js +197 -89
- package/workspace/public/welcome.html +6 -4
- package/workspace/server.js +105 -6
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/jira-sync.skill +6 -4
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/test-files.json +12 -3
- package/workspace/test-lists/c3-ib-items.json +9 -0
- package/workspace/test-lists/c3-ib-more.json +9 -0
- package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
- package/workspace/test-lists/c3-initiative-boards.json +6 -0
- package/workspace/test-lists/calendar-tidy-up.json +6 -0
- package/workspace/test-lists/console3-briefing.json +9 -0
- package/workspace/test-lists/console3-foundation.json +9 -0
- package/workspace/test-lists/console3-reports.json +3 -3
- package/workspace/test-lists/meeting-notes.json +10 -4
- package/workspace/test-lists/workspace-headless-jobs.json +5 -0
- package/workspace/test-lists/workspace-package-script-closure.json +6 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
- package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
- package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
- package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
- package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
- package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
- package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
- package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
- package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* and a failed POST is toasted while the page keeps the change
|
|
16
16
|
* reload a stored layout is there after a reload
|
|
17
17
|
* share the frame (Headline, Sections, Needs your call, Sources) for Leadership and for Team; Save as draft POSTs ONE draft;
|
|
18
|
-
* the schedule
|
|
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
|
|
19
20
|
* library one row per report with data-home, never on the dashboard; a widget carries data-ref-row and opens its case in place
|
|
20
21
|
* drafts the Drafts tab shows the report families
|
|
21
22
|
* phone 400px: one column, no horizontal page scroll
|
|
@@ -136,6 +137,14 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
136
137
|
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')]));
|
|
137
138
|
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));
|
|
138
139
|
|
|
140
|
+
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(() => {
|
|
141
|
+
const bar = document.querySelector('[data-rp3] > .calbar'); if (!bar || !bar.querySelector("[data-rp3-scope]")) return false;
|
|
142
|
+
const t = bar.querySelector('.rp3-tabs').getBoundingClientRect(); const s = bar.querySelector('[data-rp3-scope]').getBoundingClientRect();
|
|
143
|
+
return Math.abs(t.top - s.top) < 40 && !document.querySelector('.rp3-intro') && bar.querySelectorAll('.pill').length === 3;
|
|
144
|
+
})));
|
|
145
|
+
ok('the Library tab carries its count as a badge', /7/.test(await page.textContent('[data-rp3-tab="lib"] .bdg')));
|
|
146
|
+
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));
|
|
147
|
+
|
|
139
148
|
/* ── portfolio ── */
|
|
140
149
|
ok('Portfolio draws its seven default reports, each a builder card', !!(await loaded(page, 7)), JSON.stringify(await order(page)));
|
|
141
150
|
ok('the portfolio order is the default order', JSON.stringify(await order(page)) === JSON.stringify(DEF_P), JSON.stringify(await order(page)));
|
|
@@ -149,7 +158,7 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
149
158
|
|
|
150
159
|
/* ── scopes, the chooser, and one read per scope ── */
|
|
151
160
|
await spy(page);
|
|
152
|
-
await page.click(
|
|
161
|
+
await page.click('[data-rp3-scope="board"]');
|
|
153
162
|
ok('Board shows a chooser of the boards', !!(await until(async () => (await page.$$eval(D + ' [data-rp3-pick="board"] option', (a) => a.map((o) => o.value))).join(',') === 'shop,enterprise')));
|
|
154
163
|
ok('Board draws its five default reports', !!(await loaded(page, 5)), JSON.stringify(await order(page)));
|
|
155
164
|
const bRows = await page.$$eval(D + ' [data-rp3-w="delivery"] [data-cs-row]', (a) => a.map((g) => g.getAttribute('data-cs-row')));
|
|
@@ -160,7 +169,7 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
160
169
|
await loaded(page, 5);
|
|
161
170
|
const eRows = await page.$$eval(D + ' [data-rp3-w="delivery"] [data-cs-row]', (a) => a.map((g) => g.getAttribute('data-cs-row')));
|
|
162
171
|
ok('Board Enterprise: delivery shows only SSO', JSON.stringify(eRows) === '["sso"]', JSON.stringify(eRows));
|
|
163
|
-
await page.click(
|
|
172
|
+
await page.click('[data-rp3-scope="initiative"]');
|
|
164
173
|
ok('Initiative shows a chooser of the five initiatives', !!(await until(async () => (await page.$$(D + ' [data-rp3-pick="initiative"] option')).length === 5)));
|
|
165
174
|
await page.selectOption(D + ' [data-rp3-pick="initiative"]', 'sso');
|
|
166
175
|
await until(async () => /Initiative: SSO/.test(await page.textContent(D + ' [data-rp3-title]')));
|
|
@@ -171,15 +180,15 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
171
180
|
await until(async () => /onboarding/.test(await page.textContent(D + ' [data-rp3-title]')));
|
|
172
181
|
await loaded(page, 4);
|
|
173
182
|
ok('a scope with nothing for a report draws that report\'s own empty card', !!(await page.$(D + ' [data-rp3-w="signals"] [data-cs-empty="true"]')));
|
|
174
|
-
await page.click(
|
|
175
|
-
await page.click(
|
|
183
|
+
await page.click('[data-rp3-scope="portfolio"]'); await loaded(page, 7);
|
|
184
|
+
await page.click('[data-rp3-scope="board"]'); await loaded(page, 5);
|
|
176
185
|
const reads = await page.evaluate(() => window.__rp3spy.reads);
|
|
177
186
|
ok('the data route is read once per scope and id, and not again within the minute', JSON.stringify(reads) === JSON.stringify(['board|shop', 'board|enterprise', 'initiative|checkout', 'initiative|sso', 'initiative|onb']), JSON.stringify(reads));
|
|
178
187
|
await page.click(D + ' [data-rp3-edit]');
|
|
179
188
|
const freeB = await page.$$eval(D + ' [data-rp3-free]', (a) => a.map((n) => n.getAttribute('data-rp3-free')));
|
|
180
189
|
ok('the Board add list holds only reports that fit a board (no north star, no maturity)', !freeB.includes('north') && !freeB.includes('maturity'), JSON.stringify(freeB));
|
|
181
190
|
await page.click(D + ' [data-rp3-edit]');
|
|
182
|
-
await page.click(
|
|
191
|
+
await page.click('[data-rp3-scope="portfolio"]'); await loaded(page, 7);
|
|
183
192
|
|
|
184
193
|
/* ── edit ── */
|
|
185
194
|
await spy(page);
|
|
@@ -229,31 +238,34 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
229
238
|
ok('Reset POSTs reports: null', P.length === 1 && P[0].body.reports === null && P[0].body.key === 'portfolio', JSON.stringify(P));
|
|
230
239
|
await page.click(D + ' [data-rp3-edit]');
|
|
231
240
|
// a board layout is kept per key
|
|
232
|
-
await page.click(
|
|
241
|
+
await page.click('[data-rp3-scope="board"]'); await loaded(page, 5);
|
|
233
242
|
await page.click(D + ' [data-rp3-edit]'); await page.click(D + ' [data-rp3-rm="synergy"]'); await until(async () => (await order(page)).length === 4);
|
|
234
243
|
await page.click(D + ' [data-rp3-edit]');
|
|
235
244
|
P = await posts(page, '/api/report-dashboards/layout');
|
|
236
245
|
ok('a board layout is POSTed under board:<id>', P.length === 2 && P[1].body.key === 'board:shop', JSON.stringify(P.map((q) => q.body.key)));
|
|
237
|
-
await page.click(
|
|
246
|
+
await page.click('[data-rp3-scope="portfolio"]'); await loaded(page, 7);
|
|
238
247
|
ok('the portfolio keeps its own layout when a board changes', JSON.stringify(await order(page)) === JSON.stringify(DEF_P));
|
|
239
248
|
|
|
240
249
|
/* ── share ── */
|
|
250
|
+
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]')));
|
|
241
251
|
await spy(page);
|
|
242
252
|
await page.click(D + ' [data-rp3-share]');
|
|
243
253
|
const sheet = await until(() => page.$('[data-rp3-sheet] [data-rp3-frame="Headline"]'));
|
|
244
254
|
ok('Share opens a sheet with the report frame', !!sheet);
|
|
245
|
-
const frames = await page.$$eval('[data-rp3-sheet] [data-rp3-frame]
|
|
255
|
+
const frames = await page.$$eval('[data-rp3-sheet] [data-rp3-frame] h2', (a) => a.map((h) => h.textContent.replace(/\d+$/, '')));
|
|
246
256
|
ok('the frame has Headline, Sections, Needs your call, Sources', JSON.stringify(frames) === JSON.stringify(['Headline', 'Sections', 'Needs your call', 'Sources']), JSON.stringify(frames));
|
|
247
257
|
ok('Leadership: one sentence per report, no numbers', await page.$eval('[data-rp3-sheet] [data-rp3-aud]', (n) => n.getAttribute('data-rp3-aud')) === 'Leadership'
|
|
248
|
-
&& (await page.$$('[data-rp3-sheet] [data-rp3-frame="Sections"]
|
|
258
|
+
&& (await page.$$('[data-rp3-sheet] [data-rp3-frame="Sections"] ul.chgl > li')).length === 7 && !(await page.$('[data-rp3-sheet] [data-rp3-nums]')));
|
|
249
259
|
const need = await page.textContent('[data-rp3-sheet] [data-rp3-frame="Needs your call"]');
|
|
250
260
|
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));
|
|
251
261
|
await page.click('[data-rp3-sheet] [data-rp3-audbtn="Team"]');
|
|
252
262
|
ok('Team adds the numbers behind each sentence', !!(await until(async () => (await page.$$('[data-rp3-sheet] [data-rp3-nums]')).length >= 5)));
|
|
253
263
|
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"]')));
|
|
254
|
-
const sched = await page
|
|
255
|
-
ok('the schedule control
|
|
256
|
-
ok('the
|
|
264
|
+
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()]));
|
|
265
|
+
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));
|
|
266
|
+
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()));
|
|
267
|
+
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')));
|
|
268
|
+
ok('Save schedule is a primary button', !!(await page.$('[data-rp3-sheet] [data-rp3-sched-save]:not([disabled])')));
|
|
257
269
|
await page.click('[data-rp3-sheet] [data-rp3-draft]');
|
|
258
270
|
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]')))));
|
|
259
271
|
ok('Save as draft is off once the draft is saved', await page.$eval('[data-rp3-sheet] [data-rp3-draft]', (b) => b.disabled));
|
|
@@ -262,11 +274,19 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
262
274
|
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)
|
|
263
275
|
&& B.body.split('\n').filter((l) => /^- /.test(l)).every((l) => /\[reported\]$/.test(l)), JSON.stringify(B).slice(0, 300));
|
|
264
276
|
ok('nothing else was POSTed from the sheet (no send, no schedule)', (await page.evaluate(() => window.__rp3spy.posts.length)) === 1);
|
|
277
|
+
await page.click('[data-rp3-sheet] [data-sched="weekly"]');
|
|
278
|
+
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()))));
|
|
279
|
+
ok('choosing is not saving: nothing is POSTed yet', (await page.evaluate(() => window.__rp3spy.posts.length)) === 1);
|
|
280
|
+
await page.click('[data-rp3-sheet] [data-rp3-sched-save]');
|
|
281
|
+
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]')))));
|
|
282
|
+
P = await posts(page, '/api/report-dashboards/schedule');
|
|
283
|
+
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));
|
|
284
|
+
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]')))));
|
|
265
285
|
await page.keyboard.press('Escape');
|
|
266
286
|
ok('Escape closes the sheet', !!(await until(async () => !(await page.$('[data-rp3-sheet]')))));
|
|
267
287
|
|
|
268
288
|
/* ── library and one home ── */
|
|
269
|
-
ok('the dashboard has no data-home row; every widget carries data-ref-row', !(await page.$(D + ' [data-home]')) && (await page.$$(D + '
|
|
289
|
+
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);
|
|
270
290
|
await page.click(D + ' [data-rp3-about="outcomes"]');
|
|
271
291
|
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()); })));
|
|
272
292
|
await page.click(D + ' [data-rp3-golib="outcomes"]');
|
|
@@ -276,15 +296,34 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
276
296
|
ok('the library has one row per report, each its home', libRows.length === 7 && new Set(libRows).size === 7, JSON.stringify(libRows));
|
|
277
297
|
ok('library rows are not references', !(await page.$(L + ' [data-ref-row]')));
|
|
278
298
|
const lrow = await page.textContent(L + ' [data-home="maturity"]');
|
|
279
|
-
ok('a library row shows the question, the source and where it is used', /Where does each pillar stand/.test(lrow) && /Pillar maturity records/.test(lrow) && /
|
|
299
|
+
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));
|
|
280
300
|
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"]'))));
|
|
281
301
|
await page.click(L + ' [data-home="bets"]');
|
|
282
302
|
ok('a library row opens the report\'s card in place', !!(await until(() => page.$(L + ' [data-home="bets"] .xp.open [data-cs="bets"]'))));
|
|
283
|
-
ok('a board layout shows in where it is used', /Board
|
|
303
|
+
ok('a board layout shows in where it is used', /On: .*Board/.test(await page.textContent(L + ' [data-home="synergy"]')));
|
|
284
304
|
|
|
285
305
|
/* ── drafts ── */
|
|
306
|
+
const DR = '[data-rp3-pane="drafts"]';
|
|
307
|
+
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')));
|
|
286
308
|
await page.click('[data-rp3-tab="drafts"]');
|
|
287
|
-
ok('the Drafts tab
|
|
309
|
+
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')))));
|
|
310
|
+
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);
|
|
311
|
+
const dtext = await page.textContent(DR + ' [data-draft-row]');
|
|
312
|
+
ok('a draft row shows its audience and that it was not sent', /For Team/.test(dtext) && /Not sent/.test(dtext), dtext);
|
|
313
|
+
const sch = await page.textContent(DR + ' [data-rp3-scheduled]');
|
|
314
|
+
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);
|
|
315
|
+
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
|
|
316
|
+
&& /Made by schedule/.test(await page.textContent(DR + ' [data-draft-row="out-demo-rep3-s1"]')));
|
|
317
|
+
await page.click(DR + ' [data-draft="preview"]');
|
|
318
|
+
ok('Preview opens the draft text in a sheet', !!(await until(async () => /Headline:/.test((await page.textContent('[data-rp3-draft-text]').catch(() => '')) || ''))));
|
|
319
|
+
await page.keyboard.press('Escape');
|
|
320
|
+
await spy(page);
|
|
321
|
+
await page.click(DR + ' [data-draft="discard"]');
|
|
322
|
+
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);
|
|
323
|
+
await page.click('[data-c3-toast-action]');
|
|
324
|
+
ok('Undo brings the draft back', !!(await until(async () => (await page.$$(DR + ' [data-draft-row]')).length === 3)));
|
|
325
|
+
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"]')));
|
|
326
|
+
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')));
|
|
288
327
|
ok('the dashboards are hidden while Drafts is open', await page.$eval(D, (n) => n.hidden));
|
|
289
328
|
await page.click('[data-rp3-tab="dash"]');
|
|
290
329
|
|
|
@@ -296,7 +335,7 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
|
|
|
296
335
|
ok('400px: no horizontal page scroll on the dashboard', await noScroll(pp), String(await pp.evaluate(() => document.documentElement.scrollWidth)));
|
|
297
336
|
const lefts = await pp.$$eval(D + ' [data-rp3-w]', (a) => [...new Set(a.map((w) => Math.round(w.getBoundingClientRect().left)))]);
|
|
298
337
|
ok('400px: the widgets stack in one column', lefts.length === 1, JSON.stringify(lefts));
|
|
299
|
-
await pp.click(
|
|
338
|
+
await pp.click('[data-rp3-scope="initiative"]'); await loaded(pp, 4);
|
|
300
339
|
await pp.click(D + ' [data-rp3-edit]');
|
|
301
340
|
ok('400px: Initiative scope in edit mode has no horizontal scroll', await noScroll(pp));
|
|
302
341
|
await pp.click(D + ' [data-rp3-edit]');
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/* c3-settings-test.js — the console3 Settings page (initiative console3-settings).
|
|
4
|
+
*
|
|
5
|
+
* Loads /c3.html against a real server with its own state directory and checks: the gear, the More menu item and
|
|
6
|
+
* ?tab=settings open Settings; every section draws; search finds a setting across sections; a change to a store setting
|
|
7
|
+
* is saved through POST /api/settings and read back; the theme switch sets the page theme; the start page and the
|
|
8
|
+
* time-of-day switches take effect on the next load; and the page throws no error.
|
|
9
|
+
*/
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { spawn } = require('child_process');
|
|
12
|
+
const { findBrowser } = require('./lib/find-browser');
|
|
13
|
+
const T = require('./test-run').configure({ name: 'c3-settings' });
|
|
14
|
+
|
|
15
|
+
const BASE = T.BASE;
|
|
16
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
17
|
+
const results = [];
|
|
18
|
+
const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + extra : ''}`); };
|
|
19
|
+
let srv = null; let browser = null;
|
|
20
|
+
const finish = async (code) => {
|
|
21
|
+
if (browser) await browser.close().catch(() => {});
|
|
22
|
+
if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
|
|
23
|
+
T.killTmux();
|
|
24
|
+
const bad = results.filter((x) => !x.cond).length;
|
|
25
|
+
console.log(`\n${results.length - bad}/${results.length} c3-settings assertions passed`);
|
|
26
|
+
process.exit(code !== undefined ? code : (bad ? 1 : 0));
|
|
27
|
+
};
|
|
28
|
+
process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
29
|
+
|
|
30
|
+
const SECTIONS = ['about', 'workspaces', 'boards', 'connectors', 'recording', 'time', 'sharing', 'access', 'sessions', 'privacy', 'appearance', 'updates'];
|
|
31
|
+
|
|
32
|
+
async function waitFor(page, fn, arg, ms = 8000) {
|
|
33
|
+
try { await page.waitForFunction(fn, arg, { timeout: ms }); return true; } catch { return false; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
(async () => {
|
|
37
|
+
const exe = findBrowser();
|
|
38
|
+
if (!exe) { console.log('✗ no Chrome/Chromium on this machine — the c3-settings suite cannot run'); return finish(1); }
|
|
39
|
+
let chromium;
|
|
40
|
+
try { ({ chromium } = require('playwright-core')); } catch { console.log('✗ playwright-core is not installed — the c3-settings suite cannot run'); return finish(1); }
|
|
41
|
+
|
|
42
|
+
srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
|
|
43
|
+
await T.waitForServer(BASE);
|
|
44
|
+
browser = await chromium.launch({ executablePath: exe });
|
|
45
|
+
const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
|
46
|
+
const page = await ctx.newPage();
|
|
47
|
+
const errors = [];
|
|
48
|
+
page.on('pageerror', (e) => errors.push(String(e && e.message || e)));
|
|
49
|
+
page.on('dialog', (d) => d.dismiss().catch(() => {}));
|
|
50
|
+
|
|
51
|
+
/* ── A: the ways in ── */
|
|
52
|
+
await page.goto(BASE + '/c3.html');
|
|
53
|
+
ok('A1 the header has a Settings gear', await waitFor(page, () => !!document.querySelector('[data-c3pg="settings"]')));
|
|
54
|
+
await page.click('[data-c3pg="settings"]');
|
|
55
|
+
ok('A2 the gear opens Settings', await waitFor(page, () => { const p = document.querySelector('[data-c3-pane="settings"]'); return p && !p.hidden && !!p.querySelector('[data-c3set]'); }));
|
|
56
|
+
ok('A3 the title says Settings', (await page.textContent('[data-c3pg="title"]')).trim() === 'Settings');
|
|
57
|
+
ok('A4 the address names the page', new URL(page.url()).searchParams.get('tab') === 'settings');
|
|
58
|
+
ok('A5 no tab is selected while Settings shows', await page.evaluate(() => ![...document.querySelectorAll('[role="tab"]')].some((b) => b.getAttribute('aria-selected') === 'true')));
|
|
59
|
+
await page.click('[data-c3-tab="work"]');
|
|
60
|
+
ok('A6 a tab press leaves Settings', await waitFor(page, () => document.querySelector('[data-c3-pane="settings"]').hidden && !document.querySelector('[data-c3-pane="work"]').hidden));
|
|
61
|
+
await page.click('[data-c3pg="more"]');
|
|
62
|
+
ok('A7 the More menu has Settings', await waitFor(page, () => !!document.querySelector('.c3pg-menu [data-a="settings"]')));
|
|
63
|
+
await page.click('.c3pg-menu [data-a="settings"]');
|
|
64
|
+
ok('A8 the More menu item opens Settings', await waitFor(page, () => !document.querySelector('[data-c3-pane="settings"]').hidden));
|
|
65
|
+
|
|
66
|
+
/* ── B: every section draws ── */
|
|
67
|
+
await waitFor(page, () => !!document.querySelector('[data-c3set-section] .st-row'));
|
|
68
|
+
ok('B0 Settings opens on About you, drawn as console3 section cards', await waitFor(page, () => !!document.querySelector('[data-c3set-section="about"] .card.sect .list .nrow.st-row #c3set-you-name')));
|
|
69
|
+
ok('B1 the nav lists twelve sections', (await page.$$eval('[data-c3set-nav] [data-c3set-sec]', (b) => b.length)) === SECTIONS.length);
|
|
70
|
+
for (const id of SECTIONS) {
|
|
71
|
+
await page.click(`[data-c3set-nav] [data-c3set-sec="${id}"]`);
|
|
72
|
+
const drew = await waitFor(page, (s) => { const x = document.querySelector('[data-c3set-section="' + s + '"]'); return x && x.querySelectorAll('.st-row, .sect, .fold').length > 0; }, id, 4000);
|
|
73
|
+
ok(`B2 section ${id} draws`, drew);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ── C: search ── */
|
|
77
|
+
await page.fill('[data-c3set-search]', 'language');
|
|
78
|
+
ok('C1 search finds the recording language', await waitFor(page, () => !!document.querySelector('[data-c3set-body] [data-c3set-row="notes.language"]')));
|
|
79
|
+
await page.fill('[data-c3set-search]', 'zzqx');
|
|
80
|
+
ok('C2 search with no match says so', await waitFor(page, () => !!document.querySelector('[data-c3set-none]')));
|
|
81
|
+
await page.fill('[data-c3set-search]', '');
|
|
82
|
+
|
|
83
|
+
/* ── D: a store setting is saved and read back ── */
|
|
84
|
+
await page.click('[data-c3set-nav] [data-c3set-sec="time"]');
|
|
85
|
+
await waitFor(page, () => !!document.querySelector('#c3set-time\\.workStart'));
|
|
86
|
+
await page.selectOption('#c3set-time\\.workStart', '8');
|
|
87
|
+
const saved = await waitFor(page, async () => { const r = await fetch('/api/settings'); const j = await r.json(); const s = j.settings.find((x) => x.key === 'time.workStart'); return s && s.value === 8 && s.changed; });
|
|
88
|
+
ok('D1 a change is saved through the settings API', saved);
|
|
89
|
+
ok('D1b the save says so in a toast with Undo', await waitFor(page, () => { const t = document.querySelector('[data-c3-toast]'); return !!t && /Saved: Working hours start/.test(t.textContent) && !!t.querySelector('[data-c3-toast-action]'); }));
|
|
90
|
+
ok('D2 the section shows the change', await waitFor(page, () => !!document.querySelector('[data-c3set-row="time.workStart"] .st-dot')));
|
|
91
|
+
ok('D3 the nav counts the change', await waitFor(page, () => { const b = document.querySelector('[data-c3set-sec="time"] .bdg'); return b && b.textContent === '1'; }));
|
|
92
|
+
ok('D3b the section offers Reset section', await waitFor(page, () => !!document.querySelector('[data-c3set-reset-sec="time"]')));
|
|
93
|
+
const rs = await page.$('[data-c3set-row="time.workStart"] [data-c3set-reset]');
|
|
94
|
+
if (rs) await rs.click();
|
|
95
|
+
ok('D4 Reset puts the default back', await waitFor(page, async () => { const j = await (await fetch('/api/settings')).json(); const s = j.settings.find((x) => x.key === 'time.workStart'); return s && s.value === 9 && !s.changed; }));
|
|
96
|
+
const refused = await page.evaluate(async () => { const { pmPost } = await import('/pm/http.js'); return pmPost('/api/settings', { key: 'time.workStart', value: 99 }); });
|
|
97
|
+
ok('D5 a value out of range is refused', refused && refused.ok === false && !/page only/.test(refused.error || ''), JSON.stringify(refused));
|
|
98
|
+
|
|
99
|
+
/* ── D6: About you saves to the proactive settings the Welcome page uses ── */
|
|
100
|
+
await page.click('[data-c3set-nav] [data-c3set-sec="about"]');
|
|
101
|
+
await waitFor(page, () => !!document.querySelector('#c3set-you-role'));
|
|
102
|
+
await page.fill('#c3set-you-role', 'Senior PM, Payments');
|
|
103
|
+
await page.press('#c3set-you-role', 'Tab');
|
|
104
|
+
ok('D6 About you saves the role', await waitFor(page, async () => { const j = await (await fetch('/api/proactive/settings')).json(); return j.settings && j.settings.about && j.settings.about.role === 'Senior PM, Payments'; }));
|
|
105
|
+
|
|
106
|
+
/* ── D7: a feature PMOS does not have yet shows as a row with its control off (decision 9) ── */
|
|
107
|
+
await page.click('[data-c3set-nav] [data-c3set-sec="access"]');
|
|
108
|
+
ok('D7 phone pairing shows, turned off, with the reason', await waitFor(page, () => { const r = document.querySelector('[data-c3set-row="access.pair"]'); return !!r && !!r.querySelector('button:disabled') && !!r.querySelector('.fold'); }));
|
|
109
|
+
|
|
110
|
+
/* ── E: theme ── */
|
|
111
|
+
await page.click('[data-c3set-nav] [data-c3set-sec="appearance"]');
|
|
112
|
+
await waitFor(page, () => !!document.querySelector('[data-c3set-row="look.theme"] [data-v="dark"]'));
|
|
113
|
+
await page.click('[data-c3set-row="look.theme"] [data-v="dark"]');
|
|
114
|
+
ok('E1 the theme switch sets the page theme', await waitFor(page, () => document.documentElement.getAttribute('data-pm-page-theme') === 'dark'));
|
|
115
|
+
await page.click('[data-c3set-row="look.theme"] [data-v="light"]');
|
|
116
|
+
|
|
117
|
+
/* ── F: start page and time of day take effect on the next load ── */
|
|
118
|
+
const post = (key, value) => page.evaluate(async ([k, v]) => { const { pmPost } = await import('/pm/http.js'); return pmPost('/api/settings', { key: k, value: v }); }, [key, value]);
|
|
119
|
+
const f0 = [await post('look.start', 'cal'), await post('time.tint', false), await post('time.strip', false)];
|
|
120
|
+
ok('F0 the start page and switches save', f0.every((r) => r && r.ok), JSON.stringify(f0));
|
|
121
|
+
await page.goto(BASE + '/c3.html');
|
|
122
|
+
ok('F1 the saved start page opens', await waitFor(page, () => { const p = document.querySelector('[data-c3-pane="cal"]'); return p && !p.hidden; }));
|
|
123
|
+
ok('F2 the tint switch reaches the page', await waitFor(page, () => document.getElementById('c3-root').getAttribute('data-c3-tint') === 'off'));
|
|
124
|
+
ok('F3 the daylight strip is hidden', await waitFor(page, () => { const s = document.querySelector('.c3pg-daylight'); return !s || getComputedStyle(s).display === 'none'; })); // the stylesheet loads after the attribute is set
|
|
125
|
+
await page.goto(BASE + '/c3.html?tab=settings');
|
|
126
|
+
ok('F4 ?tab=settings opens Settings directly', await waitFor(page, () => { const p = document.querySelector('[data-c3-pane="settings"]'); return p && !p.hidden && !!p.querySelector('[data-c3set]'); }));
|
|
127
|
+
await post('look.start', 'brief'); await post('time.tint', true); await post('time.strip', true);
|
|
128
|
+
|
|
129
|
+
/* ── G: the palette ── */
|
|
130
|
+
await page.goto(BASE + '/c3.html');
|
|
131
|
+
await waitFor(page, () => !!document.querySelector('[data-c3pg="search"]'));
|
|
132
|
+
await page.click('[data-c3pg="search"]');
|
|
133
|
+
await sleep(200);
|
|
134
|
+
await page.keyboard.type('Settings');
|
|
135
|
+
ok('G1 the palette lists Settings', await waitFor(page, () => [...document.querySelectorAll('[role="option"], .pal li, .pal button, [data-pal-item]')].some((x) => /Settings/.test(x.textContent)), null, 3000));
|
|
136
|
+
|
|
137
|
+
ok('H1 the page throws no error', errors.length === 0, errors.slice(0, 3).join(' | '));
|
|
138
|
+
finish();
|
|
139
|
+
})();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* so nothing reaches the server and the fixture (public/pm/c3-demo-data.js pmItems) is placed by the same rule the server uses.
|
|
6
6
|
*
|
|
7
7
|
* views six views in order (Inbox, Today, Upcoming, Waiting, Someday, Review), each with its count; no Triage, Week, Later
|
|
8
|
-
* lens
|
|
8
|
+
* lens; the right column is Focus on every view, with no Flow today (console3-deck-flow-fixes); no Jobs card, Group by job, ring or plan deck (console3-work-parity)
|
|
9
9
|
* one home none of the decisions and approvals whose home is Briefing · Needs you is a Work row or Work text
|
|
10
10
|
* Focus Today opens with up to three suggestions, each with the reason; Accept pins, Not today hides, Edit title saves, Unpin
|
|
11
11
|
* offline an item done outside PMOS shows "outside PMOS" and the person; Looks done? has Confirm (with Undo); Not yet moves it
|
|
@@ -46,7 +46,9 @@ const LABELS = ['Inbox', 'Today', 'Upcoming', 'Waiting', 'Someday', 'Review'];
|
|
|
46
46
|
const DECISIONS = ['Approve pricing-page copy before handoff', 'Confirm the growth assumption in the Q4 forecast', 'Say yes to Sam: ship the logging change',
|
|
47
47
|
'Self-serve onboarding: continue or park?', 'Pick interview slots with 3 customers', 'Assign owners to 2 unowned workshop actions',
|
|
48
48
|
'Confirm the 1 Oct numbers before the 10:30 update', 'Accept the 15 Oct interview invite', 'Review the 13:00 interview guide',
|
|
49
|
-
'Cut Checkout v2 to 3 core flows to hold 24 Oct?'
|
|
49
|
+
'Cut Checkout v2 to 3 core flows to hold 24 Oct?'];
|
|
50
|
+
/* The agent's drafts moved from Briefing to the Review view of Work (console3-work-parity R10): they are not Work items, but they show in Review. */
|
|
51
|
+
const DRAFTS = ['Reply to Marta: SSO 14 Oct, SCIM in Nov', 'Weekly status for leadership', 'Prep note for 10:30 Checkout sync'];
|
|
50
52
|
const W = '[data-pm-work]';
|
|
51
53
|
|
|
52
54
|
(async () => {
|
|
@@ -90,7 +92,10 @@ const W = '[data-pm-work]';
|
|
|
90
92
|
ok('each view shows its count (3, 5, 6, 2, 2, 3)', JSON.stringify(VIEWS.map((v) => c0[v])) === '[3,5,6,2,2,3]', JSON.stringify(c0));
|
|
91
93
|
ok('the page opens on Today', await st((w) => (document.querySelector(w + ' [data-pm-lens-head]') || {}).getAttribute && document.querySelector(w + ' [data-pm-lens-head]').getAttribute('data-pm-lens-head') === 'today', W));
|
|
92
94
|
const text0 = await st((w) => document.querySelector(w).textContent, W);
|
|
93
|
-
ok('no Triage, Week or Later view
|
|
95
|
+
ok('no Triage, Week or Later view', !(await st((w) => !!document.querySelector(w + ' [data-lens="triage"], ' + w + ' [data-lens="week"], ' + w + ' [data-lens="later"]'), W)));
|
|
96
|
+
ok('no Flow today card (the PM removed it)', !(await st((w) => !!document.querySelector(w + ' [data-pm-flow]'), W)) && !/Flow today|Your part is the commit/.test(text0));
|
|
97
|
+
ok('no Jobs card, Group by job, Today ring or plan deck', !(await st((w) => !!document.querySelector(w + ' [data-pm-jobs], ' + w + ' [data-pm-group], ' + w + ' [data-c3pg="plan"], ' + w + ' .lh svg[role="img"]'), W)) && !/Jobs to be done|Group by job/.test(text0));
|
|
98
|
+
ok('Focus leads the right column', await st((w) => { const r = document.querySelector(w + ' .bcol.r'); return !!r && !!r.firstElementChild && r.firstElementChild.matches('[data-pm-focus]'); }, W));
|
|
94
99
|
|
|
95
100
|
/* one home (R9): no decision is a Work row in any view, nor Work text */
|
|
96
101
|
const seen = new Set(); let allText = '';
|
|
@@ -102,16 +107,29 @@ const W = '[data-pm-work]';
|
|
|
102
107
|
/* Today and Focus (R4) */
|
|
103
108
|
await view('today');
|
|
104
109
|
const today0 = await titles();
|
|
105
|
-
ok('Today lists
|
|
110
|
+
ok('Today lists Overdue, then Due today, then Flagged', today0[0] === 'Call Priya about the Q4 budget variance' && today0[1] === 'Send the revised Checkout v2 timeline to Dara' && today0[4] === 'Prep the one-pager for the Search relevance review', JSON.stringify(today0));
|
|
111
|
+
const sects = await st((w) => [...document.querySelectorAll(w + ' [data-pm-sect]')].map((n) => [n.getAttribute('data-pm-sect'), n.querySelectorAll('[data-pm-item]').length, !!n.querySelector('[data-pm-legend]')]), W);
|
|
112
|
+
ok('…each in its own card with a count, the legend on the first only', JSON.stringify(sects) === '[["overdue",2,true],["due",2,false],["flagged",1,false]]', JSON.stringify(sects));
|
|
113
|
+
const chipsTxt = await st((w) => ({ over: (document.querySelector(w + ' [data-pm-due="overdue"]') || {}).textContent, today: (document.querySelector(w + ' [data-pm-due="today"]') || {}).textContent, iso: /\d{4}-\d{2}-\d{2}/.test(document.querySelector(w + ' [data-pm-list="today"]').textContent), done: document.querySelectorAll(w + ' [data-pm-list="today"] [data-pm-done]').length }), W);
|
|
114
|
+
ok('rows read "N days overdue", "Due today", no ISO dates, and each has a Done button', /overdue/.test(chipsTxt.over) && chipsTxt.today === 'Due today' && !chipsTxt.iso && chipsTxt.done === 5, JSON.stringify(chipsTxt));
|
|
106
115
|
const foc = await st((w) => [...document.querySelectorAll(w + ' [data-pm-focus] [data-pm-focus-card]')].map((c) => ({ key: c.getAttribute('data-pm-focus-card'), state: c.getAttribute('data-pm-focus-state'), why: (c.querySelector('.wkfc-why') || {}).textContent || '' })), W);
|
|
107
116
|
ok('Today opens with Focus: three suggested jobs, each with the reason', foc.length === 3 && foc.every((f) => f.state === 'suggested' && /^Why: \S/.test(f.why)), JSON.stringify(foc));
|
|
108
|
-
|
|
109
|
-
ok('…at the top of Today, above the list', focBefore);
|
|
117
|
+
ok('Focus is a list of rows in the right column', await st((w) => !!document.querySelector(w + ' .bcol.r [data-pm-focus] li.nrow[data-pm-focus-card]') && !!document.querySelector(w + ' [data-pm-focus-job]'), W));
|
|
110
118
|
const [k1, k2, k3] = foc.map((f) => f.key);
|
|
111
119
|
await click(W + ' [data-pm-focus-accept="' + k1 + '"]');
|
|
112
120
|
ok('Accept pins the job to Today', !!await until(() => st(([w, k]) => { const c = document.querySelector(w + ' [data-pm-focus-card="' + k + '"]'); return c && c.getAttribute('data-pm-focus-state') === 'accepted'; }, [W, k1]), 5000));
|
|
113
121
|
await click(W + ' [data-pm-focus-dismiss="' + k2 + '"]');
|
|
114
122
|
ok('Not today takes a suggestion away', !!await until(() => st(([w, k]) => !document.querySelector(w + ' [data-pm-focus-card="' + k + '"]'), [W, k2]), 5000));
|
|
123
|
+
ok('…and the footer says how many are marked not today, with Bring back', await st((w) => /^1 marked not today/.test((document.querySelector(w + ' [data-pm-focus-hidden]') || {}).textContent || ''), W));
|
|
124
|
+
await click(W + ' [data-pm-focus-restore]');
|
|
125
|
+
ok('Bring back returns it as a suggestion', !!await until(() => st(([w, k]) => { const c = document.querySelector(w + ' [data-pm-focus-card="' + k + '"]'); return c && c.getAttribute('data-pm-focus-state') === 'suggested'; }, [W, k2]), 5000));
|
|
126
|
+
await click(W + ' [data-pm-focus-job]');
|
|
127
|
+
ok('a Focus row with a job opens the job\'s sheet (its words, Keep, Delete)', !!await until(() => st(() => { const sh = document.querySelector('[data-shell="sheet"] [data-pm-case]'); return !!sh && !!sh.querySelector('[data-pm-job-edit$=":want"]'); }), 4000));
|
|
128
|
+
await page.keyboard.press('Escape');
|
|
129
|
+
await click(W + ' [data-pm-job-add-toggle]');
|
|
130
|
+
ok('New job opens a sheet with the one-line draft', !!await until(() => st(() => !!document.querySelector('[data-shell="sheet"] [data-pm-add="/api/pm-jobs/add"]')), 4000));
|
|
131
|
+
await page.keyboard.press('Escape');
|
|
132
|
+
ok('both sheets close with Esc', !!await until(() => st(() => !document.querySelector('[data-shell="sheet"]')), 3000));
|
|
115
133
|
await click(W + ' [data-pm-focus-edit="' + k3 + '"]');
|
|
116
134
|
const inp = await until(() => st(([w, k]) => !!document.querySelector(w + ' [data-pm-focus-input="' + k + '"]'), [W, k3]), 4000);
|
|
117
135
|
if (inp) await page.fill(W + ' [data-pm-focus-input="' + k3 + '"]', 'Prep the search one-pager before lunch');
|
|
@@ -134,20 +152,20 @@ const W = '[data-pm-work]';
|
|
|
134
152
|
await toastUndo();
|
|
135
153
|
ok('…and Undo brings it back', !!await until(async () => (await titles()).includes('Talk to Dara about QA capacity for the beta') && (await counts()).today === 5, 5000));
|
|
136
154
|
const call = await open('Call Priya about the Q4 budget variance');
|
|
137
|
-
ok('an item moved before says how often', !!call && /Moved
|
|
155
|
+
ok('an item moved before says how often', !!call && /Moved 1×/.test(await st((s) => document.querySelector(s).textContent, W + ' ' + call)));
|
|
138
156
|
if (call) await click(W + ' ' + call + ' [data-pm-notyet="tomorrow"]');
|
|
139
157
|
ok('Not yet, tomorrow: it leaves Today and Upcoming gains it', !!await until(async () => { const c = await counts(); return !(await titles()).includes('Call Priya about the Q4 budget variance') && c.today === 4 && c.upcoming === 7; }, 5000), JSON.stringify(await counts()));
|
|
140
158
|
|
|
141
159
|
/* Upcoming (R5) */
|
|
142
160
|
await view('upcoming');
|
|
143
|
-
const days = await st((w) => [...document.querySelectorAll(w + ' [data-pm-day]')].map((d) => ({ key: d.getAttribute('data-pm-day'), head: d.querySelector('h3').textContent, rows: [...d.querySelectorAll('[data-pm-title]')].map((t) => t.textContent) })), W);
|
|
144
|
-
ok('Upcoming
|
|
161
|
+
const days = await st((w) => [...document.querySelectorAll(w + ' [data-pm-day]')].map((d) => ({ key: d.getAttribute('data-pm-day'), head: (d.querySelector('h3, h2') || {}).textContent, rows: [...d.querySelectorAll('[data-pm-title]')].map((t) => t.textContent), clear: /Clear/.test((d.querySelector('.wkday-none') || {}).textContent || '') })), W);
|
|
162
|
+
ok('Upcoming is a Next 7 days card with one heading per day, then a separate Later card', days.length === 8 && days[0].key === '2026-10-09' && /^Tomorrow/.test(days[0].head) && days[7].key === 'later' && /^Later/.test(days[7].head) && days[1].clear, JSON.stringify(days.map((d) => d.key + ':' + d.head)));
|
|
145
163
|
ok('…each item under the day it arrives', days.length === 8 && days[0].rows.includes('Set up a meeting with Sam about the logging rollout') && days[0].rows.includes('Call Priya about the Q4 budget variance')
|
|
146
164
|
&& days.find((d) => d.key === '2026-10-12').rows.includes('Write release notes for SSO 1.0') && days[7].rows.includes('Write the Q1 discovery plan'), JSON.stringify(days.map((d) => d.rows)));
|
|
147
165
|
const nb = await st((w) => { const s = [...document.querySelectorAll(w + ' [data-pm-item]')].find((r) => /Write release notes for SSO 1\.0/.test(r.textContent)); const c = s && s.querySelector('[data-pm-notbefore]'); return c ? c.textContent : null; }, W);
|
|
148
166
|
ok('a deferred item says "Not before" its date', nb === 'Not before Mon 12 Oct', String(nb));
|
|
149
167
|
const adv = await st((w) => [...document.querySelectorAll(w + ' [data-pm-resolves]')].map((n) => n.textContent), W);
|
|
150
|
-
ok('an item that advances a team task names it ("Advances
|
|
168
|
+
ok('an item that advances a team task names it ("Advances team task <title>", or the id when the title is unknown)', adv.includes('Advances team task SSO general release checklist') && adv.includes('Advances team task returns-interview-5'), JSON.stringify(adv));
|
|
151
169
|
const sam = await open('Set up a meeting with Sam about the logging rollout');
|
|
152
170
|
if (sam) { await page.fill(W + ' ' + sam + ' [data-pm-outcome]', 'Booked for Tuesday 10:00'); await click(W + ' ' + sam + ' [data-pm-offline-done]'); }
|
|
153
171
|
ok('Done with a one-line outcome closes an offline item', !!sam && !!await until(async () => !(await titles()).includes('Set up a meeting with Sam about the logging rollout') && (await counts()).upcoming === 6, 5000));
|
|
@@ -160,6 +178,9 @@ const W = '[data-pm-work]';
|
|
|
160
178
|
|
|
161
179
|
/* Review (R7) */
|
|
162
180
|
await view('review');
|
|
181
|
+
const drafts = await st((w) => [...document.querySelectorAll(w + ' [data-pm-review-drafts] [data-draft-row] .tt')].map((n) => n.textContent), W);
|
|
182
|
+
ok('Review also carries the agent\'s drafts that Briefing used to show', DRAFTS.every((t) => drafts.includes(t)) && drafts.length >= 6, JSON.stringify(drafts));
|
|
183
|
+
ok('…and its badge still counts the weekly review only', (await counts()).review === 3);
|
|
163
184
|
const prog = () => st((w) => { const r = document.querySelector(w + ' [data-pm-review]'); return r ? { id: r.getAttribute('data-pm-review'), p: r.querySelector('[data-pm-review-progress]').textContent, t: r.querySelector('[data-pm-title]').textContent } : null; }, W);
|
|
164
185
|
const r1 = await until(prog, 5000);
|
|
165
186
|
ok('Review shows one item at a time, the most stale first, with progress', !!r1 && r1.p === '1 of 3' && r1.t === 'Clean up the Checkout Jira labels', JSON.stringify(r1));
|
|
@@ -174,6 +195,17 @@ const W = '[data-pm-work]';
|
|
|
174
195
|
if (r3) await click(W + ' [data-pm-review-drop="' + r3.id + '"]');
|
|
175
196
|
ok('Drop ends the pass with "Review done"', !!await until(() => st((w) => { const d = document.querySelector(w + ' [data-pm-review-done]'); return !!d && /Review done/.test(d.textContent); }, W), 5000));
|
|
176
197
|
|
|
198
|
+
/* Focus and the header on every view; Inbox and Waiting carry their own furniture */
|
|
199
|
+
const every = [];
|
|
200
|
+
for (const v of VIEWS) { await view(v); every.push(await st((w) => !document.querySelector(w + ' [data-pm-flow]') && !!document.querySelector(w + ' .bcol.r [data-pm-focus]'), W)); }
|
|
201
|
+
ok('Focus shows on every view, and Flow today on none', every.every(Boolean), JSON.stringify(every));
|
|
202
|
+
await view('inbox');
|
|
203
|
+
ok('Inbox: an Inbox card, a Decide button that opens the row, and the note about Initiatives', await st((w) => !!document.querySelector(w + ' [data-pm-list="inbox"] h2') && /^Inbox/.test(document.querySelector(w + ' [data-pm-list="inbox"] h2').textContent) && !!document.querySelector(w + ' [data-pm-decide]') && /triaged in/.test((document.querySelector(w + ' [data-pm-inbox-note]') || {}).textContent || ''), W));
|
|
204
|
+
await click(W + ' [data-pm-decide]');
|
|
205
|
+
ok('…Decide opens the row\'s case file', !!await until(() => st((w) => !!document.querySelector(w + ' .xp.open [data-pm-case]'), W), 4000));
|
|
206
|
+
await view('waiting');
|
|
207
|
+
ok('Waiting: each row says who and since when, and has a Nudge button that sends nothing', await st((w) => { const r = document.querySelector(w + ' [data-pm-list="waiting"] [data-pm-item]'); return !!r && /^Waiting for \S/.test((r.querySelector('[data-pm-waiting-chip]') || {}).textContent || '') && !!r.querySelector('[data-pm-nudge]'); }, W));
|
|
208
|
+
ok('every view header names how many decisions wait in Briefing and offers Open Briefing', await st((w) => /decisions? waits? in Briefing, Needs you\./.test(document.querySelector(w + ' [data-pm-lens-head]').textContent) && !!document.querySelector(w + ' [data-pm-open-briefing]'), W));
|
|
177
209
|
/* capture (R2): the chip names the view the server will place the line in */
|
|
178
210
|
const chip = async (line) => { await page.fill(W + ' [data-pm-add]', line); return until(() => st((w) => { const t = document.querySelector(w + ' [data-pm-chips] [data-pm-lands]'); return t ? t.textContent : null; }, W), 3000); };
|
|
179
211
|
const lands = [await chip('Read the security brief'), await chip('Send Dana the new date by tomorrow'), await chip('Ship the fix due today'), await chip('Get the contract back waiting on Lee')];
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Items the console3 page makes actionable on more than one tab today (c3-one-home-test.js). Known-failing until the Phase 2 restructure of the console3 feedback plan; the suite fails only on a duplicate that is not listed here. Remove an entry when its duplicate is gone.",
|
|
3
|
-
"duplicates": [
|
|
4
|
-
{
|
|
5
|
-
"title": "sso is live with 3 customers",
|
|
6
|
-
"tabs": [
|
|
7
|
-
"brief",
|
|
8
|
-
"init"
|
|
9
|
-
]
|
|
10
|
-
}
|
|
11
|
-
]
|
|
3
|
+
"duplicates": []
|
|
12
4
|
}
|
|
@@ -9,9 +9,21 @@ timestamp: 2026-10-11
|
|
|
9
9
|
|
|
10
10
|
## Cards
|
|
11
11
|
|
|
12
|
-
Every card in a deck has the same height. Text that does not fit is cut short. Press a card to turn it over. The card turns in
|
|
12
|
+
Every card in a deck has the same height at rest. Text that does not fit is cut short. The front shows the numbers the card carries as tiles, with a short kind label. Press a card to turn it over. The card turns in about a third of a second. The back grows to fit its content, so the options and the confirm control are never cut off. The other cards in the row keep their size, and the page below the deck moves down. Press the top line of the back to turn it again. When you decide, the card shows a check and a message offers **Undo**. With reduced motion on, the card changes at once.
|
|
13
13
|
|
|
14
|
-
The
|
|
14
|
+
The page background is white. A soft, slow glow of colour drifts behind the page. With reduced motion on, it is one still picture.
|
|
15
|
+
|
|
16
|
+
The tabs show counts: Briefing shows how many things need you, Work shows its inbox, and Initiatives shows new asks to triage. The Sessions button and the dock button show a bolt. In the demo, the Record button and its arrow are visible when nothing is recording.
|
|
17
|
+
|
|
18
|
+
### Kinds of card
|
|
19
|
+
|
|
20
|
+
The label on a card says what kind of thing happened, for example **Deadline moved**, **Promise out of date**, **Blocked**, **Conflict**, **Risk**, **Waiting on someone** or **Report due**. A win keeps the label **Big win** or **Small win**. The colour of the card shows how serious it is: red is critical, amber is a warning, green is good news, and the accent colour is neutral.
|
|
21
|
+
|
|
22
|
+
Each kind has a short list of what its card must say. For example, a **Deadline moved** card says what moved, by how much, by when you must decide and the one move. When the card does not know a part, the back of the card says so under **Not known yet**. PMOS does not guess a missing part.
|
|
23
|
+
|
|
24
|
+
A card shows only when it is relevant enough and links to a page that you can open. Critical cards always show. When nothing is relevant enough, there is no card.
|
|
25
|
+
|
|
26
|
+
The icon of a card comes from fixed rules over the kind of the card. A kind the rules do not know gets a neutral icon. No card text leaves your machine to choose an icon.
|
|
15
27
|
|
|
16
28
|
## Confirm an act
|
|
17
29
|
|
|
@@ -37,4 +49,4 @@ Needs you also lists your own items that are overdue or due today. Overdue items
|
|
|
37
49
|
|
|
38
50
|
## Work
|
|
39
51
|
|
|
40
|
-
The Work page
|
|
52
|
+
The Work page shows **Focus** in its right column. There is no Flow today card and no Jobs card: you open a job from a Focus row.
|
|
@@ -7,22 +7,26 @@ timestamp: 2026-10-11
|
|
|
7
7
|
|
|
8
8
|
# The Briefing: today's brief
|
|
9
9
|
|
|
10
|
-
The Briefing is the first page of the console at `/c3.html`. It
|
|
10
|
+
The Briefing is the first page of the console at `/c3.html`. It has two columns. The left column holds the greeting, the headline cards and Needs you. The right column holds Today and Portfolio pulse. It holds no calendar. The full calendar is the Calendar tab.
|
|
11
|
+
|
|
12
|
+
Review is in the Work page, in its Review lens. Handled, What I could stop and Data you can lean on are in Reports, under Drafts.
|
|
11
13
|
|
|
12
14
|
The Briefing shows only what PMOS already holds. When PMOS holds nothing for a section, the section says so.
|
|
13
15
|
|
|
14
16
|
## Headline cards
|
|
15
17
|
|
|
16
|
-
The cards at the top are the signals that pass the relevance bar. Turn a card over to read its case.
|
|
18
|
+
The cards at the top are the signals that pass the relevance bar. Turn a card over to read its case.
|
|
19
|
+
|
|
20
|
+
Above the cards, the greeting names the part of the day, for example "Good morning, Wawan". The name is the first word of the git user name on the computer where PMOS runs. When git holds no name, the greeting has none. Five buttons stand for morning, midday, afternoon, evening and night. Press one to preview that part of the day: its line and its cards. Nothing is saved. Press the part that is now, or wait for the clock to move on, and the preview ends.
|
|
17
21
|
|
|
18
22
|
## Today
|
|
19
23
|
|
|
20
|
-
Today lists the meetings of the day, in time order. Each row shows:
|
|
24
|
+
Today starts with one line: the meeting that is on now or next, how many meetings are left, and how many of them are prepped, with a small ring. **Open calendar** opens the calendar. Under it, Today lists the meetings of the day, in time order. Each row shows:
|
|
21
25
|
|
|
22
26
|
- The start time and the length.
|
|
23
27
|
- The people and the initiative of the meeting.
|
|
24
|
-
- **Prepared
|
|
25
|
-
- **Before
|
|
28
|
+
- **Prepared**, as chips: what PMOS holds for the meeting. This is the agent's prep note, your notes on the event, the notes you started for it, and the last notes of the same meeting. When PMOS holds none, the row says "Nothing prepared yet".
|
|
29
|
+
- **Before you go**, as chips: your open items that name the meeting. A meeting with nothing held says "nothing prepared". A meeting with everything done says "prep ready".
|
|
26
30
|
|
|
27
31
|
A meeting that is over is dimmed. The meeting under way is marked **Now**. The next one is marked **Next**. When the calendar holds no meeting for today, Today says so.
|
|
28
32
|
|
|
@@ -30,7 +34,7 @@ A Today row is a reference. It has no button that changes anything.
|
|
|
30
34
|
|
|
31
35
|
## Needs you
|
|
32
36
|
|
|
33
|
-
Needs you lists what only you can do. The rows come in this order:
|
|
37
|
+
Needs you lists what only you can do. Its header says how many decisions, how many items are yours, and how many are overdue. A decision row has a **decision** tag. When it cannot be undone it also says **hard to undo**. An item of yours has a **Work** chip: Work is where the item lives. The rows come in this order:
|
|
34
38
|
|
|
35
39
|
1. Your own items that are overdue. The most late is first. The row says how late.
|
|
36
40
|
2. Your own items due today.
|
|
@@ -47,7 +51,7 @@ PMOS can see evidence that an outside item is done. For example, a meeting with
|
|
|
47
51
|
|
|
48
52
|
## Portfolio pulse
|
|
49
53
|
|
|
50
|
-
Portfolio pulse shows one bar for the status of the initiatives and one row for each initiative. Press a row to open it in place. It shows why the initiative has that status, and the promised date against the forecast. The page does not change.
|
|
54
|
+
Portfolio pulse says how many initiatives need attention, then shows one bar for the status of the initiatives and one row for each initiative. **All initiatives** at the bottom opens the Initiatives page. Press a row to open it in place. It shows why the initiative has that status, and the promised date against the forecast. The page does not change.
|
|
51
55
|
|
|
52
56
|
To go to the initiative, press **Open initiative** inside the opened row.
|
|
53
57
|
|
|
@@ -57,4 +61,4 @@ An item has its actions in one place only. On the Briefing, Today and Portfolio
|
|
|
57
61
|
|
|
58
62
|
## On a phone
|
|
59
63
|
|
|
60
|
-
At 400 pixels wide the Briefing is one column with no sideways scroll. Each button in an opened row is 44 pixels high.
|
|
64
|
+
At 400 pixels wide the Briefing is one column (greeting, cards, Needs you, Today, Portfolio pulse) with no sideways scroll. Each button in an opened row is 44 pixels high.
|
|
@@ -7,7 +7,7 @@ timestamp: 2026-10-11
|
|
|
7
7
|
|
|
8
8
|
# Calendar popup
|
|
9
9
|
|
|
10
|
-
The calendar popup shows the
|
|
10
|
+
The calendar popup shows the Calendar tab's grid in a drawer over any page of the console (`/c3.html`). You do not leave the page you are on.
|
|
11
11
|
|
|
12
12
|
## Open it
|
|
13
13
|
|
|
@@ -23,7 +23,7 @@ The key **C** does nothing in these cases:
|
|
|
23
23
|
|
|
24
24
|
## Use it
|
|
25
25
|
|
|
26
|
-
The popup has the same
|
|
26
|
+
The popup has the same grid as the Calendar tab, with the same five zoom levels, the layer chips, the detail, the Free time list and the lanes. It has no headline deck. It opens on the Week the first time. Press **+** or **-** to zoom. The popup opens at the zoom you used last, and with the layers you hid left hidden. The console keeps that zoom until you change it.
|
|
27
27
|
|
|
28
28
|
The popup and the Calendar tab are two separate views. A zoom or a date you choose in one view does not change the other.
|
|
29
29
|
|