create-pmos 0.186.0 → 0.187.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/scripts/workflow-state.js +2 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-ib-triage-backlog-test.js +20 -13
- package/workspace/c3-ini-life-test.js +167 -0
- package/workspace/c3-initiatives-test.js +6 -6
- package/workspace/c3-phone-test.js +67 -16
- package/workspace/c3-quiet-test.js +2 -1
- package/workspace/guide/initiative-boards.md +37 -2
- package/workspace/guide/phone-companion.md +19 -4
- package/workspace/lib/engine/boards.js +73 -18
- package/workspace/lib/engine/workflow-state.js +2 -1
- package/workspace/lib/initiative-rows.js +2 -2
- package/workspace/lib/routes-boards.js +119 -6
- package/workspace/lib/routes-meeting-focus.js +19 -7
- package/workspace/lib/vendor/scripts/workflow-state.js +2 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-boards.js +82 -17
- package/workspace/public/pm/c3-demo-focus.js +39 -1
- package/workspace/public/pm/c3-focus.css +53 -0
- package/workspace/public/pm/c3-focus.js +188 -44
- package/workspace/public/pm/c3-ib-initiatives.js +127 -60
- package/workspace/public/pm/c3-ib-items.js +2 -2
- package/workspace/public/pm/c3-ini-life.css +32 -0
- package/workspace/public/pm/c3-ini-life.js +379 -0
- package/workspace/public/pm/c3-ini-ui.js +12 -7
- package/workspace/public/pm/c3-initiatives.js +14 -4
- package/workspace/public/pm/c3-phone.css +3 -1
- package/workspace/public/pm/c3-phone.js +28 -8
- package/workspace/server.js +7 -2
- package/workspace/test-lists/c3-ini-life.json +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.187.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-13T05:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T05:30:39Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-13T05:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T05:30:39Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -1603,7 +1603,8 @@ function readLifecycle(root) {
|
|
|
1603
1603
|
for (const r of cachedJsonl(path.join(L.runsDir, 'lifecycle.jsonl'))) {
|
|
1604
1604
|
if (!r || typeof r.initiative_id !== 'string') continue;
|
|
1605
1605
|
if (r.state !== 'active' && !STOPPED_STATES.includes(r.state)) continue;
|
|
1606
|
-
|
|
1606
|
+
// `back` (c3-initiative-lifecycle): the day an on-hold initiative comes back to the PM, when the hold named one.
|
|
1607
|
+
latest.set(r.initiative_id, { state: r.state, reason: r.reason || '', pm: r.pm || '', at: r.at || '', ...(r.state === 'on-hold' && /^\d{4}-\d{2}-\d{2}$/.test(r.back || '') ? { back: r.back } : {}) });
|
|
1607
1608
|
}
|
|
1608
1609
|
return latest;
|
|
1609
1610
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-13T05:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T05:30:39Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* PMOS workspace — the Triage and Initiatives tabs of a board on the console3 Initiatives page (mockup v5, 34-initiatives.js tabTriage and
|
|
3
3
|
* tabInitiatives), in demo mode (browser). Triage: add an ask and see its intake draft, Accept (the anchor is required; the initiative
|
|
4
|
-
* starts on the board's default workflow), Ask for more, Decline with a reason. Initiatives: the
|
|
5
|
-
* owner filters (comma lists for "At risk or late"), sort with its direction, the numbered pager, the Move
|
|
6
|
-
* is the server's page of 25.
|
|
4
|
+
* starts on the board's default workflow), Ask for more, Decline with a reason. Initiatives: the view menu, the category, status and
|
|
5
|
+
* owner filters in the filter menu (comma lists for "At risk or late"), sort with its direction, the numbered pager, the Move items of
|
|
6
|
+
* the row menu, and that every answer is the server's page of 25 (c3-initiative-lifecycle moved the controls into one row).
|
|
7
7
|
*
|
|
8
8
|
* The files it drives (named so `run-tests.js --changed` traces a change in any of them to this suite): public/pm/c3-ib-triage.js,
|
|
9
|
-
* c3-ib-initiatives.js, c3-initiatives.js, c3-initiatives.css, c3-ini-ui.js, c3-ini-work.js, c3-demo-boards.js, c3-demo.js, c3-sheet.js,
|
|
9
|
+
* c3-ib-initiatives.js, c3-ini-life.js, c3-initiatives.js, c3-initiatives.css, c3-ini-ui.js, c3-ini-work.js, c3-demo-boards.js, c3-demo.js, c3-sheet.js,
|
|
10
10
|
* c3-toast.js and c3-ini-harness.js. Its list fragment is test-lists/c3-ib-triage-backlog.json.
|
|
11
11
|
*/
|
|
12
12
|
'use strict';
|
|
@@ -71,37 +71,44 @@ const { harness } = require('./c3-ini-harness');
|
|
|
71
71
|
const seq = () => st(() => Number(document.querySelector('[data-ini-panel="initiatives"]').dataset.ibbSeq));
|
|
72
72
|
const after = async (s0) => { await until(async () => (await seq()) > s0 && (await st(() => document.querySelector('[data-ini-panel="initiatives"]').dataset.ibbState)) === 'ready'); };
|
|
73
73
|
await ready();
|
|
74
|
-
const
|
|
75
|
-
|
|
74
|
+
const total = () => st(() => document.querySelector('[data-ibb="body"] .sh2 .bdg').textContent);
|
|
75
|
+
const count0 = await st(() => document.querySelector('[data-ibb="views"]').textContent);
|
|
76
|
+
ok('L1 the tab opens on Now and next, 25 rows a page', (await rows()).length === 25 && /Now and next/.test(count0), count0);
|
|
77
|
+
/* the filter menu holds the selects; a pick reads the server again */
|
|
78
|
+
const pickFilter = async (k, v) => { if (!(await st(() => !!document.querySelector('[data-ibb-filter]')))) await page.click('[data-ibb="filters"]'); await page.selectOption('[data-ibb-filter="' + k + '"]', v); };
|
|
79
|
+
const pickView = async (k) => { await page.keyboard.press('Escape'); await page.click('[data-ibb="views"]'); await page.click('[data-ibb-view="' + k + '"]'); };
|
|
76
80
|
const r0 = await st(() => document.querySelector('[data-ibb-row]').textContent);
|
|
77
81
|
ok('L2 a row shows its workflow template, its stage dots, status, owner, progress and dates', /Product|Quick fix|Research only/.test(r0) && await st(() => !!document.querySelector('[data-ibb-row] .ini-steps') && !!document.querySelector('[data-ibb-row] .ini-mini')), r0.slice(0, 200));
|
|
78
82
|
let s0 = await seq();
|
|
79
|
-
await
|
|
83
|
+
await pickFilter('status', 'at risk,late');
|
|
80
84
|
await after(s0);
|
|
81
85
|
const st1 = await st(() => [...document.querySelectorAll('[data-ibb-row]')].every((r) => /At risk|Late/.test(r.textContent)));
|
|
82
86
|
ok('L3 "At risk or late" is a comma list the server reads as any of', st1 && (await rows()).length > 0);
|
|
83
87
|
s0 = await seq();
|
|
84
|
-
await
|
|
88
|
+
await pickView('all');
|
|
85
89
|
await after(s0);
|
|
86
90
|
s0 = await seq();
|
|
87
|
-
await
|
|
91
|
+
await pickFilter('cat', 'Learn');
|
|
88
92
|
await after(s0);
|
|
89
93
|
const learn = await st(() => [...document.querySelectorAll('[data-ibb-row]')].map((r) => r.dataset.cat));
|
|
90
94
|
ok('L4 the Category filter keeps one category, whatever each initiative\'s workflow calls its stage', learn.length > 0 && learn.every((c) => c === 'Learn'), learn.join(','));
|
|
91
95
|
s0 = await seq();
|
|
96
|
+
await page.keyboard.press('Escape');
|
|
92
97
|
await page.click('[data-ibb="clear"]');
|
|
93
98
|
await after(s0);
|
|
94
99
|
const pager = await st(() => [...document.querySelectorAll('[data-ibb-page]')].map((b) => b.dataset.ibbPage).join(','));
|
|
95
|
-
ok('L5 All has the numbered pager (
|
|
100
|
+
ok('L5 All has the numbered pager (125 initiatives, 5 pages)', /^1,2,.*5$/.test(pager) && (await total()) === '125', pager);
|
|
96
101
|
s0 = await seq();
|
|
97
102
|
await page.click('[data-ibb-page="5"]');
|
|
98
103
|
await after(s0);
|
|
99
|
-
ok('L6 a page number reads that page from the server', (await rows()).length ===
|
|
104
|
+
ok('L6 a page number reads that page from the server', (await rows()).length === 25 && /101–125 of 125/.test(await st(() => document.querySelector('[data-ibb="pager"]').textContent)));
|
|
100
105
|
s0 = await seq();
|
|
101
|
-
await page.
|
|
106
|
+
await page.click('[data-ibb="sort"]');
|
|
107
|
+
await page.click('[data-ibb-sort="title"]');
|
|
102
108
|
await after(s0);
|
|
103
109
|
const t1 = await st(() => [...document.querySelectorAll('[data-ibb-row] .tt')].map((n) => n.textContent));
|
|
104
110
|
s0 = await seq();
|
|
111
|
+
await page.click('[data-ibb="sort"]');
|
|
105
112
|
await page.click('[data-ibb="dir"]');
|
|
106
113
|
await after(s0);
|
|
107
114
|
const t2 = await st(() => [...document.querySelectorAll('[data-ibb-row] .tt')].map((n) => n.textContent));
|
|
@@ -112,7 +119,7 @@ const { harness } = require('./c3-ini-harness');
|
|
|
112
119
|
await page.click('[data-ibb-move="IDPF-07"]');
|
|
113
120
|
await until(() => st(() => !!document.querySelector('[data-ini="menu"] [data-ini-moveto]')));
|
|
114
121
|
const opts = await st(() => [...document.querySelectorAll('[data-ini-moveto]')].map((b) => b.dataset.iniMoveto).join(','));
|
|
115
|
-
ok('L8
|
|
122
|
+
ok('L8 the row menu lists the stages of the initiative\'s own workflow first', opts === 'Discover,Plan,Build & Deliver,Monitor & Evaluate', opts);
|
|
116
123
|
await page.click('[data-ini-moveto="Discover"]');
|
|
117
124
|
await until(() => store(() => window.__c3boards.rows.find((r) => r.id === 'IDPF-07').stage === 'Discover'));
|
|
118
125
|
await until(async () => /IDPF-07 moved to Discover/.test(await toast()));
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* PMOS workspace — initiative lifecycle on the console3 Initiatives page, in demo mode (browser): c3-initiative-lifecycle, done-criteria
|
|
3
|
+
* 4 and 5 (planning/prd/c3-initiative-lifecycle-prd.md; mockup v2, approved 2026-10-13).
|
|
4
|
+
*
|
|
5
|
+
* H the board's Initiatives tab at 1280×860 has two rows of controls above the list: the context row (breadcrumb, status, the tabs,
|
|
6
|
+
* a Board settings icon) and the toolbar (search, view menu, filter and sort icons, New initiative); no view pills and no
|
|
7
|
+
* labelled dropdowns; the filter menu groups Status into Active, Paused and Closed, and its badge counts the filters on
|
|
8
|
+
* S On hold and Cancelled are statuses: a paused row shows since when, the check-back day and the reason; a cancelled row is hidden
|
|
9
|
+
* until "N cancelled hidden" or the Status filter asks for it
|
|
10
|
+
* M from the row menu: Put on hold (with a reason and a check-back day), Resume, Cancel (a reason is required), Reopen, Archive and
|
|
11
|
+
* Restore, each with Undo on its toast; the archive view; Merge (the duplicate hint, the picked task moves, Undo merge moves it back)
|
|
12
|
+
* P the initiative page: the banner of an on-hold and of a merged initiative, and its "…" menu
|
|
13
|
+
*
|
|
14
|
+
* The files it drives (named so `run-tests.js --changed` traces a change in any of them to this suite): public/pm/c3-ini-life.js,
|
|
15
|
+
* c3-ini-life.css, c3-ib-initiatives.js, c3-initiatives.js, c3-ini-ui.js, c3-demo-boards.js, c3-quiet.js, c3-quiet.css, c3-sheet.js,
|
|
16
|
+
* c3-toast.js and c3-ini-harness.js. Its list fragment is test-lists/c3-ini-life.json.
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
const { harness } = require('./c3-ini-harness');
|
|
20
|
+
|
|
21
|
+
(async () => {
|
|
22
|
+
const H = await harness('c3-ini-life', { width: 1280 });
|
|
23
|
+
const { page, ok, until, st, open, toast } = H;
|
|
24
|
+
await page.setViewportSize({ width: 1280, height: 860 });
|
|
25
|
+
const store = (f, a) => st(f, a);
|
|
26
|
+
const row = (id) => store((x) => { const r = window.__c3boards.rows.find((y) => y.id === x); return r ? { status: r.status, stopped: r.stopped, archived: r.archived } : null; }, id);
|
|
27
|
+
const ready = () => until(() => st(() => { const p = document.querySelector('[data-ini-panel="initiatives"]'); return p && p.dataset.ibbState === 'ready'; }));
|
|
28
|
+
const rows = () => st(() => [...document.querySelectorAll('[data-ibb="rows"] [data-ibb-row]')].map((r) => r.dataset.ibbRow));
|
|
29
|
+
const seq = () => st(() => Number(document.querySelector('[data-ini-panel="initiatives"]').dataset.ibbSeq));
|
|
30
|
+
const after = async (s0) => { await until(async () => (await seq()) > s0 && (await st(() => document.querySelector('[data-ini-panel="initiatives"]').dataset.ibbState)) === 'ready'); };
|
|
31
|
+
const menuFor = async (id, pick) => { await page.click('[data-ibb-menu="' + id + '"]'); await until(() => st(() => !!document.querySelector('[data-ini="menu"]'))); if (pick) await page.click('[data-ini="menu"] ' + pick); };
|
|
32
|
+
const sheetText = () => st(() => { const s = document.querySelector('[data-il="sheet"]'); return s ? s.textContent : ''; });
|
|
33
|
+
const toastHas = (re) => until(async () => re.test(await toast()));
|
|
34
|
+
const undo = () => page.click('[data-c3-toast-action]');
|
|
35
|
+
|
|
36
|
+
/* ── H · the header ── */
|
|
37
|
+
await open('board:idpf:initiatives');
|
|
38
|
+
await ready();
|
|
39
|
+
const geo = await st(() => {
|
|
40
|
+
const p = document.querySelector('[data-ini="page"]');
|
|
41
|
+
const list = p.querySelector('[data-ibb="body"]').getBoundingClientRect().top;
|
|
42
|
+
const vis = (n) => { const r = n.getBoundingClientRect(); const cs = getComputedStyle(n); return r.width > 0 && r.height > 0 && cs.visibility !== 'hidden' && r.bottom <= list; };
|
|
43
|
+
const ctrls = [...p.querySelectorAll('button, input, select')].filter(vis);
|
|
44
|
+
const tops = ctrls.map((n) => n.getBoundingClientRect().top).sort((a, b) => a - b);
|
|
45
|
+
const bands = tops.reduce((acc, t) => (acc.length && t - acc[acc.length - 1] < 24 ? acc : [...acc, t]), []); // controls within 24px share a row
|
|
46
|
+
return { rows: bands.length, pills: [...p.querySelectorAll('[data-ibb-view]')].filter(vis).length, selects: [...p.querySelectorAll('select')].filter(vis).length,
|
|
47
|
+
gear: (() => { const g = p.querySelector('[data-ini="board-settings"]'); return g && vis(g) ? { label: g.getAttribute('aria-label'), text: g.textContent.trim() } : null; })() };
|
|
48
|
+
});
|
|
49
|
+
ok('H1 two rows of controls above the list; no view pills, no labelled dropdowns', geo.rows === 2 && geo.pills === 0 && geo.selects === 0, JSON.stringify(geo));
|
|
50
|
+
ok('H2 Board settings is an icon button with its name as the label', !!geo.gear && geo.gear.label === 'Board settings' && geo.gear.text === '', JSON.stringify(geo.gear));
|
|
51
|
+
await page.click('[data-ibb="filters"]');
|
|
52
|
+
const groups = await st(() => [...document.querySelectorAll('[data-ibb-filter="status"] optgroup')].map((g) => g.label + ':' + [...g.children].map((o) => o.textContent).join('/')).join(' | '));
|
|
53
|
+
ok('H3 the filter menu groups Status into Active, Paused and Closed', groups === 'Active:All active/On track/At risk or late/At risk/Late | Paused:On hold | Closed:Done or cancelled/Done/Cancelled', groups);
|
|
54
|
+
let s0 = await seq();
|
|
55
|
+
await page.selectOption('[data-ibb-filter="owner"]', 'You');
|
|
56
|
+
await after(s0);
|
|
57
|
+
const badge = await st(() => { const b = document.querySelector('[data-ibb="filters"]'); return b.getAttribute('aria-label') + '|' + ((b.querySelector('.bdg') || {}).textContent || ''); });
|
|
58
|
+
const chips = await st(() => [...document.querySelectorAll('[data-ibb-unset]')].map((c) => c.textContent.trim()).join(','));
|
|
59
|
+
ok('H4 a filter on: the icon badge counts it, and it shows as a removable chip in the list header', badge === 'Filters, 1 on|1' && /^Owner: You/.test(chips), badge + ' ' + chips);
|
|
60
|
+
await page.keyboard.press('Escape');
|
|
61
|
+
s0 = await seq();
|
|
62
|
+
await page.click('[data-ibb-unset="owner"]');
|
|
63
|
+
await after(s0);
|
|
64
|
+
ok('H5 removing the chip removes the filter', await st(() => !document.querySelector('[data-ibb-unset]') && !document.querySelector('[data-ibb="filters"] .bdg')));
|
|
65
|
+
|
|
66
|
+
/* ── S · on hold and cancelled as statuses ── */
|
|
67
|
+
await open('board:idpf:initiatives:view=all');
|
|
68
|
+
await ready();
|
|
69
|
+
const held = await st(() => { const r = document.querySelector('[data-ibb-row="IDPF-152"]'); return r ? r.textContent : ''; });
|
|
70
|
+
ok('S1 an on-hold row: On hold, since when, the check-back day and the reason', /On hold/.test(held) && /since /.test(held) && /check back /.test(held) && /Q1 budget/.test(held), held.slice(0, 220));
|
|
71
|
+
ok('S2 a cancelled row is hidden from All, and the footer says so', !(await rows()).includes('IDPF-153') && /1 cancelled hidden/.test(await st(() => document.querySelector('[data-ibb="foot"]').textContent)));
|
|
72
|
+
s0 = await seq();
|
|
73
|
+
await page.click('[data-ibb="show-cancelled"]');
|
|
74
|
+
await after(s0);
|
|
75
|
+
ok('S3 "1 cancelled hidden" opens the cancelled ones (Status: Cancelled)', (await rows()).join() === 'IDPF-153' && /Status: Cancelled/.test(await st(() => document.querySelector('[data-ibb="chips"]').textContent)));
|
|
76
|
+
|
|
77
|
+
/* ── M · the row menu ── */
|
|
78
|
+
await open('board:idpf:initiatives:q=IDPF-07&view=all');
|
|
79
|
+
await ready();
|
|
80
|
+
await menuFor('IDPF-07');
|
|
81
|
+
const groupsM = await st(() => [...document.querySelectorAll('[data-ini="menu"] .ini-mlbl')].map((n) => n.textContent).join(','));
|
|
82
|
+
ok('M1 the row menu: Move to a stage, Status, Tidy up', groupsM === 'Move to a stage,Status,Tidy up' && await st(() => !!document.querySelector('[data-ini="menu"] [data-il-hold]') && !!document.querySelector('[data-ini="menu"] [data-il-archive]')), groupsM);
|
|
83
|
+
await page.click('[data-ini="menu"] [data-il-hold]');
|
|
84
|
+
await until(() => st(() => !!document.querySelector('[data-il="sheet"] [data-il-do="hold"]')));
|
|
85
|
+
await page.fill('[data-il-text="reason"]', 'Waiting on the partner contract');
|
|
86
|
+
await page.click('[data-il-set="back"][data-v="two"]');
|
|
87
|
+
ok('M2 Put on hold asks why and when to check back, and says what happens', /WHY IS IT PAUSED/i.test(await sheetText()) && /never turns Late/.test(await sheetText()) && await st(() => document.querySelector('[data-il-set="back"][data-v="two"]').getAttribute('aria-pressed') === 'true'));
|
|
88
|
+
await page.click('[data-il-do="hold"]');
|
|
89
|
+
await until(async () => ((await row('IDPF-07')) || {}).status === 'on hold');
|
|
90
|
+
const h7 = await row('IDPF-07');
|
|
91
|
+
ok('M3 the hold is recorded with its reason and a check-back day', h7.stopped && h7.stopped.reason === 'Waiting on the partner contract' && /^\d{4}-\d{2}-\d{2}$/.test(h7.stopped.back || ''), JSON.stringify(h7));
|
|
92
|
+
await toastHas(/IDPF-07 is on hold/);
|
|
93
|
+
await undo();
|
|
94
|
+
await until(async () => ((await row('IDPF-07')) || {}).status === 'on track');
|
|
95
|
+
ok('M4 Undo on the toast makes it active again', ((await row('IDPF-07')) || {}).status === 'on track');
|
|
96
|
+
await ready();
|
|
97
|
+
await until(async () => (await st(() => (document.querySelector('[data-ibb-row="IDPF-07"]') || {}).dataset.status)) === 'on track');
|
|
98
|
+
await menuFor('IDPF-07', '[data-il-cancel]');
|
|
99
|
+
await until(() => st(() => !!document.querySelector('[data-il="sheet"]')));
|
|
100
|
+
ok('M5 Cancel needs a reason before it can be pressed', await st(() => !document.querySelector('[data-il-do="cancel"]') && /Pick a reason/.test(document.querySelector('[data-il="sheet"]').textContent)));
|
|
101
|
+
await page.click('[data-il-set="why"][data-v="replaced"]');
|
|
102
|
+
ok('M6 "Replaced by another initiative" points to Merge', await st(() => !!document.querySelector('[data-il="merge-instead"]')));
|
|
103
|
+
await page.click('[data-il-set="why"][data-v="worth"]');
|
|
104
|
+
await page.click('[data-il-do="cancel"]');
|
|
105
|
+
await until(async () => ((await row('IDPF-07')) || {}).status === 'cancelled');
|
|
106
|
+
ok('M7 Cancel records the reason; the row leaves the list', /^Not worth it now/.test(((await row('IDPF-07')).stopped || {}).reason || '') && await until(async () => !(await rows()).includes('IDPF-07')));
|
|
107
|
+
s0 = await seq();
|
|
108
|
+
await page.click('[data-ibb="show-cancelled"]');
|
|
109
|
+
await after(s0);
|
|
110
|
+
await menuFor('IDPF-07', '[data-il-reopen]');
|
|
111
|
+
await until(async () => ((await row('IDPF-07')) || {}).status === 'on track');
|
|
112
|
+
await toastHas(/IDPF-07 is reopened/); // the demo store changes before the answer reaches the page, so wait for the toast too
|
|
113
|
+
ok('M8 Reopen from the row menu makes it active again', ((await row('IDPF-07')) || {}).status === 'on track' && /IDPF-07 is reopened/.test(await toast()), JSON.stringify([((await row('IDPF-07')) || {}).status, await toast()]));
|
|
114
|
+
|
|
115
|
+
await open('board:idpf:initiatives:q=IDPF-37&view=all');
|
|
116
|
+
await ready();
|
|
117
|
+
await menuFor('IDPF-37', '[data-il-archive]');
|
|
118
|
+
await until(() => st(() => !!document.querySelector('[data-il-do="archive"]')));
|
|
119
|
+
await page.click('[data-il-do="archive"]');
|
|
120
|
+
await until(async () => !!((await row('IDPF-37')) || {}).archived);
|
|
121
|
+
ok('M9 Archive takes it out of the list, with the reason', (((await row('IDPF-37')).archived) || {}).reason === 'Not related to this board' && await until(async () => !(await rows()).includes('IDPF-37')));
|
|
122
|
+
s0 = await seq();
|
|
123
|
+
await page.fill('[data-ibb="q"]', '');
|
|
124
|
+
await page.keyboard.press('Enter');
|
|
125
|
+
await after(s0);
|
|
126
|
+
s0 = await seq();
|
|
127
|
+
await page.click('[data-ibb="show-archive"]');
|
|
128
|
+
await after(s0);
|
|
129
|
+
const arch = await rows();
|
|
130
|
+
ok('M10 "N archived" opens the archive: the archived and the merged, each with Restore or Undo merge', ['IDPF-37', 'IDPF-154', 'IDPF-155'].every((x) => arch.includes(x))
|
|
131
|
+
&& await st(() => /Undo merge/.test(document.querySelector('[data-ibb-row="IDPF-155"]').textContent) && /Restore/.test(document.querySelector('[data-ibb-row="IDPF-37"]').textContent)), arch.join());
|
|
132
|
+
await page.click('[data-il-restore="IDPF-37"]');
|
|
133
|
+
await until(async () => !((await row('IDPF-37')) || {}).archived);
|
|
134
|
+
ok('M11 Restore brings it back as it was', !(await row('IDPF-37')).archived && await until(async () => !(await rows()).includes('IDPF-37')));
|
|
135
|
+
|
|
136
|
+
/* merge: the duplicate hint, the picked task moves, Undo merge moves it back */
|
|
137
|
+
await open('board:idpf:initiatives:q=IDPF-151');
|
|
138
|
+
await ready();
|
|
139
|
+
ok('M12 a likely duplicate shows the hint', /Looks like a duplicate of IDPF-12/.test(await st(() => document.querySelector('[data-ibb-row="IDPF-151"]').textContent)));
|
|
140
|
+
await page.click('[data-il-merge="IDPF-151"]');
|
|
141
|
+
await until(() => st(() => !!document.querySelector('[data-il-do="merge"]')));
|
|
142
|
+
ok('M13 Merge picks the most alike first and lists what moves', await st(() => document.querySelector('[data-il-set="pick"][aria-pressed="true"]').dataset.v === 'IDPF-12' && document.querySelector('[data-il-move="tasks"]').checked) && /Merge into IDPF-12/.test(await sheetText()));
|
|
143
|
+
await page.click('[data-il-do="merge"]');
|
|
144
|
+
await until(async () => !!((await row('IDPF-151')) || {}).archived);
|
|
145
|
+
const moved = await store(() => window.__c3boards.tasks.find((t) => t.id === 'task-demo-l1').initiative);
|
|
146
|
+
ok('M14 the merge archives the duplicate as Merged into IDPF-12 and moves its task', ((await row('IDPF-151')).archived || {}).into === 'IDPF-12' && moved === 'IDPF-12', moved);
|
|
147
|
+
await toastHas(/IDPF-151 merged into IDPF-12/);
|
|
148
|
+
await undo();
|
|
149
|
+
await until(async () => !((await row('IDPF-151')) || {}).archived);
|
|
150
|
+
ok('M15 Undo merge moves the task back and restores the duplicate', await store(() => window.__c3boards.tasks.find((t) => t.id === 'task-demo-l1').initiative) === 'IDPF-151');
|
|
151
|
+
|
|
152
|
+
/* ── P · the initiative page ── */
|
|
153
|
+
await open('ini:idpf:IDPF-152:overview');
|
|
154
|
+
const ban = await st(() => (document.querySelector('[data-il="banner"]') || {}).textContent || '');
|
|
155
|
+
ok('P1 an on-hold initiative: the banner with the reason and the check-back day, Resume and Cancel', /On hold since/.test(ban) && /Q1 budget/.test(ban) && /Check back/.test(ban) && /Resume/.test(ban), ban);
|
|
156
|
+
await page.click('[data-il-menu="IDPF-152"]');
|
|
157
|
+
const pm = await st(() => [...document.querySelectorAll('[data-ini="menu"] .ini-mlbl')].map((n) => n.textContent).join(','));
|
|
158
|
+
ok('P2 the page "…" menu holds Status and Tidy up', pm === 'Status,Tidy up' && await st(() => !!document.querySelector('[data-ini="menu"] [data-il-resume]')), pm);
|
|
159
|
+
await page.keyboard.press('Escape');
|
|
160
|
+
await page.click('[data-il="banner"] [data-il-resume]');
|
|
161
|
+
await until(async () => ((await row('IDPF-152')) || {}).status === 'on track');
|
|
162
|
+
ok('P3 Resume from the banner: active again, the banner is gone', await until(() => st(() => !document.querySelector('[data-il="banner"]'))));
|
|
163
|
+
await open('ini:idpf:IDPF-155:overview');
|
|
164
|
+
const mban = await st(() => (document.querySelector('[data-il="banner"]') || {}).textContent || '');
|
|
165
|
+
ok('P4 a merged initiative: the banner names where it went, with Undo merge', /Merged into IDPF-07/.test(mban) && /Undo merge/.test(mban), mban);
|
|
166
|
+
await H.finish();
|
|
167
|
+
})();
|
|
@@ -21,10 +21,10 @@ const { harness } = require('./c3-ini-harness');
|
|
|
21
21
|
const ids = await until(() => st(() => [...document.querySelectorAll('[data-ini="boards"] [data-ini-board]')].map((r) => r.dataset.iniBoard).join(',')));
|
|
22
22
|
ok('N1 the Portfolio lists the 5 boards in order', ids === 'idpf,pay,growth,office,hiring', ids);
|
|
23
23
|
const pf = await st(() => document.querySelector('[data-ini="portfolio"]').closest('[data-ini="page"]').textContent);
|
|
24
|
-
ok('N2 the header counts boards, initiatives and new asks; the sentence names the boards off track', /5 boards/.test(pf) && /
|
|
24
|
+
ok('N2 the header counts boards, initiatives and new asks; the sentence names the boards off track', /5 boards/.test(pf) && /315 initiatives/.test(pf) && /4 new asks/.test(pf) && /1 board is off track/.test(pf), pf.slice(0, 300));
|
|
25
25
|
const idpfRow = await st(() => document.querySelector('[data-ini-board="idpf"]').textContent);
|
|
26
26
|
ok('N3 a board row shows health, owner, Build and deliver with its Jira key, the four categories and the next release',
|
|
27
|
-
/Needs attention/.test(idpfRow) && /Build and deliver/.test(idpfRow) && /Jira · IDPF/.test(idpfRow) && /Discover\s*
|
|
27
|
+
/Needs attention/.test(idpfRow) && /Build and deliver/.test(idpfRow) && /Jira · IDPF/.test(idpfRow) && /Discover\s*29/.test(idpfRow) && /Learn\s*31/.test(idpfRow) && /IDPF 3\.1/.test(idpfRow), idpfRow.slice(0, 300));
|
|
28
28
|
const offRow = await st(() => document.querySelector('[data-ini-board="office"]').textContent);
|
|
29
29
|
ok('N4 a board with Build and deliver off says PMOS tasks only', /PMOS tasks only/.test(offRow) && !/Jira/.test(offRow), offRow.slice(0, 200));
|
|
30
30
|
const tl = await until(() => st(() => { const t = document.querySelector('[data-ini="pf-timeline"] .ini-tl'); return t && t.querySelectorAll('[data-tlb]').length; }));
|
|
@@ -43,11 +43,11 @@ const { harness } = require('./c3-ini-harness');
|
|
|
43
43
|
const glance = await until(() => st(() => document.querySelectorAll('[data-ini="glance"] [data-ini-row]').length));
|
|
44
44
|
ok('B4 Summary lists initiatives at a glance with their own stage dots', glance > 0 && await st(() => !!document.querySelector('[data-ini="glance"] .ini-steps')), String(glance));
|
|
45
45
|
await page.click('[data-ini-catgo="Do"]');
|
|
46
|
-
await until(() => st(() => document.querySelector('[data-ibb-
|
|
47
|
-
ok('B5 a category bar opens the Initiatives tab filtered to that category', /board:idpf:initiatives:/.test(await st(() => new URLSearchParams(location.search).get('init'))) && await st(() => document.querySelector('[data-ibb-
|
|
46
|
+
await until(() => st(() => /Do/.test((document.querySelector('[data-ibb-unset="cat"]') || {}).textContent || '')));
|
|
47
|
+
ok('B5 a category bar opens the Initiatives tab filtered to that category', /board:idpf:initiatives:/.test(await st(() => new URLSearchParams(location.search).get('init'))) && await st(() => /Do/.test((document.querySelector('[data-ibb-unset="cat"]') || {}).textContent || '')));
|
|
48
48
|
await page.reload({ waitUntil: 'load' });
|
|
49
|
-
await until(() => st(() => document.querySelector('[data-ibb-
|
|
50
|
-
ok('B6 a reload comes back to the same tab and filter', await st(() => document.querySelector('[data-ini-tab="initiatives"]').getAttribute('aria-pressed') === 'true' && document.querySelector('[data-ibb-
|
|
49
|
+
await until(() => st(() => /Do/.test((document.querySelector('[data-ibb-unset="cat"]') || {}).textContent || '')));
|
|
50
|
+
ok('B6 a reload comes back to the same tab and filter', await st(() => document.querySelector('[data-ini-tab="initiatives"]').getAttribute('aria-pressed') === 'true' && /Do/.test((document.querySelector('[data-ibb-unset="cat"]') || {}).textContent || '')));
|
|
51
51
|
|
|
52
52
|
await open('board:office:summary');
|
|
53
53
|
const offTabs = await st(() => [...document.querySelectorAll('[data-ini="board-tabs"] [data-ini-tab]')].map((b) => b.dataset.iniTab).join(','));
|
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* tell text sent from the ask bar records a capture and clears the bar
|
|
10
10
|
* brief the morning card counts every row the desktop Briefing counts; See all lists them; a row opens its details
|
|
11
11
|
* meeting a Later today row and the morning card open the meeting's sheet with its brief (GET /api/meeting-focus)
|
|
12
|
-
*
|
|
12
|
+
* count the "N waiting" count in the header opens the whole list
|
|
13
|
+
* record the red button opens focus for a talk that is not on the calendar (no topics, What's being said, the Note sheet, Ask PMOS quietly);
|
|
14
|
+
* Back to your day keeps recording and shows the notes being taken; the bar's Focus brings focus back; Stop in focus asks, then stops
|
|
13
15
|
* debrief 11:25 shows the decisions and actions of the recorded Checkout sync; a meeting with no recording asks you to tell PMOS
|
|
14
16
|
* nodesk there is no Full console; a phone that chose it before 2026-10-13 shows the companion again
|
|
15
|
-
* focus
|
|
17
|
+
* focus Record on a meeting opens focus in one column: the clock, the now line, Topics, Captured, Note and Your slides as sheets,
|
|
18
|
+
* a context chip in a sheet, Ask and Stop at the foot, nothing wider than the screen; Back to your day closes it and recording goes on
|
|
16
19
|
* narrow a long title and a long list of people never widen the companion past the screen
|
|
17
20
|
* desktop at 1280 wide no companion is drawn
|
|
18
21
|
* 620 the companion shows up to 640 wide, where the phone styling starts
|
|
@@ -81,6 +84,14 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
81
84
|
ok('done: Done records a write to /api/pm-items/done', await waitFor(page, (n) => ((window.__c3demoWrites || []).filter((w) => String(w.path || w.url || '').startsWith('/api/pm-items/done')).length) > n, before, 5000));
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/* ── the count in the header opens the whole list ── */
|
|
88
|
+
ok('count: the header count is a button', await waitFor(page, () => document.querySelector('[data-c3ph-count]')?.tagName === 'BUTTON', null, 8000));
|
|
89
|
+
if (await page.$('[data-c3ph-count]')) {
|
|
90
|
+
await page.click('[data-c3ph-count]');
|
|
91
|
+
ok('count: it opens the list of what waits', await waitFor(page, () => document.querySelectorAll('[data-c3ph-all] [data-c3ph-all-row]').length > 0, null, 5000));
|
|
92
|
+
await page.keyboard.press('Escape');
|
|
93
|
+
await waitFor(page, () => !document.querySelector('[data-c3ph-all]'), null, 3000);
|
|
94
|
+
}
|
|
84
95
|
/* ── the brief: everything the desktop Briefing counts waits here, and each row opens ── */
|
|
85
96
|
ok('brief: the morning card counts what waits on you', await waitFor(page, () => /wait on you|waits on you/.test(document.querySelector('[data-c3ph-moment="morning"]')?.textContent || ''), null, 8000));
|
|
86
97
|
ok('brief: the briefing needs are in the waiting list', await waitFor(page, () => !!document.querySelector('[data-c3ph-all-open]'), null, 8000));
|
|
@@ -112,14 +123,37 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
112
123
|
ok('tell: the text is filed as a capture', await waitFor(page, () => (window.__c3demoWrites || []).some((w) => w.path === '/api/capture' && w.body && /beta date/.test(w.body.text)), null, 5000));
|
|
113
124
|
ok('tell: the bar is cleared', (await page.inputValue('[data-c3ph-input]')) === '');
|
|
114
125
|
|
|
115
|
-
/* ── record ── */
|
|
126
|
+
/* ── record: a talk that is not on the calendar opens focus by its note ── */
|
|
116
127
|
await page.click('[data-c3ph-record]');
|
|
117
|
-
ok('record:
|
|
128
|
+
ok('record: recording opens focus', await waitFor(page, () => document.querySelector('[data-c3focus][data-fc-phone]')?.dataset.fcState === 'ok', null, 8000));
|
|
129
|
+
ok('record: a talk has no topics, no stage, and what is being said', await page.evaluate(() => {
|
|
130
|
+
const f = document.querySelector('[data-c3focus]');
|
|
131
|
+
return /Conversation/.test(f.querySelector('h1').textContent) && /not on the calendar/.test(f.textContent) && !f.querySelector('[data-fc="topics"]') && !f.querySelector('[data-fc="material"]')
|
|
132
|
+
&& !!f.querySelector('[data-fc="captured"]') && !!f.querySelector('[data-fc="gist"]') && !!f.querySelector('[data-fc="note-card"]');
|
|
133
|
+
}));
|
|
134
|
+
ok('record: the foot has Ask PMOS quietly and Stop', await page.evaluate(() => !!document.querySelector('[data-fc="phone-foot"] [data-fc="ask-input"]') && !!document.querySelector('[data-fc="phone-foot"] [data-fc="stop"]')));
|
|
135
|
+
ok('record: focus is no wider than the phone', await page.evaluate(() => { const f = document.querySelector('[data-c3focus]'); return f.scrollWidth <= innerWidth + 1 && [...f.querySelectorAll('*')].every((n) => n.getBoundingClientRect().right <= innerWidth + 1 || n.closest('.fc-cxs')); }));
|
|
136
|
+
await page.click('[data-c3focus] [data-fc="note"]');
|
|
137
|
+
ok('record: Note opens the live note in a sheet, focus stays', await waitFor(page, () => !!document.querySelector('[data-shell="sheet"] [data-fc="note-sheet"]') && !!document.querySelector('[data-c3focus]'), null, 5000));
|
|
138
|
+
await page.click('[data-shell="sheet"] [data-fc="note-tab"][data-tab="said"]');
|
|
139
|
+
ok('record: the note sheet shows what was said', await waitFor(page, () => /SSO copy/.test(document.querySelector('[data-fc="note-said"]')?.textContent || ''), null, 5000));
|
|
140
|
+
await page.keyboard.press('Escape');
|
|
141
|
+
await waitFor(page, () => !document.querySelector('[data-shell="sheet"] [data-fc="note-sheet"]'), null, 3000);
|
|
142
|
+
await page.fill('[data-c3focus] [data-fc="ask-input"]', 'What did Budi ask?');
|
|
143
|
+
await page.press('[data-c3focus] [data-fc="ask-input"]', 'Enter');
|
|
144
|
+
ok('record: Ask PMOS quietly adds the question to the conversation', await waitFor(page, () => /What did Budi ask/.test(document.querySelector('[data-c3focus] [data-fc="stream"]')?.textContent || ''), null, 5000));
|
|
145
|
+
await page.click('[data-c3focus] [data-fc="leave"]');
|
|
146
|
+
ok('record: Back to your day closes focus and keeps recording', await waitFor(page, () => !document.querySelector('[data-c3focus]') && !!document.querySelector('[data-c3ph-recbar] [data-c3ph-stop]'), null, 5000));
|
|
147
|
+
ok('record: the bar has Focus', !!(await page.$('[data-c3ph-recbar] [data-c3ph-focus-bar]')));
|
|
118
148
|
ok('record: the ask bar gives way while recording', !(await page.evaluate(() => !!document.querySelector('[data-c3ph-foot] [data-c3ph-input]'))));
|
|
119
149
|
ok('record: the notes being taken show while recording', await waitFor(page, () => { const c = document.querySelector('[data-c3ph-moment="live"]'); return !!c && c.dataset.c3phLive === 'ready' && !!c.querySelector('[data-c3ph-heard]') && !!c.querySelector('[data-c3ph-live-list="decisions"]'); }, null, 10000),
|
|
120
150
|
await page.evaluate(() => document.querySelector('[data-c3ph-moment]')?.outerHTML.slice(0, 300)));
|
|
121
|
-
|
|
122
|
-
ok('record:
|
|
151
|
+
await page.click('[data-c3ph-recbar] [data-c3ph-focus-bar]');
|
|
152
|
+
ok('record: Focus in the bar brings focus back', await waitFor(page, () => document.querySelector('[data-c3focus][data-fc-phone]')?.dataset.fcState === 'ok', null, 5000));
|
|
153
|
+
await page.click('[data-c3focus] [data-fc="stop"]');
|
|
154
|
+
ok('record: Stop in focus asks first', await waitFor(page, () => !!document.querySelector('[data-shell="sheet"] [data-fc="stop-ok"]'), null, 5000));
|
|
155
|
+
await page.click('[data-shell="sheet"] [data-fc="stop-ok"]');
|
|
156
|
+
ok('record: Stop recording closes focus and brings the bar back', await waitFor(page, () => !document.querySelector('[data-c3focus]') && !!document.querySelector('[data-c3ph-foot] [data-c3ph-input]') && !document.querySelector('[data-c3ph-recbar]'), null, 8000));
|
|
123
157
|
await page.close();
|
|
124
158
|
|
|
125
159
|
/* ── moments by the clock ── */
|
|
@@ -131,16 +165,33 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
131
165
|
ok('moments: the meeting card offers to record it', await page.evaluate(() => !!document.querySelector('[data-c3ph-moment] [data-c3ph-rec-meeting]') && !!document.querySelector('[data-c3ph-moment] [data-c3ph-norec]')));
|
|
132
166
|
ok('moments: button icons in the moment card stay small', await page.evaluate(() => [...document.querySelectorAll('[data-c3ph-moment] .pmc-btn svg')].every((s) => s.getBoundingClientRect().width <= 20)));
|
|
133
167
|
await page.click('[data-c3ph-rec-meeting]');
|
|
134
|
-
ok('
|
|
135
|
-
ok('
|
|
136
|
-
|
|
137
|
-
ok('focus: the
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
168
|
+
ok('focus: Record on the meeting opens focus', await waitFor(page, () => document.querySelector('[data-c3focus][data-fc-phone]')?.dataset.fcState === 'ok', null, 8000));
|
|
169
|
+
ok('focus: it is the full page, not the compact one', await page.evaluate(() => !document.querySelector('[data-c3focus][data-fc-pop]') && !document.querySelector('[data-c3focus] [data-fc="pop-open"]')));
|
|
170
|
+
ok('focus: the head has Back to your day, the clock and the bar of the talk', await page.evaluate(() => { const h = document.querySelector('[data-fc="phone-head"]'); return !!h.querySelector('[data-fc="leave"]') && /\d:\d\d/.test(h.querySelector('[data-fc="big"]')?.textContent || '') && !!h.querySelector('.fc-track'); }));
|
|
171
|
+
ok('focus: the now line and the chips Topics, Captured, Note and Your slides', await page.evaluate(() => { const n = document.querySelector('[data-c3focus] [data-fc="now-line"]'); return !!n.querySelector('[data-fc="topic"]') && ['topics', 'captured', 'note', 'material'].every((k) => !!n.querySelector('[data-fc="' + k + '"]')) && /Your slides/.test(n.textContent); }));
|
|
172
|
+
ok('focus: no stage on the page itself, no desktop Leave focus or End meeting', await page.evaluate(() => !document.querySelector('[data-c3focus] [data-fc="stage"]') && !/Leave focus|End meeting/.test(document.querySelector('[data-c3focus]').textContent)));
|
|
173
|
+
ok('focus: nothing is wider than the phone', await page.evaluate(() => { const f = document.querySelector('[data-c3focus]'); return f.scrollWidth <= innerWidth + 1 && [...f.querySelectorAll('*')].every((n) => n.getBoundingClientRect().right <= innerWidth + 1 || n.closest('.fc-cxs')); }),
|
|
174
|
+
await page.evaluate(() => [...document.querySelectorAll('[data-c3focus] *')].filter((n) => n.getBoundingClientRect().right > innerWidth + 1 && !n.closest('.fc-cxs')).slice(0, 3).map((n) => n.className + ':' + Math.round(n.getBoundingClientRect().right)).join(' ')));
|
|
175
|
+
ok('focus: the address does not change', await page.evaluate(() => !new URL(location.href).searchParams.has('focus')));
|
|
176
|
+
await page.click('[data-c3focus] [data-fc="topics"]');
|
|
177
|
+
ok('focus: Topics opens as a sheet over focus', await waitFor(page, () => !!document.querySelector('[data-shell="sheet"] [data-fc="topics-sheet"]') && !!document.querySelector('[data-c3focus]'), null, 5000));
|
|
178
|
+
await page.keyboard.press('Escape');
|
|
179
|
+
await waitFor(page, () => !document.querySelector('[data-shell="sheet"] [data-fc="topics-sheet"]'), null, 3000);
|
|
180
|
+
await page.click('[data-c3focus] [data-fc="material"]');
|
|
181
|
+
ok('focus: Your slides opens the material in a sheet, with Open', await waitFor(page, () => !!document.querySelector('[data-shell="sheet"] [data-fc="stage"] [data-fc="open-link"]'), null, 5000));
|
|
182
|
+
ok('focus: the material sheet is no wider than the phone', await page.evaluate(() => [...document.querySelectorAll('[data-shell="sheet"] [data-fc="stage"] *')].every((n) => n.getBoundingClientRect().right <= innerWidth + 1)));
|
|
183
|
+
await page.keyboard.press('Escape');
|
|
184
|
+
await waitFor(page, () => !document.querySelector('[data-shell="sheet"] [data-fc="stage"]'), null, 3000);
|
|
185
|
+
await page.click('[data-c3focus] [data-fc="chip"]');
|
|
186
|
+
ok('focus: a context chip opens its detail in a sheet and focus stays', await waitFor(page, () => !!document.querySelector('[data-shell="sheet"] [data-fc="chip-sheet"]') && !!document.querySelector('[data-c3focus]'), null, 5000));
|
|
187
|
+
await page.keyboard.press('Escape');
|
|
188
|
+
await page.click('[data-c3focus] [data-fc="note"]');
|
|
189
|
+
ok('focus: Note shows what the live note picked out', await waitFor(page, () => !!document.querySelector('[data-shell="sheet"] [data-fc-part="decisions"]'), null, 5000));
|
|
190
|
+
await page.keyboard.press('Escape');
|
|
191
|
+
await waitFor(page, () => !document.querySelector('[data-shell="sheet"] [data-fc="note-sheet"]'), null, 3000);
|
|
192
|
+
await page.click('[data-c3focus] [data-fc="leave"]');
|
|
193
|
+
ok('focus: Back to your day closes it, the companion shows and it keeps recording', await waitFor(page, () => !document.querySelector('[data-c3focus]') && !!document.querySelector('[data-c3ph-moment]') && /Checkout sync/.test(document.querySelector('[data-c3ph-recbar]')?.textContent || ''), null, 5000));
|
|
194
|
+
ok('focus: while recording, the card has Open focus', await page.evaluate(() => /Open focus/.test(document.querySelector('[data-c3ph-moment] [data-c3ph-focus]')?.textContent || '')));
|
|
144
195
|
|
|
145
196
|
if (await page.$('[data-c3ph-stop]')) await page.click('[data-c3ph-stop]');
|
|
146
197
|
|
|
@@ -62,7 +62,8 @@ const shown = (s) => { const n = document.querySelector(s); return !!n && getCom
|
|
|
62
62
|
ok('B0 the portfolio lists a board to open', opened);
|
|
63
63
|
if (opened) {
|
|
64
64
|
await page.evaluate(() => document.querySelector('[data-ini-open-board]').click());
|
|
65
|
-
const moved = await waitFor(page, () => { const t = document.querySelector('[data-ini-view="board"] [data-ini="board-tabs"]'), c = document.querySelector('[data-ini-view="board"] [data-ini="crumbs"]'); return t && c && t.parentElement === c.parentElement; });
|
|
65
|
+
const moved = await waitFor(page, () => { const t = document.querySelector('[data-ini-view="board"] [data-ini="board-tabs"]'), c = document.querySelector('[data-ini-view="board"] [data-ini="crumbs"]'); return t && c && t.parentElement === c.parentElement; }, null, 90000);
|
|
66
|
+
// live mode: the board waits for the first read of this repository, which takes 20 s here and more on a CI runner
|
|
66
67
|
ok('B1 the board tabs sit on the breadcrumb row', moved);
|
|
67
68
|
await page.waitForTimeout(300);
|
|
68
69
|
const g = await page.evaluate((shownSrc) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Initiatives on the console: Portfolio, boards and workflows"
|
|
4
4
|
description: "How to use the Initiatives tab of the console page (/): the Portfolio of boards, a board and its nine tabs, an initiative with its own workflow and stage records, and the Build and deliver setting."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-13
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Initiatives on the console: Portfolio, boards and workflows
|
|
@@ -27,7 +27,7 @@ The tabs are **Summary**, **Initiatives**, **Triage**, **Backlog**, **Board**, *
|
|
|
27
27
|
On a screen wider than 600 pixels, the breadcrumb, the board status, the tabs and the Board settings gear are on one row. **Backlog**, **Releases**, **Docs** and **Sprints** are behind **More**. Press **More** to show them. When one of them is the open tab, it stays shown.
|
|
28
28
|
|
|
29
29
|
- **Summary** shows the initiatives at a glance and one bar per category. At a glance lists up to six open initiatives. Initiatives with work, and Now initiatives that are not on track, come first. The others follow by end date. Press a bar to open the Initiatives tab with that category.
|
|
30
|
-
- **Initiatives** lists the initiatives of the board, 25 on each page.
|
|
30
|
+
- **Initiatives** lists the initiatives of the board, 25 on each page. See [Initiatives: status, hold, cancel, merge and archive](#initiatives-status-hold-cancel-merge-and-archive).
|
|
31
31
|
- **Triage** lists the new asks. Add an ask, then open it to see its draft. Press **Accept** to start an initiative. An initiative needs an anchor (a KR or a health budget). On a board, **New initiative** lists the anchors that `planning/okrs/` declares. Pick one from the list. Press **Ask for more** to ask a question, or **Decline** and give a reason.
|
|
32
32
|
- **Backlog**, **Board** and **Timeline** show the epics and items. Jira items and PMOS tasks are mixed. The **Initiative** and **Source** filters are the same on the three tabs.
|
|
33
33
|
- In Backlog, add a PMOS task to an initiative.
|
|
@@ -37,6 +37,41 @@ On a screen wider than 600 pixels, the breadcrumb, the board status, the tabs an
|
|
|
37
37
|
- **Docs** lists the documents of the board by initiative.
|
|
38
38
|
- **Sprints** shows the Jira sprints and the sprint burndown. They are read-only.
|
|
39
39
|
|
|
40
|
+
### Initiatives: status, hold, cancel, merge and archive
|
|
41
|
+
|
|
42
|
+
The Initiatives tab has one row of controls: the search box, the view menu, the filter icon, the sort icon and **New initiative**.
|
|
43
|
+
|
|
44
|
+
- **The view menu** shows the current view: Now and next, Mine, At risk, No owner or All. Now and next, Mine and No owner show only active initiatives.
|
|
45
|
+
- **The filter icon** opens Status, Stage, Category, Owner and Horizon. The Status list has three groups: Active (On track, At risk, Late), Paused (On hold) and Closed (Done, Cancelled). The badge on the icon counts the filters that are on. Each filter that is on shows as a chip above the list. Press a chip to remove that filter, or press **Clear**.
|
|
46
|
+
- **The sort icon** opens the sort keys and the order.
|
|
47
|
+
|
|
48
|
+
Each initiative has a **stage** and a **status**. The stage is the step in its own workflow. The status is one of these:
|
|
49
|
+
|
|
50
|
+
| Status | Meaning |
|
|
51
|
+
|---|---|
|
|
52
|
+
| On track, At risk, Late | The initiative is active. |
|
|
53
|
+
| On hold | You paused it. It keeps its stage, its work and its history. It never turns Late while it is paused. |
|
|
54
|
+
| Done | It reached the end of its workflow, or it was accepted or closed. |
|
|
55
|
+
| Cancelled | You stopped it. It is counted as cancelled, not done. |
|
|
56
|
+
|
|
57
|
+
A cancelled initiative is hidden from every view. The list footer says how many are hidden, for example "1 cancelled hidden". Press it to see them, or pick Cancelled in the Status filter.
|
|
58
|
+
|
|
59
|
+
Press **…** on a row to open its menu:
|
|
60
|
+
|
|
61
|
+
- **Move to a stage**: the stages of its own workflow.
|
|
62
|
+
- **Put on hold…**: give a reason and pick when to check back (in 2 weeks, in a month, next quarter, or no date). The check-back date shows on the row and on the initiative page. **Resume** makes it active again at the same stage.
|
|
63
|
+
- **Cancel…**: pick a reason (No longer needed, Replaced by another initiative, Not worth it now, Hit a kill condition, Other) and add a note if you want. **Reopen** makes it active again at the same stage.
|
|
64
|
+
- **Merge into another…**: for a duplicate. Pick the initiative to keep. The most alike ones are first. Pick what moves: the Jira epic links and the PMOS tasks. The duplicate goes to the Archive as "Merged into" the one you keep. Its documents, asks and notes stay with it. Nothing changes in Jira.
|
|
65
|
+
- **Archive…**: for an initiative that does not belong. It leaves every list, count and timeline of the board. Nothing is deleted. If it belongs on another board, the sheet can move it there instead.
|
|
66
|
+
|
|
67
|
+
When two active initiatives on the same anchor have almost the same title, the less advanced one shows "Looks like a duplicate of" the other. Press it to open Merge. In the merge sheet, press **Not a duplicate** to stop the hint.
|
|
68
|
+
|
|
69
|
+
Each action has **Undo** on its toast. The footer link "N archived" opens the archive. Press **Restore** on an archived initiative to put it back as it was. Press **Undo merge** on a merged one: the epic links and tasks that moved go back.
|
|
70
|
+
|
|
71
|
+
The initiative page shows a banner when the initiative is on hold, cancelled or archived, with its reason, its date and **Resume**, **Reopen**, **Restore** or **Undo merge**. Its **…** menu has the same status and tidy-up actions as the row menu.
|
|
72
|
+
|
|
73
|
+
Hold, cancel, resume and reopen are lines in `lifecycle.jsonl`. Archive and merge are lines in the board ledger, `boards.jsonl`. No initiative record is changed.
|
|
74
|
+
|
|
40
75
|
### Board settings
|
|
41
76
|
|
|
42
77
|
Press the gear in the board header to open the board settings.
|