create-pmos 0.181.0 → 0.183.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-phone-test.js +63 -15
- package/workspace/c3-quiet-test.js +1 -1
- package/workspace/guide/phone-companion.md +17 -7
- package/workspace/lib/c3-full-console.js +4 -4
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-phone.js +16 -0
- package/workspace/public/pm/c3-focus.js +8 -6
- package/workspace/public/pm/c3-phone-detail.js +83 -0
- package/workspace/public/pm/c3-phone.css +9 -7
- package/workspace/public/pm/c3-phone.js +105 -30
- package/workspace/public/pm/c3-shell.js +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.183.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-13T03:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T03:53:55Z"}
|
|
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-13T03:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T03:53:55Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-13T03:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T03:53:55Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
/* c3-phone-test.js — the phone companion (initiative c3-phone-companion; PRD done-criteria 1 to 6 and 8).
|
|
3
|
+
/* c3-phone-test.js — the phone companion (initiative c3-phone-companion; PRD done-criteria 1 to 6 and 8): c3-phone.js, its sheets c3-phone-detail.js and its demo notes c3-demo-phone.js.
|
|
4
4
|
* A real server and a real Chromium drive /c3.html in demo mode (clock frozen at Thu 8 Oct 2026 08:12; ?at=HH:MM moves the companion's clock).
|
|
5
5
|
*
|
|
6
6
|
* phone at 400 wide the companion shows and the desktop grid does not
|
|
7
7
|
* moments 08:12 morning; 10:10 before Checkout sync; 10:45 in the meeting with the Record offer
|
|
8
8
|
* done Done on the waiting item records a write to /api/pm-items/done
|
|
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
|
+
* meeting a Later today row and the morning card open the meeting's sheet with its brief (GET /api/meeting-focus)
|
|
12
|
+
* record the red button shows the recording line and the notes being taken (just heard, decisions); Stop brings the bar back
|
|
11
13
|
* debrief 11:25 shows the decisions and actions of the recorded Checkout sync; a meeting with no recording asks you to tell PMOS
|
|
12
|
-
*
|
|
14
|
+
* nodesk there is no Full console; a phone that chose it before 2026-10-13 shows the companion again
|
|
15
|
+
* focus the meeting companion opens over the companion and Back to your day closes it
|
|
16
|
+
* narrow a long title and a long list of people never widen the companion past the screen
|
|
13
17
|
* desktop at 1280 wide no companion is drawn
|
|
18
|
+
* 620 the companion shows up to 640 wide, where the phone styling starts
|
|
14
19
|
* No script error may be raised.
|
|
15
20
|
*/
|
|
16
21
|
const path = require('path');
|
|
@@ -76,6 +81,31 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
76
81
|
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));
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
/* ── the brief: everything the desktop Briefing counts waits here, and each row opens ── */
|
|
85
|
+
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
|
+
ok('brief: the briefing needs are in the waiting list', await waitFor(page, () => !!document.querySelector('[data-c3ph-all-open]'), null, 8000));
|
|
87
|
+
if (await page.$('[data-c3ph-all-open]')) {
|
|
88
|
+
await page.click('[data-c3ph-all-open]');
|
|
89
|
+
const n = await waitFor(page, () => document.querySelectorAll('[data-c3ph-all] [data-c3ph-all-row]').length > 1, null, 5000);
|
|
90
|
+
ok('brief: See all lists every row that waits', n);
|
|
91
|
+
const nonPm = await page.evaluate(() => [...document.querySelectorAll('[data-c3ph-all-row]')].length);
|
|
92
|
+
await page.click('[data-c3ph-all-row] .pmc-row__open');
|
|
93
|
+
ok('brief: a row opens its details', await waitFor(page, () => !!document.querySelector('[data-c3ph-need]'), null, 5000), nonPm);
|
|
94
|
+
await page.keyboard.press('Escape');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ── a meeting opens its brief ── */
|
|
98
|
+
const laterRow = await waitFor(page, () => !!document.querySelector('[data-c3ph-later-row] .pmc-row__open'), null, 8000);
|
|
99
|
+
ok('meeting: the Later today rows can be opened', laterRow);
|
|
100
|
+
if (laterRow) {
|
|
101
|
+
await page.click('[data-c3ph-later-row] .pmc-row__open');
|
|
102
|
+
ok('meeting: the sheet shows the meeting', await waitFor(page, () => !!document.querySelector('[data-c3ph-meeting]'), null, 5000));
|
|
103
|
+
ok('meeting: the sheet reads the brief of the meeting', await waitFor(page, () => document.querySelector('[data-c3ph-brief]')?.dataset.c3phBrief === 'ready' && !!document.querySelector('[data-c3ph-sect="gist"]'), null, 8000),
|
|
104
|
+
await page.evaluate(() => document.querySelector('[data-c3ph-brief]')?.outerHTML.slice(0, 300)));
|
|
105
|
+
await page.keyboard.press('Escape');
|
|
106
|
+
}
|
|
107
|
+
ok('meeting: the morning card opens the brief of the next meeting', !!(await page.$('[data-c3ph-moment="morning"] [data-c3ph-meeting-open]')));
|
|
108
|
+
|
|
79
109
|
/* ── tell PMOS ── */
|
|
80
110
|
await page.fill('[data-c3ph-input]', 'Ask Maya about the beta date');
|
|
81
111
|
await page.press('[data-c3ph-input]', 'Enter');
|
|
@@ -86,6 +116,8 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
86
116
|
await page.click('[data-c3ph-record]');
|
|
87
117
|
ok('record: the recording line shows with Stop', await waitFor(page, () => !!document.querySelector('[data-c3ph-recbar] [data-c3ph-stop]'), null, 8000));
|
|
88
118
|
ok('record: the ask bar gives way while recording', !(await page.evaluate(() => !!document.querySelector('[data-c3ph-foot] [data-c3ph-input]'))));
|
|
119
|
+
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
|
+
await page.evaluate(() => document.querySelector('[data-c3ph-moment]')?.outerHTML.slice(0, 300)));
|
|
89
121
|
if (await page.$('[data-c3ph-stop]')) await page.click('[data-c3ph-stop]');
|
|
90
122
|
ok('record: Stop brings the bar back', await waitFor(page, () => !!document.querySelector('[data-c3ph-foot] [data-c3ph-input]') && !document.querySelector('[data-c3ph-recbar]'), null, 8000));
|
|
91
123
|
await page.close();
|
|
@@ -101,6 +133,15 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
101
133
|
await page.click('[data-c3ph-rec-meeting]');
|
|
102
134
|
ok('moments: Record starts recording Checkout sync', await waitFor(page, () => /Checkout sync/.test(document.querySelector('[data-c3ph-recbar]')?.textContent || ''), null, 8000));
|
|
103
135
|
ok('moments: while recording, the card opens the meeting companion', await waitFor(page, () => !!document.querySelector('[data-c3ph-moment] [data-c3ph-focus]'), null, 5000));
|
|
136
|
+
const fbtn = await waitFor(page, () => !!document.querySelector('[data-c3ph-focus]'), null, 8000);
|
|
137
|
+
ok('focus: the Open the meeting companion button is found', fbtn);
|
|
138
|
+
if (fbtn) {
|
|
139
|
+
await page.click('[data-c3ph-focus]');
|
|
140
|
+
ok('focus: the compact focus page shows with Back to your day', await waitFor(page, () => !!document.querySelector('[data-c3focus][data-fc-pop] [data-fc="leave"]') && !document.querySelector('[data-c3focus] [data-fc="pop-open"]'), null, 8000));
|
|
141
|
+
await page.click('[data-c3focus] [data-fc="pop-head"] [data-fc="leave"]');
|
|
142
|
+
ok('focus: Back to your day closes it and the companion shows', await waitFor(page, () => !document.querySelector('[data-c3focus]') && !!document.querySelector('[data-c3ph-moment]'), null, 5000));
|
|
143
|
+
}
|
|
144
|
+
|
|
104
145
|
if (await page.$('[data-c3ph-stop]')) await page.click('[data-c3ph-stop]');
|
|
105
146
|
|
|
106
147
|
await page.close();
|
|
@@ -115,22 +156,29 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
115
156
|
await page.close();
|
|
116
157
|
page = await open(pctx, '/c3.html?demo=1&at=10:45');
|
|
117
158
|
|
|
118
|
-
/* ──
|
|
119
|
-
await page.
|
|
120
|
-
|
|
159
|
+
/* ── the meeting companion and back ── */
|
|
160
|
+
ok('nodesk: the companion offers no Full console', await page.evaluate(() => !document.querySelector('[data-c3ph-desk]') && !/Full console/.test(document.querySelector('[data-c3ph]').textContent)));
|
|
161
|
+
/* ── a long title and a long list of people stay inside the screen ── */
|
|
162
|
+
ok('narrow: long text does not widen the companion', await page.evaluate(() => {
|
|
163
|
+
const long = 'SC-01 Technical blueprint and estimate: ideal build versus the minimum viable path for the partner programme, MTGA41, Victor, Ho, Jasmine, Cheng, Cara, Chai, Tan, Lee';
|
|
164
|
+
document.querySelectorAll('[data-c3ph] .c3ph-big, [data-c3ph] .c3ph-tt, [data-c3ph] .pmc-title__text, [data-c3ph] .pmc-row__meta > span').forEach((n) => { n.textContent = long; });
|
|
165
|
+
const host = document.querySelector('[data-c3ph]');
|
|
166
|
+
return host.scrollWidth <= innerWidth + 1 && document.documentElement.scrollWidth <= innerWidth + 1;
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
/* ── a phone that chose Full console before it was dropped shows the companion again ── */
|
|
170
|
+
await page.evaluate(() => localStorage.setItem('pmos.c3.phone', 'desk'));
|
|
121
171
|
await page.reload({ waitUntil: 'load' });
|
|
122
|
-
await waitFor(page, () => !!document.querySelector('
|
|
123
|
-
ok('desk: the choice survives a reload', await page.evaluate(() => !document.querySelector('[data-c3ph]')));
|
|
124
|
-
const more = await page.$('[data-c3pg="more"]');
|
|
125
|
-
if (more) {
|
|
126
|
-
await more.click();
|
|
127
|
-
const item = await page.$('[data-a="phcomp"]');
|
|
128
|
-
ok('desk: More offers Phone companion', !!item);
|
|
129
|
-
if (item) { await item.click(); ok('desk: Phone companion brings the companion back', await waitFor(page, () => !!document.querySelector('[data-c3ph-moment]'), null, 5000)); }
|
|
130
|
-
} else ok('desk: the More button is found', false);
|
|
172
|
+
ok('nodesk: an old Full console choice is cleared and the companion shows', await waitFor(page, () => !!document.querySelector('[data-c3ph-moment]') && !localStorage.getItem('pmos.c3.phone'), null, 8000));
|
|
131
173
|
await page.close();
|
|
132
174
|
await pctx.close();
|
|
133
175
|
|
|
176
|
+
/* ── 620 wide: console3's phone styling starts at 640, so the companion must too (no squeezed desktop between) ── */
|
|
177
|
+
const mctx = await ctxOf(620);
|
|
178
|
+
page = await open(mctx, '/c3.html?demo=1');
|
|
179
|
+
ok('phone: the companion shows at 620 wide', await page.evaluate(shown, '[data-c3ph]'));
|
|
180
|
+
await mctx.close();
|
|
181
|
+
|
|
134
182
|
/* ── desktop ── */
|
|
135
183
|
const dctx = await ctxOf(1280);
|
|
136
184
|
page = await open(dctx, '/c3.html?demo=1', () => !!document.querySelector('nav.c3tabs [role="tab"]'));
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* more Backlog, Releases, Docs and Sprints show only after More; each still opens
|
|
10
10
|
* calendar one Layers button shows and hides the layer chips
|
|
11
11
|
* reports no deck is drawn
|
|
12
|
-
* phone at 400 wide
|
|
12
|
+
* phone at 400 wide on the desktop layout (lib/c3-full-console.js), nothing changes: the title shows and no dot is drawn (c3-phone-test.js tests the companion)
|
|
13
13
|
* No script error may be raised.
|
|
14
14
|
*/
|
|
15
15
|
const path = require('path');
|
|
@@ -7,7 +7,7 @@ timestamp: 2026-10-13
|
|
|
7
7
|
|
|
8
8
|
# Guide: the phone companion
|
|
9
9
|
|
|
10
|
-
When the console (`/`) is
|
|
10
|
+
When the console (`/`) is 640 pixels wide or less, for example on a phone, it shows the companion and not the desktop pages. The companion is not a small copy of the desktop. It shows the one thing that is useful now, and it keeps decisions for your computer.
|
|
11
11
|
|
|
12
12
|
## The moment card
|
|
13
13
|
|
|
@@ -15,9 +15,9 @@ The top card shows where you are in your day and why it is useful now. PMOS choo
|
|
|
15
15
|
|
|
16
16
|
| Moment | When | What the card shows |
|
|
17
17
|
|---|---|---|
|
|
18
|
-
| Your day | Before noon, with no meeting in the next 30 minutes | The next meeting, the number of
|
|
19
|
-
| Before you walk in | The next meeting starts in 30 minutes or less | What is prepared for it,
|
|
20
|
-
| In the meeting | A meeting is in progress | **Record this meeting?** with **Record** and **Not this one
|
|
18
|
+
| Your day | Before noon, with no meeting in the next 30 minutes | The next meeting, the number of things that wait on you and how many are overdue, and **The brief of this meeting** |
|
|
19
|
+
| Before you walk in | The next meeting starts in 30 minutes or less | What is prepared for it, the items to close first, each with **Done**, and **The brief of this meeting** |
|
|
20
|
+
| In the meeting | A meeting is in progress | **Record this meeting?** with **Record** and **Not this one**, and **Open the meeting companion**, which opens the meeting focus page. |
|
|
21
21
|
| Just ended | A meeting ended less than 20 minutes ago | **What came out of it?** If you recorded the meeting, the card shows the summary, the decisions, the actions and the open questions from its notes, the same notes as on the Notes page. While PMOS writes the notes, the card says so. If you did not record it, **Tell PMOS** puts the meeting name in the bar so you can type the result. |
|
|
22
22
|
| Free time | 15 minutes or more before the next meeting | The items that fit in the time: overdue items first, then items due today, at most two. Each has **Done** and **Later today**. |
|
|
23
23
|
| Wrapping up | From 17:00, or after noon with no meeting left | What is still open and what waits for your computer |
|
|
@@ -26,7 +26,15 @@ When the moment changes, a short message shows at the top for 6 seconds. The mes
|
|
|
26
26
|
|
|
27
27
|
## Waiting on you
|
|
28
28
|
|
|
29
|
-
Under the moment card,
|
|
29
|
+
Under the moment card, the things that wait on you show one at a time ("1 of 9"). They are the same things that the **Needs you** section of the desktop Briefing counts: your own items, the Jira and plan items assigned to you that are overdue, blocked or due today, the items of the morning brief, and the decisions.
|
|
30
|
+
|
|
31
|
+
- For your own Work items, **Done** marks the item done; the message that follows has **Undo**. **Later today** shows the item again later.
|
|
32
|
+
- For the other rows, **Details** shows why the row is there, its facts and a link to where it lives. Decisions wait for your computer.
|
|
33
|
+
- **Next** shows the next row. **See all** lists every row; tap a row to see its details.
|
|
34
|
+
|
|
35
|
+
## Meetings
|
|
36
|
+
|
|
37
|
+
Under the card, **Later today** lists the rest of today's meetings. Tap a meeting to see its brief: the time, the people, the initiative, then the same brief as the meeting card of the desktop Calendar (in short, what matters today, what came out of the last one, what to be ready with, and the links). From the brief you can record the meeting or open the meeting companion.
|
|
30
38
|
|
|
31
39
|
## Tell PMOS
|
|
32
40
|
|
|
@@ -34,11 +42,13 @@ Type a note, a to-do or an idea in the bar at the bottom and press Enter. PMOS p
|
|
|
34
42
|
|
|
35
43
|
## Record
|
|
36
44
|
|
|
37
|
-
The red button records a conversation or a meeting with the same recorder as **Record** on the desktop. While it records, the bar shows the name, the time and **Stop
|
|
45
|
+
The red button records a conversation or a meeting with the same recorder as **Record** on the desktop. While it records, the bar shows the name, the time and **Stop**, and the top card shows the notes that are being taken: the last lines that were heard, and the decisions, actions and open questions that PMOS has found so far. The card reads the note again every 5 seconds.
|
|
38
46
|
|
|
39
47
|
## The full console
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
A phone always shows the companion. There is no way to the desktop pages on a phone: use your computer for them. In a window wider than 640 pixels the console always shows the desktop pages.
|
|
50
|
+
|
|
51
|
+
**Open the meeting companion** opens the meeting helper over the companion. To go back, press **Back to your day** at the top or the bottom. The meeting continues to record.
|
|
42
52
|
|
|
43
53
|
## Limits
|
|
44
54
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
/* lib/c3-full-console.js — for browser suites that test the console's desktop layout at phone width.
|
|
3
|
-
* Since c3-phone-companion, console3 at
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Since c3-phone-companion, console3 at 640px or less shows the phone companion. The PM has no way to the desktop layout there
|
|
4
|
+
* (Full console was dropped on 2026-10-13); pmos.c3.testDesk = 'desk' keeps it, for suites only. fullConsole(browser) makes every
|
|
5
|
+
* new context start with it, so a suite that resizes to 400 wide still tests the desktop layout. c3-phone-test.js tests the companion itself. */
|
|
6
6
|
function fullConsole(browser) {
|
|
7
7
|
const make = browser.newContext.bind(browser);
|
|
8
8
|
browser.newContext = async (...args) => {
|
|
9
9
|
const ctx = await make(...args);
|
|
10
|
-
await ctx.addInitScript(() => { try { localStorage.setItem('pmos.c3.
|
|
10
|
+
await ctx.addInitScript(() => { try { localStorage.setItem('pmos.c3.testDesk', 'desk'); } catch { /* no storage */ } });
|
|
11
11
|
return ctx;
|
|
12
12
|
};
|
|
13
13
|
return browser;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.183.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.183.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -12,3 +12,19 @@ const NOTE = {
|
|
|
12
12
|
};
|
|
13
13
|
export const demoNoteFor = (m) => (m && (m.id === 'm2' || m.title === NOTE.title) ? NOTE : null);
|
|
14
14
|
export const demoNote = (id) => (id === NOTE.id ? { ok: true, note: NOTE } : null);
|
|
15
|
+
|
|
16
|
+
/* The note of a recording under way (?demo=1): what the phone companion shows while it records. The shapes are the real ones of GET
|
|
17
|
+
* /api/notes/one: transcript lines with their source, and the bullets the live notes pick out, each with its category. */
|
|
18
|
+
const LIVE = {
|
|
19
|
+
state: 'recording', speakers: { mic: 'You', meeting: 'Dara' },
|
|
20
|
+
transcript: [
|
|
21
|
+
{ t: 12, source: 'meeting', text: 'The sandbox keys are two days late, so 24 Oct is gone.' },
|
|
22
|
+
{ t: 31, source: 'mic', text: 'Then we hold beta on 30 Oct for the two partners already set up.' },
|
|
23
|
+
{ t: 48, source: 'meeting', text: 'I can get the new keys date in writing by Friday.' },
|
|
24
|
+
],
|
|
25
|
+
bullets: [
|
|
26
|
+
{ id: 0, cat: 'decision', text: 'Beta stays on 30 Oct for the two partners already set up' },
|
|
27
|
+
{ id: 1, cat: 'action', text: 'Dara gets the sandbox keys date in writing by Friday' },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
export const demoLive = (st) => ({ ok: true, note: { ...LIVE, id: st.id, title: st.title } });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* pm/c3-focus.js — the Meeting focus page of console3 (initiative c3-meeting-focus; the mockup the PM approved on 2026-10-12, version 7).
|
|
2
|
-
* openFocus({ event, date, pop, meet, mtitle }) closeFocus()
|
|
2
|
+
* openFocus({ event, date, pop, back, meet, mtitle }) closeFocus()
|
|
3
3
|
* One calm full-screen page over console3, inside #c3-root: it covers the header, the tabs and the dock (they stay in the DOM, set inert) and Leave
|
|
4
4
|
* focus gives them back. Recording is not touched by leaving. It reads GET /api/meeting-focus every 3 seconds while it is open:
|
|
5
5
|
* header title, time, people, initiative; the clock (starts in, or elapsed and left); REC with Start recording or End meeting (the note dock);
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* In demo mode (?demo=1) c3-demo-focus.js answers the reads with the mockup's Partner API review.
|
|
13
13
|
* Compact mode (?pop=1, initiative c3-meet-extension): the page the Chrome extension frames over a Google Meet call. Only a head (PMOS, the topic or "Starts in",
|
|
14
14
|
* the time left, a fold button), What's being said, the conversation and a foot (Open focus in PMOS, only you see this). The stage, the bar of the talk, the now line and
|
|
15
|
-
* the captured items are not drawn, and there is no Leave focus.
|
|
15
|
+
* the captured items are not drawn, and there is no Leave focus. With back (the phone companion opens it so) the head and the foot have
|
|
16
|
+
* "Back to your day" in place of Open focus in PMOS, which closes the page as Leave focus does. ?focus=now first asks GET /api/meeting-focus/now (meet = the Meet code, mtitle = the tab's title) which
|
|
16
17
|
* meeting the call is; when none fits, the page says so and lists today's meetings to pick from. Fold shows the head only and tells the frame's parent with one
|
|
17
18
|
* message, { pmosFocus: 'fold' | 'open' }, that carries no data. */
|
|
18
19
|
import { el } from './dom.js';
|
|
@@ -80,7 +81,7 @@ export function closeFocus() {
|
|
|
80
81
|
if (f.opener && document.contains(f.opener) && typeof f.opener.focus === 'function') { try { f.opener.focus({ preventScroll: true }); } catch { /* not focusable any more */ } }
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
export function openFocus({ event, date, pop, meet, mtitle } = {}) {
|
|
84
|
+
export function openFocus({ event, date, pop, back, meet, mtitle } = {}) {
|
|
84
85
|
closeFocus();
|
|
85
86
|
const ev = event || {};
|
|
86
87
|
const want = String(ev.id || '');
|
|
@@ -102,7 +103,8 @@ export function openFocus({ event, date, pop, meet, mtitle } = {}) {
|
|
|
102
103
|
el('div', { class: 'fc-popbody', 'data-fc': 'pop-body' },
|
|
103
104
|
r.band = el('div', { class: 'fc-bandwrap' }), convo,
|
|
104
105
|
r.foot = el('div', { class: 'fc-foot', 'data-fc': 'pop-foot' },
|
|
105
|
-
|
|
106
|
+
back ? pbtn('ghost', 'Back to your day', 'prev', { 'data-fc': 'leave', title: 'Back to the companion. The meeting keeps recording.' })
|
|
107
|
+
: lk({ 'data-fc': 'pop-open', title: 'Opens the full focus page in a new tab' }, icon('out', 'sm'), 'Open focus in PMOS'), el('span', { class: 'dim mono' }, 'only you see this'))))
|
|
106
108
|
: el('div', { class: 'fc-page' },
|
|
107
109
|
r.top = el('header', { class: 'card fc-top', 'aria-label': 'Meeting' }),
|
|
108
110
|
r.now = el('div', { class: 'fc-nowl', 'data-fc': 'now-line' }),
|
|
@@ -112,7 +114,7 @@ export function openFocus({ event, date, pop, meet, mtitle } = {}) {
|
|
|
112
114
|
el('aside', { class: 'fc-w', 'aria-label': 'Conversation' }, convo)));
|
|
113
115
|
root.append(page);
|
|
114
116
|
F = { id, date: day, ev0: ev, root, r, sig: {}, data: null, offset: 0, ui: { open: new Set(), folded: new Set(), pulled: [], stage: 0, picker: false, newTopic: false, addTopic: false, heldOpen: false, pending: new Map(), failed: new Map(), pill: false },
|
|
115
|
-
pop: !!pop, meet: String(meet || ''), mtitle: String(mtitle || ''), choices: null,
|
|
117
|
+
pop: !!pop, back: !!back, meet: String(meet || ''), mtitle: String(mtitle || ''), choices: null,
|
|
116
118
|
timers: {}, askTimers: new Map(), sheet: null, sheetOpen: false, status: 'reading', error: '',
|
|
117
119
|
opener: document.activeElement && document.activeElement !== document.body ? document.activeElement : null, seenNotes: 0 };
|
|
118
120
|
// a fixed box inside an element with container-type is placed against that element, not the screen: lift it while the page is open
|
|
@@ -232,7 +234,7 @@ function drawPopHead(d, ph) {
|
|
|
232
234
|
: ph === 'after' ? (e.title || 'Meeting') + ' has ended' : cur ? cur.text : e.title;
|
|
233
235
|
const left = d && ph === 'during' && Number.isFinite(f) ? el('span', { class: 'mono dim', 'data-fc': 'left', title: 'Time left' }, clockText((f - nowMs()) / 1000)) : null;
|
|
234
236
|
const lbl = F.ui.pill ? 'Open' : 'Fold to a pill';
|
|
235
|
-
return [el('b', { class: 'mono' }, 'PMOS'), el('span', { class: 'fc-ph-topic', 'data-fc': 'pop-topic', title: typeof label === 'string' ? label : '' }, label), left,
|
|
237
|
+
return [F.back ? ib('Back to your day', 'prev', { 'data-fc': 'leave' }) : null, el('b', { class: 'mono' }, 'PMOS'), el('span', { class: 'fc-ph-topic', 'data-fc': 'pop-topic', title: typeof label === 'string' ? label : '' }, label), left,
|
|
236
238
|
ib(lbl, F.ui.pill ? 'next' : 'fold', { 'data-fc': 'pill', 'aria-pressed': String(F.ui.pill) })];
|
|
237
239
|
});
|
|
238
240
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* pm/c3-phone-detail.js — the phone companion's sheets (initiative c3-phone-companion). Each opens as the console3 bottom sheet (c3-sheet.js).
|
|
2
|
+
* openMeeting(m, opts) a meeting of today: when, who, the initiative, what is prepared, then its brief as GET /api/meeting-focus
|
|
3
|
+
* returns it (lib/calendar-layers.js briefOf, the same brief as the desktop Calendar card): In short, What matters,
|
|
4
|
+
* Since the last one, To prepare, links. opts: { onRecord(m), onFocus(m), recording } adds Record it and
|
|
5
|
+
* Open the meeting companion. A failed read keeps what the companion already knew and says so.
|
|
6
|
+
* openNeed(r, opts) one thing that waits on the PM (a row of waitRows in c3-phone.js): why, the facts, the source link, and
|
|
7
|
+
* Done and Later today when the row is the PM's own Work item. opts: { onDone(r), onLater(r) }
|
|
8
|
+
* openAll(rows, opts) every row that waits, as a list; a press opens that row (openNeed). opts as openNeed.
|
|
9
|
+
* Nothing here writes: the buttons call back into c3-phone.js, which owns the writes and their Undo. */
|
|
10
|
+
import { el } from './dom.js';
|
|
11
|
+
import { button, row } from './primitives.js';
|
|
12
|
+
import { pmGet } from './http.js';
|
|
13
|
+
import { c3ic } from './pm-work-view.js';
|
|
14
|
+
import { openSheet, closeSheet } from './c3-sheet.js';
|
|
15
|
+
|
|
16
|
+
const p2 = (n) => String(n).padStart(2, '0');
|
|
17
|
+
const hm = (iso) => { const d = new Date(iso); return p2(d.getHours()) + ':' + p2(d.getMinutes()); };
|
|
18
|
+
const dayOf = (iso) => { const d = new Date(iso); return d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()); };
|
|
19
|
+
const line = (t, attrs) => el('div', { class: 'c3ph-line', ...(attrs || {}) }, t);
|
|
20
|
+
const sect = (key, title, ...kids) => el('div', { class: 'c3ph-fit', 'data-c3ph-sect': key }, el('b', null, title), ...kids.filter(Boolean));
|
|
21
|
+
const btn = (label, variant, icon, attrs, onClick) => button({ label, variant, icon, attrs: { ...(attrs || {}), class: 'c3ph-b' }, onClick });
|
|
22
|
+
const link = (url, label) => el('a', { class: 'chip2', href: /^https?:\/\//.test(url) ? url : 'https://' + url, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), label);
|
|
23
|
+
|
|
24
|
+
/** The parts of a briefOf brief, as phone sections. Pure over the brief; null parts are left out. */
|
|
25
|
+
export function briefParts(b) {
|
|
26
|
+
if (!b) return [];
|
|
27
|
+
const out = [];
|
|
28
|
+
if (b.gist && b.gist.length) out.push(sect('gist', 'In short', line(b.gist.join(' '))));
|
|
29
|
+
if (b.matters && b.matters.length) out.push(sect('matters', b.planning ? 'To settle in planning' : 'What matters today', ...b.matters.map((x) => line([x.tag ? x.tag + ': ' : '', x.text, x.meta ? ' · ' + x.meta : ''].join('')))));
|
|
30
|
+
if (b.last) {
|
|
31
|
+
const l = b.last;
|
|
32
|
+
out.push(sect('last', 'Since the last one',
|
|
33
|
+
l.private ? line('That note is private, so only its title shows: ' + (l.title || 'Untitled note') + '.') : line(l.overview || 'That note has no summary yet.'),
|
|
34
|
+
...(l.private ? [] : (l.decisions || []).map((t) => line('Decision: ' + t))),
|
|
35
|
+
...(l.private ? [] : (l.actions || []).map((a) => line('From last time: ' + a.text + [a.owner, a.due && 'due ' + a.due].filter(Boolean).map((s) => ' · ' + s).join(''))))));
|
|
36
|
+
}
|
|
37
|
+
if (b.prep) out.push(sect('prep', 'To be ready', line(b.prep)));
|
|
38
|
+
const links = [...(b.links || []).filter((l) => l && l.url).map((l) => link(l.url, l.label)), ...(b.refs || []).filter((r) => r && r.link).map((r) => link(r.link, r.label))];
|
|
39
|
+
if (links.length) out.push(el('div', { class: 'chips2', 'data-c3ph-sect': 'links' }, ...links));
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function openMeeting(m, opts = {}) {
|
|
44
|
+
const known = [
|
|
45
|
+
line(hm(m.start) + (m.end ? '–' + hm(m.end) : '') + (m.minutes ? ' · ' + m.minutes + ' min' : '')),
|
|
46
|
+
m.people && m.people.length ? line(m.people.join(', ')) : null,
|
|
47
|
+
m.initiative ? line('Initiative: ' + m.initiative) : null,
|
|
48
|
+
...(m.prepared || []).map((x) => el('div', { class: 'c3ph-prep' }, c3ic(x.kind === 'prep' ? 'spark' : 'doc', 'sm'), x.text)),
|
|
49
|
+
...(m.before || []).map((x) => line('Before you go: ' + x.title)),
|
|
50
|
+
].filter(Boolean);
|
|
51
|
+
const brief = el('div', { class: 'c3ph-sheetbody', 'data-c3ph-brief': 'loading' }, line('Reading the brief of this meeting.'));
|
|
52
|
+
const acts = el('div', { class: 'c3ph-acts' },
|
|
53
|
+
opts.onRecord && !opts.recording ? btn('Record it', 'primary', 'mic', { 'data-c3ph-sheet-record': m.id }, () => { closeSheet(); opts.onRecord(m); }) : null,
|
|
54
|
+
opts.onFocus ? btn('Open the meeting companion', 'outline', 'target', { 'data-c3ph-sheet-focus': m.id }, () => { closeSheet(); opts.onFocus(m); }) : null);
|
|
55
|
+
const sh = openSheet({ title: m.title, icon: 'calendar', body: [el('div', { class: 'c3ph-sheetbody', 'data-c3ph-meeting': m.id }, ...known), brief, acts] });
|
|
56
|
+
pmGet('/api/meeting-focus?event=' + encodeURIComponent(m.id) + '&date=' + dayOf(m.start))
|
|
57
|
+
.then((j) => {
|
|
58
|
+
const parts = j && j.ok !== false ? briefParts(j.brief) : null;
|
|
59
|
+
brief.dataset.c3phBrief = parts ? (parts.length ? 'ready' : 'empty') : 'error';
|
|
60
|
+
brief.replaceChildren(...(parts === null ? [line('The brief could not be read' + (j && j.error ? ': ' + j.error : '.'))]
|
|
61
|
+
: parts.length ? parts : [line('PMOS holds nothing more on this meeting. Link it to an initiative on your computer, and what matters there shows here.')]));
|
|
62
|
+
}, () => { brief.dataset.c3phBrief = 'error'; brief.replaceChildren(line('The brief could not be read.')); });
|
|
63
|
+
return sh;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function openNeed(r, opts = {}) {
|
|
67
|
+
const body = el('div', { class: 'c3ph-sheetbody', 'data-c3ph-need': r.id },
|
|
68
|
+
el('span', { class: 'c3ph-step' }, r.label.toUpperCase()),
|
|
69
|
+
r.why ? line(r.why) : null,
|
|
70
|
+
...(r.facts || []).map((f) => line(f)),
|
|
71
|
+
r.url ? el('div', { class: 'chips2' }, link(r.url, r.urlLabel || 'Open the source')) : null,
|
|
72
|
+
r.pm ? el('div', { class: 'c3ph-acts two' },
|
|
73
|
+
btn('Done', 'primary', 'check', { 'data-c3ph-sheet-done': r.id }, () => { closeSheet(); opts.onDone && opts.onDone(r); }),
|
|
74
|
+
btn('Later today', 'ghost', 'clock', { 'data-c3ph-sheet-later': r.id }, () => { closeSheet(); opts.onLater && opts.onLater(r); }))
|
|
75
|
+
: line(r.decide ? 'A decision: make it on your computer, where the options and their evidence are.' : 'Change it where it lives, or on your computer.'));
|
|
76
|
+
return openSheet({ title: r.headline, icon: 'inbox', body });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function openAll(rows, opts = {}) {
|
|
80
|
+
const list = el('div', { class: 'c3ph-sheetbody', 'data-c3ph-all': String(rows.length) },
|
|
81
|
+
...rows.map((r) => row({ title: r.headline, meta: [r.label], compact: true, onOpen: () => openNeed(r, opts), openLabel: 'Open ' + r.headline, attrs: { 'data-c3ph-all-row': r.id } })));
|
|
82
|
+
return openSheet({ title: rows.length + (rows.length === 1 ? ' thing waits on you' : ' things wait on you'), icon: 'inbox', body: list });
|
|
83
|
+
}
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
* Everything is scoped under .c3 and uses tokens only. */
|
|
5
5
|
.c3.c3-phone-on .c3grid{display:none !important}
|
|
6
6
|
.c3.c3-phone-on{min-height:100dvh}
|
|
7
|
-
.c3 .c3ph{display:flex;flex-direction:column;min-height:100dvh;background:var(--background)}
|
|
7
|
+
.c3 .c3ph{display:flex;flex-direction:column;min-height:100dvh;width:100%;max-width:100vw;min-width:0;overflow-x:hidden;background:var(--background)}
|
|
8
|
+
/* a long title or a long list of people wraps or is cut with an ellipsis; it never widens the companion past the screen */
|
|
9
|
+
.c3 .c3ph *{min-width:0}
|
|
10
|
+
.c3 .c3ph-body,.c3 .c3ph-card,.c3 .c3ph-later,.c3 .c3ph-fit,.c3 .c3ph-sheetbody{grid-template-columns:minmax(0,1fr)}
|
|
11
|
+
.c3 .c3ph-big,.c3 .c3ph-tt,.c3 .c3ph-line,.c3 .c3ph-hd b{overflow-wrap:anywhere}
|
|
8
12
|
.c3 .c3ph-hd{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:calc(10px + env(safe-area-inset-top)) 16px 10px;position:sticky;top:0;background:var(--background);z-index:2}
|
|
9
|
-
.c3 .c3ph-hd b{font-size:var(--fs-4)}
|
|
10
|
-
.c3 .c3ph-hdr{display:flex;align-items:center;gap:8px}
|
|
13
|
+
.c3 .c3ph-hd b{font-size:var(--fs-4);flex:1;min-width:0}
|
|
14
|
+
.c3 .c3ph-hdr{display:flex;align-items:center;gap:8px;flex:none}
|
|
11
15
|
.c3 .c3ph-dotn{display:inline-flex;align-items:center;gap:4px;font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
|
|
12
16
|
.c3 .c3ph-body{flex:1;display:grid;gap:12px;align-content:start;padding:0 12px 16px}
|
|
13
17
|
.c3 .c3ph-card{display:grid;gap:8px;padding:14px}
|
|
@@ -20,15 +24,13 @@
|
|
|
20
24
|
.c3 .c3ph-rec-why{font-size:var(--fs-2);line-height:1.45;background:var(--muted);border-radius:8px;padding:8px 10px}
|
|
21
25
|
.c3 .c3ph-prep{display:flex;gap:6px;align-items:flex-start;font-size:var(--fs-2);line-height:1.45}
|
|
22
26
|
.c3 .c3ph-before,.c3 .c3ph-fit{display:grid;gap:6px;padding-top:8px;border-top:1px solid var(--border);font-size:var(--fs-2)}
|
|
23
|
-
.c3 .c3ph-before{grid-template-columns:1fr auto;align-items:center}
|
|
27
|
+
.c3 .c3ph-before{grid-template-columns:minmax(0,1fr) auto;align-items:center}
|
|
24
28
|
.c3 .c3ph-acts{display:grid;gap:8px}
|
|
25
29
|
.c3 .c3ph-acts.two{grid-template-columns:1fr 1fr}
|
|
26
30
|
.c3 .c3ph-b{min-height:44px;justify-content:center}
|
|
27
31
|
.c3 .c3ph-cnt{display:flex;justify-content:space-between;font-weight:600;font-size:var(--fs-2)}
|
|
28
32
|
.c3 .c3ph-cnt .dim{font-weight:400;font-family:var(--mono)}
|
|
29
|
-
.c3 .c3ph-later{display:grid;gap:4px;font-size:var(--fs-2);color:var(--muted-foreground);padding:0 4px}
|
|
30
|
-
.c3 .c3ph-later > div{display:flex;gap:10px}
|
|
31
|
-
.c3 .c3ph-later .mono{font-family:var(--mono);min-width:42px}
|
|
33
|
+
.c3 .c3ph-later{display:grid;overflow:hidden;gap:4px;font-size:var(--fs-2);color:var(--muted-foreground);padding:0 4px}
|
|
32
34
|
.c3 .c3ph-ft{position:sticky;bottom:0;display:flex;align-items:center;gap:10px;padding:8px 12px calc(12px + env(safe-area-inset-bottom));border-top:1px solid var(--border);background:var(--card);z-index:2}
|
|
33
35
|
.c3 .c3ph-ask{flex:1;display:flex;align-items:center;gap:8px;border:1px solid var(--border);border-radius:8px;padding:0 4px 0 12px;min-height:44px;color:var(--muted-foreground);background:var(--background)}
|
|
34
36
|
.c3 .c3ph-ask input.c3ph-in{flex:1;min-width:0;border:0;background:transparent;font-family:inherit;font-size:var(--fs-3);color:var(--foreground);padding:10px 0;outline:none;border-radius:0}
|
|
@@ -3,17 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* It has no tabs and no inbox to visit. It brings the one thing that matters now (c3-phone-model.js momentOf), then the PM's own items one
|
|
5
5
|
* at a time, then what comes later today. Two controls sit at the bottom: "Tell PMOS" and the red Record button.
|
|
6
|
-
* moment card meeting under way: "Record this meeting?" (Record / Not this one)
|
|
6
|
+
* moment card meeting under way: "Record this meeting?" (Record / Not this one), and the meeting companion (c3-focus.js, compact)
|
|
7
|
+
* recording (any recording): the live notes of GET /api/notes/one, read every 5 s: what was just heard, and the decisions,
|
|
8
|
+
* actions and questions picked out so far (dest-notes.js partsOf), as the desktop note shows them
|
|
7
9
|
* before a meeting: when it starts, who, what is prepared, the "before you go" items with Done
|
|
8
10
|
* just ended: tell PMOS what came out of it a free gap: the own items that fit, with Done and Later today
|
|
9
11
|
* morning: the next meeting and how much waits wrap: what is still open, what waits for the desk
|
|
10
|
-
* waiting card
|
|
11
|
-
*
|
|
12
|
+
* waiting card what waits on the PM, as the desktop Briefing counts it: the own items of GET /api/brief-page, then the needs of
|
|
13
|
+
* GET /api/briefing (assigned Jira and WBS items, Work items, morning brief items, decisions), one at a time, with See all.
|
|
14
|
+
* An own Work item has Done (POST /api/pm-items/done, Undo: /reopen) and Later today (POST /api/pm-items/notyet); any other row
|
|
15
|
+
* opens its details (c3-phone-detail.js openNeed). Decisions are not answered on the phone: they wait for the desk.
|
|
16
|
+
* meetings the next meeting and each row of Later today open the meeting's sheet with its brief (c3-phone-detail.js openMeeting).
|
|
12
17
|
* Tell PMOS a line of text, filed by POST /api/capture { ws, text }: it lands in the Work inbox, where triage files it. The small mic
|
|
13
18
|
* points to the keyboard's own dictation; this page does not send audio anywhere.
|
|
14
19
|
* Record the red button records a conversation or a meeting, the same recorder as the desktop Record control (note-dock.js).
|
|
15
20
|
* While it records, the bar shows the clock and Stop. In demo mode it drives the simulated recorder and saves nothing.
|
|
16
|
-
*
|
|
21
|
+
* There is no way to the desktop pages on a phone. The meeting companion opens as the compact focus page with "Back to your day".
|
|
17
22
|
* The moment is worked out again every minute; when it changes, a short banner says so. ?at=HH:MM sets the clock in demo mode only. */
|
|
18
23
|
import { el } from './dom.js';
|
|
19
24
|
import { button, iconButton } from './primitives.js';
|
|
@@ -23,6 +28,9 @@ import { onBriefPage, loadBriefPage } from './c3-brief-data.js';
|
|
|
23
28
|
import { onBriefing, loadBriefing } from './shell-ticker.js';
|
|
24
29
|
import { toast } from './c3-toast.js';
|
|
25
30
|
import { momentOf, inText } from './c3-phone-model.js';
|
|
31
|
+
import { openMeeting, openNeed, openAll } from './c3-phone-detail.js';
|
|
32
|
+
import { cardView } from './briefing-model.js';
|
|
33
|
+
import { row } from './primitives.js';
|
|
26
34
|
|
|
27
35
|
const CSS_ID = 'c3-phone-css';
|
|
28
36
|
const p2 = (n) => String(n).padStart(2, '0');
|
|
@@ -30,6 +38,7 @@ const hm = (iso) => { const d = new Date(iso); return p2(d.getHours()) + ':' + p
|
|
|
30
38
|
const first = (n) => String(n || '').trim().split(/\s+/)[0];
|
|
31
39
|
const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
32
40
|
const MO = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
41
|
+
const LIVE_MS = 5000; // how often the note being recorded is read again
|
|
33
42
|
const clockOf = (msec) => { const s = Math.max(0, Math.floor(msec / 1000)); return Math.floor(s / 60) + ':' + p2(s % 60); };
|
|
34
43
|
|
|
35
44
|
function ensureCss() {
|
|
@@ -37,10 +46,11 @@ function ensureCss() {
|
|
|
37
46
|
document.head.appendChild(el('link', { id: CSS_ID, rel: 'stylesheet', href: '/pm/c3-phone.css' }));
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
/** Is this device in phone mode? Phone width
|
|
49
|
+
/** Is this device in phone mode? Phone width: there is no choice to show the desktop pages on a phone (Wawan, 2026-10-13).
|
|
50
|
+
* pmos.c3.testDesk is for the browser suites that test the desktop layout at phone width (lib/c3-full-console.js); nothing offers it. */
|
|
41
51
|
export function phoneWanted(store) {
|
|
42
|
-
if (typeof matchMedia !== 'function' || !matchMedia('(max-width:
|
|
43
|
-
return !(store && store.get('pmos.c3.
|
|
52
|
+
if (typeof matchMedia !== 'function' || !matchMedia('(max-width:640px)').matches) return false;
|
|
53
|
+
return !(store && store.get('pmos.c3.testDesk') === 'desk');
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
export function mountPhone(root, ctx = {}) {
|
|
@@ -90,6 +100,20 @@ export function mountPhone(root, ctx = {}) {
|
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
102
|
if (S.clock) S.clock.textContent = st.mode === 'recording' && st.started ? clockOf(dock.now() - st.started) : st.mode === 'paused' ? 'paused' : '';
|
|
103
|
+
if (st.mode !== 'idle' && st.id && !S.liveBusy && (!S.live || S.live.id !== st.id || Date.now() - S.live.at > LIVE_MS)) readLive(st);
|
|
104
|
+
if (st.mode === 'idle' && S.live) { S.live = null; draw(); }
|
|
105
|
+
}
|
|
106
|
+
/* ── the note being recorded (GET /api/notes/one): what was just heard, and the decisions, actions and questions picked out so far ── */
|
|
107
|
+
async function readLive(st) {
|
|
108
|
+
S.liveBusy = true;
|
|
109
|
+
try {
|
|
110
|
+
const [src, dn, vn] = await Promise.all([demo ? import('./c3-demo-phone.js') : null, import('./dest-notes.js'), import('./view-notes.js')]);
|
|
111
|
+
const one = demo ? src.demoLive(st) : await pmGet('/api/notes/one?id=' + encodeURIComponent(st.id));
|
|
112
|
+
const n = one && one.ok !== false ? one.note : null;
|
|
113
|
+
S.live = { id: st.id, at: Date.now(), note: n, parts: n ? dn.partsOf(n) : null, speakerOf: vn.speakerOf };
|
|
114
|
+
} catch { S.live = { id: st.id, at: Date.now(), note: null, parts: null }; }
|
|
115
|
+
S.liveBusy = false;
|
|
116
|
+
draw();
|
|
93
117
|
}
|
|
94
118
|
|
|
95
119
|
/* ── writes ── */
|
|
@@ -146,24 +170,68 @@ export function mountPhone(root, ctx = {}) {
|
|
|
146
170
|
const card = (hero, attrs, ...kids) => el('section', { class: 'card c3ph-card' + (hero ? ' lead' : ''), ...(attrs || {}) }, ...kids);
|
|
147
171
|
const btn = (label, variant, icon, attrs, onClick) => button({ label, variant, icon, attrs: { ...(attrs || {}), class: 'c3ph-b' }, onClick });
|
|
148
172
|
const who = (m) => (m.people && m.people.length ? m.people.map(first).join(', ') : '');
|
|
149
|
-
const own = () => ((S.page && S.page.needs) || []).filter((n) => n && n.id && !S.gone.has(n.id));
|
|
150
|
-
|
|
173
|
+
const own = () => ((S.page && S.page.needs) || []).filter((n) => n && n.id && n.kind !== 'decision' && !S.gone.has(n.id));
|
|
174
|
+
/* everything that waits on the PM, as the desktop Briefing's Needs you counts it (c3-brief-needs.js needsCount): the own items of
|
|
175
|
+
* GET /api/brief-page, then the briefing's needs (assigned Jira and WBS items, own Work items, morning brief items, decisions). */
|
|
176
|
+
function waitRows() {
|
|
177
|
+
const out = []; const seen = new Set();
|
|
178
|
+
own().forEach((n) => { seen.add(n.id); out.push({ id: n.id, pm: true, headline: n.headline, raw: n, why: n.evidence && n.evidence.text ? 'Looks done: ' + n.evidence.text + '.' : null,
|
|
179
|
+
label: n.kind === 'overdue' ? (n.days_late === 1 ? '1 day late' : n.days_late + ' days late') : n.kind === 'due-today' ? 'Due today' : n.person ? 'Talk to ' + first(n.person) : 'Outside PMOS' }); });
|
|
180
|
+
((S.briefing && S.briefing.needs) || []).forEach((n, i) => {
|
|
181
|
+
if (!n) return;
|
|
182
|
+
const pm = n.origin === 'work' && !!n.itemId;
|
|
183
|
+
const id = pm ? n.itemId : String(n.key || 'need' + i);
|
|
184
|
+
if (seen.has(id) || (n.itemId && seen.has(n.itemId)) || S.gone.has(id)) return;
|
|
185
|
+
seen.add(id);
|
|
186
|
+
if (n.card) {
|
|
187
|
+
const v = cardView(n.card);
|
|
188
|
+
out.push({ id, pm: false, decide: true, headline: v.headline, label: v.tag + (v.deadline && v.deadline.text ? ' · ' + v.deadline.text : ''), why: n.card.why && n.card.why.text, url: n.card.url, urlLabel: n.card.url_label, facts: [] });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
out.push({ id, pm, decide: n.kind === 'Decide', headline: n.headline, label: [n.kind, n.dueText && n.dueText !== n.kind ? n.dueText : null, n.p].filter(Boolean).join(' · '),
|
|
192
|
+
why: n.why, url: n.url, urlLabel: n.url_label, facts: (n.facts || []).map((f) => f && f.text).filter(Boolean) });
|
|
193
|
+
});
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
const decisions = () => waitRows().filter((r) => !r.pm).length;
|
|
197
|
+
const needOpts = { onDone: (r) => itemDone(r), onLater: (r) => itemLater(r, 'later-today') };
|
|
198
|
+
const focus = (m) => import('./c3-focus.js').then((f) => f.openFocus({ event: { id: m.id, start: m.start }, pop: true, back: true })).catch(() => toast('The meeting companion did not open.'));
|
|
199
|
+
/* a meeting's sheet (c3-phone-detail.js): Record it while the meeting has not ended, and the meeting companion */
|
|
200
|
+
function meetingSheet(m) {
|
|
201
|
+
const t = now().getTime(); const ends = Date.parse(m.end || m.start) || 0;
|
|
202
|
+
openMeeting(m, { recording: recState().mode !== 'idle', onRecord: t < ends ? (x) => startRec(x) : null, onFocus: focus });
|
|
203
|
+
}
|
|
151
204
|
|
|
152
|
-
function
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
205
|
+
function liveCard(st, mo) {
|
|
206
|
+
const L = S.live && S.live.id === st.id ? S.live : null;
|
|
207
|
+
const n = L && L.note; const p = L && L.parts;
|
|
208
|
+
const ev = (n && n.meeting && n.meeting.event_id) ? { id: n.meeting.event_id, start: (mo.meeting && mo.meeting.start) || now().toISOString() } : (mo.kind === 'meeting' ? mo.meeting : null);
|
|
209
|
+
const heard = n ? (n.transcript || []).filter((x) => x && x.text && !x.dropped).slice(-3) : [];
|
|
210
|
+
const list = (label, rows) => (rows && rows.length ? el('div', { class: 'c3ph-fit', 'data-c3ph-live-list': label.toLowerCase() }, el('b', null, label + ' · ' + rows.length), ...rows.slice(-2).map((x) => line(x.text))) : null);
|
|
211
|
+
const ov = p && p.overview ? (p.overview.length > 220 ? p.overview.slice(0, 217) + '…' : p.overview) : '';
|
|
212
|
+
return card(true, { 'data-c3ph-moment': 'live', 'data-c3ph-live': !L ? 'loading' : n ? 'ready' : 'none' },
|
|
213
|
+
why(st.mode === 'paused' ? 'Paused' : 'Recording now'), big(st.title || 'Conversation'),
|
|
214
|
+
ov ? line(ov) : null,
|
|
215
|
+
p ? list('Decisions', p.decisions) : null, p ? list('Actions', p.actions) : null, p ? list('Open questions', p.questions) : null,
|
|
216
|
+
heard.length ? el('div', { class: 'c3ph-fit', 'data-c3ph-heard': String(heard.length) }, el('b', null, 'Just heard'), ...heard.map((x) => line((L.speakerOf ? L.speakerOf(n, x.source) + ': ' : '') + x.text)))
|
|
217
|
+
: line(!L ? 'Opening the notes.' : n ? 'Listening. What is said shows here, and PMOS picks out the decisions, actions and questions as they come.' : 'The notes of this recording could not be read. They are kept on your computer.'),
|
|
218
|
+
ev ? acts(btn('Open the meeting companion', 'outline', 'target', { 'data-c3ph-focus': ev.id }, () => focus(ev))) : null);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function itemCard(r, hero) {
|
|
222
|
+
if (!r.label) return itemCard(waitRows().find((x) => x.id === r.id) || { ...r, pm: true, label: 'Yours' }, hero);
|
|
223
|
+
const looks = r.raw && r.raw.evidence;
|
|
224
|
+
return [el('span', { class: 'c3ph-step' }, r.label.toUpperCase()), el('b', { class: 'c3ph-tt' }, r.headline),
|
|
225
|
+
r.why ? line(r.why) : null,
|
|
226
|
+
r.pm ? acts(btn(looks ? 'Confirm done' : 'Done', hero ? 'primary' : 'outline', 'check', { 'data-c3ph-done': r.id }, () => itemDone(r)),
|
|
227
|
+
btn('Later today', 'ghost', 'clock', { 'data-c3ph-later': r.id }, () => itemLater(r, 'later-today')))
|
|
228
|
+
: acts(btn('Details', 'outline', 'chevron-right', { 'data-c3ph-details': r.id }, () => openNeed(r, needOpts)))];
|
|
158
229
|
}
|
|
159
230
|
|
|
160
231
|
function momentCard(mo) {
|
|
161
232
|
const m = mo.meeting;
|
|
162
233
|
if (mo.kind === 'meeting') {
|
|
163
|
-
const
|
|
164
|
-
const recording = st.mode !== 'idle';
|
|
165
|
-
const open = btn('Open the meeting companion', recording ? 'primary' : 'outline', 'target', { 'data-c3ph-focus': m.id }, () => import('./c3-focus.js').then((f) => f.openFocus({ event: { id: m.id, start: m.start }, pop: true })).catch(() => toast('The meeting companion did not open.')));
|
|
166
|
-
if (recording) return card(true, { 'data-c3ph-moment': 'meeting' }, why('Under way · ends ' + inText(mo.minutes)), big(m.title), line('Recording. PMOS follows the talk and writes the notes.'), acts(open));
|
|
234
|
+
const open = btn('Open the meeting companion', 'outline', 'target', { 'data-c3ph-focus': m.id }, () => focus(m));
|
|
167
235
|
if (S.noRec.has(m.id)) return card(true, { 'data-c3ph-moment': 'meeting' }, why('Under way · ends ' + inText(mo.minutes)), big(m.title), line('Not recording. PMOS follows the agenda and what you mark.'), acts(open, btn('Record from now', 'ghost', 'mic', { 'data-c3ph-rec-meeting': m.id }, () => startRec(m))));
|
|
168
236
|
return card(true, { 'data-c3ph-moment': 'meeting' }, why('Starting now' + (who(m) ? ' · ' + who(m) : '')), big('Record ' + m.title + '?'),
|
|
169
237
|
el('div', { class: 'c3ph-rec-why' }, 'Recording lets PMOS follow the talk, bring up what you need at the right topic, and write the notes for you.'),
|
|
@@ -177,7 +245,8 @@ export function mountPhone(root, ctx = {}) {
|
|
|
177
245
|
...before.map((x) => el('div', { class: 'c3ph-before', 'data-c3ph-before': x.id }, el('span', null, 'Before you go: ' + x.title),
|
|
178
246
|
btn('Done', 'outline', 'check', { 'data-c3ph-done': x.id }, () => itemDone({ id: x.id, headline: x.title })))),
|
|
179
247
|
!(m.prepared || []).length && !before.length ? line('Nothing is prepared for it.') : null,
|
|
180
|
-
line('When it starts, PMOS asks to record it.', { 'data-c3ph-recnote': '' })
|
|
248
|
+
line('When it starts, PMOS asks to record it.', { 'data-c3ph-recnote': '' }),
|
|
249
|
+
acts(btn('The brief of this meeting', 'outline', 'calendar', { 'data-c3ph-meeting-open': m.id }, () => meetingSheet(m))));
|
|
181
250
|
}
|
|
182
251
|
if (mo.kind === 'after') {
|
|
183
252
|
const db = debriefOf(m);
|
|
@@ -208,37 +277,42 @@ export function mountPhone(root, ctx = {}) {
|
|
|
208
277
|
...(fits.length ? fits.map((n) => el('div', { class: 'c3ph-fit', 'data-c3ph-fit': n.id }, ...itemCard(n, false))) : [line('Nothing of yours is due. A good moment to prepare the next meeting.')]));
|
|
209
278
|
}
|
|
210
279
|
if (mo.kind === 'morning') {
|
|
211
|
-
const
|
|
280
|
+
const w = waitRows(); const late = w.filter((r) => /overdue|late/i.test(r.label)).length;
|
|
212
281
|
return card(true, { 'data-c3ph-moment': 'morning' }, why('Your day'), big(mo.next ? mo.next.title + ' at ' + hm(mo.next.start) : 'No meetings today'),
|
|
213
|
-
line([mo.next ? 'First up, ' + inText((Date.parse(mo.next.start) - now().getTime()) / 60000) + '.' : null,
|
|
282
|
+
line([mo.next ? 'First up, ' + inText((Date.parse(mo.next.start) - now().getTime()) / 60000) + '.' : null,
|
|
283
|
+
w.length ? w.length + (w.length === 1 ? ' thing waits on you' : ' things wait on you') + (late ? ', ' + late + ' overdue.' : '.') : 'Nothing waits on you.'].filter(Boolean).join(' ')),
|
|
284
|
+
mo.next ? acts(btn('The brief of this meeting', 'outline', 'calendar', { 'data-c3ph-meeting-open': mo.next.id }, () => meetingSheet(mo.next))) : null);
|
|
214
285
|
}
|
|
215
|
-
const o =
|
|
286
|
+
const o = waitRows().filter((r) => r.pm);
|
|
216
287
|
return card(true, { 'data-c3ph-moment': 'wrap' }, why('Wrapping up'), big(o.length ? o.length + ' still open today' : 'Nothing left for today'),
|
|
217
288
|
line(decisions() ? decisions() + (decisions() === 1 ? ' decision waits' : ' decisions wait') + ' for your desk. It opens first on your Briefing.' : 'Nothing waits for your desk.'));
|
|
218
289
|
}
|
|
219
290
|
|
|
220
291
|
function waitingCard(mo) {
|
|
221
292
|
const skipIds = new Set(((mo.kind === 'gap' && mo.fits) || []).map((n) => n.id));
|
|
222
|
-
const
|
|
293
|
+
const all = waitRows().filter((r) => !skipIds.has(r.id));
|
|
294
|
+
const list = all.filter((r) => !S.skipped.has(r.id));
|
|
223
295
|
if (!list.length) return null;
|
|
224
296
|
const n = list[0];
|
|
225
297
|
return card(false, { 'data-c3ph-waiting': n.id }, el('div', { class: 'c3ph-cnt' }, el('span', null, 'Waiting on you'), el('span', { class: 'dim' }, '1 of ' + list.length)),
|
|
226
|
-
...itemCard(n, false),
|
|
298
|
+
...itemCard(n, false),
|
|
299
|
+
el('div', { class: 'c3ph-acts two' },
|
|
300
|
+
list.length > 1 ? button({ label: 'Next', variant: 'ghost', size: 'sm', iconEnd: 'chevron-right', attrs: { 'data-c3ph-skip': n.id }, onClick: () => { S.skipped.add(n.id); draw(); } }) : null,
|
|
301
|
+
all.length > 1 ? button({ label: 'See all ' + all.length, variant: 'ghost', size: 'sm', attrs: { 'data-c3ph-all-open': '' }, onClick: () => openAll(all, needOpts) }) : null));
|
|
227
302
|
}
|
|
228
303
|
|
|
229
304
|
function laterList(mo) {
|
|
230
305
|
const rows = (mo.later || []).slice(0, 3);
|
|
231
306
|
if (!rows.length) return null;
|
|
232
307
|
return el('div', { class: 'c3ph-later', 'data-c3ph-later-list': '' }, el('span', { class: 'c3ph-step' }, 'LATER TODAY'),
|
|
233
|
-
...rows.map((m) =>
|
|
308
|
+
...rows.map((m) => row({ title: m.title, meta: [hm(m.start), who(m)], compact: true, onOpen: () => meetingSheet(m), openLabel: 'Open ' + m.title, attrs: { 'data-c3ph-later-row': m.id } })));
|
|
234
309
|
}
|
|
235
310
|
|
|
236
311
|
function headLine() {
|
|
237
312
|
const d = now();
|
|
238
|
-
const n =
|
|
313
|
+
const n = waitRows().length;
|
|
239
314
|
head.replaceChildren(el('b', null, WD[d.getDay()] + ' ' + d.getDate() + ' ' + MO[d.getMonth()]),
|
|
240
|
-
el('span', { class: 'c3ph-hdr' }, n ? el('span', { class: 'c3ph-dotn', 'data-c3ph-count': String(n) }, c3ic('inbox', 'sm'), n + ' waiting') : null
|
|
241
|
-
button({ label: 'Full console', variant: 'ghost', size: 'sm', attrs: { 'data-c3ph-desk': '' }, onClick: () => ctx.onDesk && ctx.onDesk() })));
|
|
315
|
+
el('span', { class: 'c3ph-hdr' }, n ? el('span', { class: 'c3ph-dotn', 'data-c3ph-count': String(n) }, c3ic('inbox', 'sm'), n + ' waiting') : null));
|
|
242
316
|
}
|
|
243
317
|
|
|
244
318
|
const BANNER = { meeting: (m) => m.title + ' has started', before: (m, mo) => m.title + ' starts ' + inText(mo.minutes), after: (m) => m.title + ' just ended', gap: (m, mo) => (mo.minutes || 0) + ' free minutes', wrap: () => 'Time to wrap up', morning: () => 'Good morning' };
|
|
@@ -255,7 +329,8 @@ export function mountPhone(root, ctx = {}) {
|
|
|
255
329
|
S.kindKey = key;
|
|
256
330
|
host.dataset.moment = mo.kind;
|
|
257
331
|
headLine();
|
|
258
|
-
|
|
332
|
+
const st = recState();
|
|
333
|
+
body.replaceChildren(st.mode !== 'idle' ? liveCard(st, mo) : momentCard(mo), waitingCard(mo) || el('span'), laterList(mo) || el('span'));
|
|
259
334
|
}
|
|
260
335
|
|
|
261
336
|
const off1 = onBriefPage((j) => { S.page = j; draw(); });
|
|
@@ -138,7 +138,6 @@ function toggleMore() {
|
|
|
138
138
|
item('alert', 'Incident day', () => toggleIncident(), S.incident, S.demo ? { 'data-a': 'inc' } : { 'data-a': 'inc', disabled: true, title: 'No incident feed is connected, so this page shows no incident.' }),
|
|
139
139
|
item('spark', 'Agent playbooks', () => openPlaybooks(), false, { 'data-a': 'pbopen' }),
|
|
140
140
|
item('phone', 'Phone preview', () => togglePreview(), !!S.preview, { 'data-a': 'phone' }),
|
|
141
|
-
S.root.classList.contains('ph') && store.get('pmos.c3.phone') === 'desk' ? item('phone', 'Phone companion', () => { store.set('pmos.c3.phone', ''); syncPhone(); }, false, { 'data-a': 'phcomp' }) : null,
|
|
142
141
|
item(S.theme === 'dark' ? 'sun' : 'moon', S.theme === 'dark' ? 'Light theme' : 'Dark theme', () => setTheme(S.theme === 'dark' ? 'light' : 'dark')),
|
|
143
142
|
((b) => { b.firstChild.replaceWith(gearIcon('sm')); return b; })(item('info', 'Settings', () => showTab('settings', true), false, { 'data-a': 'settings' })),
|
|
144
143
|
item('term', S.dockOpen ? 'Fold the dock' : 'Open the dock', () => { S.dockOpen = !S.dockOpen; keepDock(); syncDock(); }, false, { 'data-a': 'dockt' }, 'Ctrl J'),
|
|
@@ -151,13 +150,14 @@ function toggleMore() {
|
|
|
151
150
|
document.addEventListener('keydown', S.menuOff); document.addEventListener('pointerdown', S.menuOff);
|
|
152
151
|
const first = S.menu.querySelector('.c3pg-mi:not([disabled])'); if (first) first.focus();
|
|
153
152
|
}
|
|
154
|
-
/* The phone companion (c3-phone.js): at
|
|
155
|
-
*
|
|
153
|
+
/* The phone companion (c3-phone.js): at 640px or less it fills the page and the desktop grid is hidden. There is no Full console:
|
|
154
|
+
* the old choice (pmos.c3.phone = 'desk', stored before 2026-10-13) is cleared so no phone stays on the desktop pages. */
|
|
156
155
|
function syncPhone() {
|
|
156
|
+
if (store.get('pmos.c3.phone')) store.set('pmos.c3.phone', '');
|
|
157
157
|
const want = phoneWanted(store);
|
|
158
158
|
if (want && !S.phone) {
|
|
159
159
|
S.root.classList.add('c3-phone-on');
|
|
160
|
-
S.phone = mountPhone(S.root, { ws: S.ws, demo: S.demo
|
|
160
|
+
S.phone = mountPhone(S.root, { ws: S.ws, demo: S.demo });
|
|
161
161
|
} else if (!want && S.phone) {
|
|
162
162
|
S.phone.dispose(); S.phone = null;
|
|
163
163
|
S.root.classList.remove('c3-phone-on');
|
|
@@ -607,7 +607,7 @@ async function boot() {
|
|
|
607
607
|
mountAmbient(root); // the ambient motion canvas, once, behind the grid
|
|
608
608
|
mountQuiet(root); // the quiet layout: two rows above the content, no teaching sentences (c3-quiet-pages)
|
|
609
609
|
syncPhone(); // at phone width the companion replaces the desktop frame (c3-phone-companion)
|
|
610
|
-
if (typeof matchMedia === 'function') matchMedia('(max-width:
|
|
610
|
+
if (typeof matchMedia === 'function') matchMedia('(max-width:640px)').addEventListener('change', syncPhone);
|
|
611
611
|
|
|
612
612
|
S.dock.setWorkspaces(S.workspaces, S.ws);
|
|
613
613
|
mountBell(S.bellHost, { go: (_, t) => goTarget(t) });
|