create-pmos 0.201.0 → 0.203.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.201.0",
3
+ "version": "0.203.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@2bd8a36
1
+ kernel-source: pmos@ab34930
2
2
  assembled: 2026-10-13
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-13T11:15:18Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T12:24:07Z"}
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-13T11:15:18Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T12:24:07Z"}
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-13T11:15:18Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T12:24:07Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -153,8 +153,9 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
153
153
  ok('R2 where canvas is unavailable the page still draws and raises no error', N.up && N.errors.length === 0 && await N.page.evaluate(() => document.querySelectorAll('canvas.c3amb').length === 0), N.errors.join(' | '));
154
154
  await N.ctx.close();
155
155
 
156
- /* phone width: no horizontal scroll */
157
- const M = await open({ viewport: { width: 400, height: 900 } });
156
+ /* phone width: no horizontal scroll. A phone shows the companion (c3-phone-test.js); this holds the desktop layout at that width
157
+ * (lib/c3-full-console.js). */
158
+ const M = await open({ viewport: { width: 400, height: 900 } }, null, () => { try { localStorage.setItem('pmos.c3.testDesk', 'desk'); } catch { /* no storage */ } });
158
159
  ok('0 at 400 wide the page does not scroll sideways', M.up && await M.page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), 'scrollWidth');
159
160
  ok('0 no page error (400 wide)', M.errors.length === 0, M.errors.join(' | '));
160
161
  await M.ctx.close();
@@ -1,9 +1,10 @@
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): c3-phone.js, its sheets c3-phone-detail.js and its demo notes c3-demo-phone.js.
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
+ * c3-boot.js and c3-phone-boot.js, which load the companion alone at phone width.
4
5
  * 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
6
  *
6
- * phone at 400 wide the companion shows and the desktop grid does not
7
+ * phone at 400 wide the companion shows and the desktop grid does not; only the companion's modules load
7
8
  * moments 08:12 morning; 10:10 before Checkout sync; 10:45 in the meeting with the Record offer
8
9
  * done Done on the waiting item records a write to /api/pm-items/done
9
10
  * tell text sent from the ask bar records a capture and clears the bar
@@ -73,6 +74,7 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
73
74
  let page = await open(pctx, '/c3.html?demo=1');
74
75
  ok('phone: the companion shows at 400 wide', await page.evaluate(shown, '[data-c3ph]'));
75
76
  ok('phone: the desktop grid is hidden', !(await page.evaluate(shown, '.c3grid')));
77
+ ok('phone: only the companion loads, not the desktop console', await page.evaluate(() => { const js = performance.getEntriesByType('resource').map((r) => r.name); return js.some((u) => /c3-phone-boot\.js/.test(u)) && !js.some((u) => /c3-shell\.js|calendar-views\.js|c3-initiatives\.js/.test(u)); }));
76
78
  ok('phone: no sideways scroll', await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
77
79
  ok('moments: 08:12 is morning', (await moment(page)) === 'morning', await moment(page));
78
80
  ok('phone: the ask bar and the red Record button are in the bottom bar', await page.evaluate(() => !!document.querySelector('[data-c3ph-foot] [data-c3ph-input]') && !!document.querySelector('[data-c3ph-foot] [data-c3ph-record]')));
@@ -223,6 +225,24 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
223
225
  ok('debrief: 11:25 is just after Checkout sync', (await moment(page)) === 'after', await moment(page));
224
226
  ok('debrief: the card shows the decisions and actions from the recording', await waitFor(page, () => { const c = document.querySelector('[data-c3ph-moment="after"]'); return c && c.dataset.c3phDebriefState === 'ready' && !!c.querySelector('[data-c3ph-debrief="decisions"]') && !!c.querySelector('[data-c3ph-debrief="actions"]') && /31 Oct/.test(c.textContent); }, null, 8000));
225
227
  await page.close();
228
+ /* ── the day's parts (version 7): one icon for the part of the day; working hours, the evening recap, the end of the day ── */
229
+ page = await open(pctx, '/c3.html?demo=1&at=15:30');
230
+ ok('day: the header shows one icon, for the current part of the day only', await page.evaluate(() => document.querySelectorAll('[data-c3ph-part]').length === 1 && document.querySelector('[data-c3ph-part]').dataset.c3phPart === 'work' && document.querySelector('[data-c3ph-part]').getAttribute('aria-label') === 'Working hours'));
231
+ ok('day: working hours show what the agent finished for you', await waitFor(page, () => !!document.querySelector('[data-c3ph-moment] [data-c3ph-fin="t-0000000000a1"]'), null, 8000));
232
+ await page.click('[data-c3ph-fin-open="t-0000000000a1"]');
233
+ ok('day: Read it opens the draft, and approving it waits for the desk', await waitFor(page, () => { const d = document.querySelector('[data-c3ph-draft="t-0000000000a1"]'); return !!d && /31 Oct/.test(d.textContent) && /on your computer/.test(d.textContent); }, null, 5000));
234
+ await page.close();
235
+ page = await open(pctx, '/c3.html?demo=1&at=17:40');
236
+ ok('day: 17:40 is the evening recap, with the evening icon', (await moment(page)) === 'recap' && await page.evaluate(() => document.querySelector('[data-c3ph-part]')?.dataset.c3phPart === 'evening'), await moment(page));
237
+ ok('day: the recap counts the day and lists what moved and what is still open', await waitFor(page, () => { const c = document.querySelector('[data-c3ph-moment="recap"]'); return !!c && c.querySelectorAll('[data-c3ph-stat]').length === 4 && /Checkout v2 ships on 31 Oct/.test(c.querySelector('[data-c3ph-moved]')?.textContent || '') && !!c.querySelector('[data-c3ph-open]'); }, null, 8000));
238
+ ok('day: the evening does not push the waiting card', await page.evaluate(() => !document.querySelector('[data-c3ph-waiting]')));
239
+ await page.close();
240
+ page = await open(pctx, '/c3.html?demo=1&at=21:30');
241
+ ok('day: 21:30 is the end of the day with tomorrow at a glance', await waitFor(page, () => { const c = document.querySelector('[data-c3ph-moment="night"]'); return !!c && /Fri 9 Oct/.test(c.textContent) && c.querySelectorAll('[data-c3ph-tomorrow-row]').length === 2 && !!c.querySelector('[data-c3ph-carried]'); }, null, 8000)
242
+ && await page.evaluate(() => document.querySelector('[data-c3ph-part]')?.dataset.c3phPart === 'night'));
243
+ await page.click('[data-c3ph-tomorrow-row="f2"]');
244
+ ok('day: a meeting of tomorrow opens its brief', await waitFor(page, () => !!document.querySelector('[data-c3ph-meeting="f2"]'), null, 5000));
245
+ await page.close();
226
246
  page = await open(pctx, '/c3.html?demo=1&at=17:10');
227
247
  ok('debrief: a meeting with no recording asks you to tell PMOS', await waitFor(page, () => { const c = document.querySelector('[data-c3ph-moment="after"]'); return c && c.dataset.c3phDebriefState === 'none' && !!c.querySelector('[data-c3ph-tellafter]'); }, null, 8000));
228
248
  await page.close();
@@ -11,7 +11,7 @@ When the console (`/`) is 640 pixels wide or less, for example on a phone, it sh
11
11
 
12
12
  ## The moment card
13
13
 
14
- The top card shows where you are in your day and why it is useful now. PMOS chooses the moment from today's meetings and the clock:
14
+ The top card shows where you are in your day and why it is useful now. PMOS chooses the moment from today's meetings and the clock. One icon before the date shows the part of the day: a sun for the morning, a bolt for working hours, a calendar for a meeting, a chart for the evening and a moon for the end of the day. Only the icon of the current part shows.
15
15
 
16
16
  | Moment | When | What the card shows |
17
17
  |---|---|---|
@@ -19,11 +19,15 @@ The top card shows where you are in your day and why it is useful now. PMOS choo
19
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
20
  | In the meeting | A meeting is in progress | **Record this meeting?** with **Record** and **Not this one**. Recording opens focus (see below). |
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
- | 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
- | Wrapping up | From 17:00, or after noon with no meeting left | What is still open and what waits for your computer |
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**. Under them, what the agent finished for you. |
23
+ | Finished for you | After noon, before 17:00, with no meeting near | The drafts that the agent finished today, newest first. **Read it** shows the draft. Approve a draft, or send it back with a note, on your computer: nothing is sent from the phone. Under them, the items that fit. |
24
+ | Evening recap | From 17:00 | How today went: the meetings, the decisions in their notes, the items you marked done and the agent's drafts. Then what moved, what is still open, and one thing for tonight with **Done** and **Tomorrow**. |
25
+ | End of day | From 20:00, and before 05:00 | Today in one line, the meetings of the next working day (tap one to see its brief), and how many items carry over |
24
26
 
25
27
  When the moment changes, a short message shows at the top for 6 seconds. The message shows only while the page is open. The companion does not send notifications.
26
28
 
29
+ In the evening and at the end of the day, the card does not show the things that wait on you one at a time. The number at the top right still opens them.
30
+
27
31
  ## Waiting on you
28
32
 
29
33
  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.
@@ -65,6 +69,10 @@ A phone always shows the companion. There is no way to the desktop pages on a ph
65
69
 
66
70
  The number of things that wait on you, at the top right, opens the list of all of them.
67
71
 
72
+ ## Loading
73
+
74
+ On a phone, PMOS loads only the companion, not the pages of the computer. This makes the first load faster on a phone connection. If you turn the screen so that it is wider than a phone, the page loads again and shows the pages of the computer.
75
+
68
76
  ## Limits
69
77
 
70
78
  - No notification on the lock screen, and no spoken brief.
@@ -50,6 +50,7 @@ const MODULES = Object.freeze([
50
50
  'routes-calendar-views', // console3 C2: Day/Week/Month/Quarter/Year cells, Gantt and the strategy lens (read-only)
51
51
  'routes-briefing', // console3 C1: the day-part Briefing cards from the signal engine (read-only)
52
52
  'routes-brief-page', // console3 Briefing as the brief: Today agenda and Needs you (read-only; writes go through routes-pm-items)
53
+ 'routes-phone-day', // phone companion: the agent's finished drafts, the evening recap and tomorrow at a glance (read-only)
53
54
  'routes-report-facts', // console3 D: GET /api/report-families/facts (read-only, real data only)
54
55
  'routes-records', // console3 E1a: ideas, hypotheses, decisions, KR metrics, bets (GET list; page-press add/edit/reading)
55
56
  'routes-doc-sheet', // console3 E1c: document sheet: revisions, edit with why, propose and accept by hash
@@ -164,7 +164,7 @@ function firstName(cwd) {
164
164
 
165
165
  module.exports = {
166
166
  prefixes: ['/api/brief-page'],
167
- build, todayAgenda, needsOf, evidenceFor, deferred, firstName,
167
+ build, todayAgenda, needsOf, evidenceFor, deferred, firstName, notesAdapter,
168
168
  handle: async (req, res, url, ctx) => {
169
169
  if (url.pathname !== '/api/brief-page') return false;
170
170
  if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+ /* routes-phone-day.js — GET /api/phone-day[?now=ISO] (initiative c3-phone-companion, PRD item 10: the phone follows the day).
3
+ *
4
+ * → { ok, date, finished: [task], recap: { meetings, decided, done, finished, moved: [row] }, tomorrow: { date, label, meetings: [meeting] } }
5
+ *
6
+ * READ ONLY, and built only from what PMOS already holds. Nothing is invented. The companion reads it for three moments:
7
+ * finished working hours, "Finished for you": the agent's tasks of today's day run (goals/<date>.run.json, lib/day-run.js) that have a
8
+ * draft. { id, title, summary, body, at, status, link, source }; status 'review' (waits for the PM's approval at the desk),
9
+ * 'done' (an auto task) or 'sent'. Newest first, at most 5.
10
+ * recap evening: how the day went. meetings = today's meetings that have ended; decided = the decisions in the final notes of today's
11
+ * meetings; done = the PM's own items marked done today; finished = the agent's drafts of today. moved = at most 5 rows of
12
+ * { kind: 'decision'|'done'|'agent', text, sub }: the decisions first, then the items done, then the drafts.
13
+ * tomorrow end of the day: the next working day (Monday to Friday) and its meetings, in the shape of GET /api/brief-page today[] rows
14
+ * (routes-brief-page.js todayAgenda), so the companion opens their briefs the same way.
15
+ * A private note is never read. A file that is missing or unreadable counts as empty. */
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const pmItems = require('./pm-items.js');
19
+ const { todayAgenda, notesAdapter } = require('./routes-brief-page.js');
20
+
21
+ const pad = (n) => String(n).padStart(2, '0');
22
+ const localDate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
23
+ const clip = (s, n) => { const t = String(s || '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
24
+ const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
25
+ const MO = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
26
+ const FINISHED = ['review', 'done', 'sent'];
27
+
28
+ /** The next working day after `now`, at local midnight. */
29
+ function nextWorkday(now) {
30
+ const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
31
+ while (d.getDay() === 0 || d.getDay() === 6) d.setDate(d.getDate() + 1);
32
+ return d;
33
+ }
34
+
35
+ /** The agent's finished tasks of a day run (lib/day-run.js readRun shape). */
36
+ function finishedOf(run) {
37
+ const tasks = run && Array.isArray(run.tasks) ? run.tasks : [];
38
+ return tasks.filter((t) => t && FINISHED.includes(t.status) && t.draft && t.draft.summary)
39
+ .map((t) => ({ id: t.id, title: clip(t.title, 200), summary: clip(t.draft.summary, 300), body: clip(t.draft.body, 1200), at: t.draft.at || null, status: t.status, link: t.draft.link || t.link || null, source: t.source || null }))
40
+ .sort((a, b) => String(b.at || '').localeCompare(String(a.at || '')));
41
+ }
42
+
43
+ /** deps: { events, items, run, notes: [note with final_notes], adapter } — notes are today's non-private notes, read in full. */
44
+ function build(query, deps = {}) {
45
+ const q = query || {};
46
+ let now = q.now ? new Date(q.now) : (deps.now ? new Date(deps.now) : new Date());
47
+ if (Number.isNaN(now.getTime())) now = new Date();
48
+ const today = localDate(now);
49
+ const events = deps.events || []; const items = deps.items || [];
50
+ const agenda = todayAgenda(events, items, now, deps.adapter || {});
51
+ const finished = finishedOf(deps.run);
52
+ const decisions = [];
53
+ for (const n of deps.notes || []) {
54
+ if (!n || n.private) continue;
55
+ const f = n.final_notes;
56
+ (f && Array.isArray(f.decisions) ? f.decisions : []).forEach((x) => { if (x && x.text) decisions.push({ kind: 'decision', text: clip(x.text, 200), sub: clip(n.title || (n.meeting && n.meeting.title) || 'A meeting', 80) }); });
57
+ }
58
+ const done = items.filter((i) => i && i.done && i.done_at && localDate(new Date(i.done_at)) === today);
59
+ const moved = [...decisions, ...done.map((i) => ({ kind: 'done', text: clip(i.title, 200), sub: 'Done by you' })),
60
+ ...finished.map((t) => ({ kind: 'agent', text: clip(t.title, 200), sub: t.status === 'review' ? 'Drafted by the agent; it waits for you' : 'Finished by the agent' }))].slice(0, 5);
61
+ const tm = nextWorkday(now);
62
+ return { status: 200, body: {
63
+ ok: true, date: today, finished,
64
+ recap: { meetings: agenda.filter((m) => Date.parse(m.end || m.start) <= now.getTime()).length, decided: decisions.length, done: done.length, finished: finished.length, moved },
65
+ tomorrow: { date: localDate(tm), label: WD[tm.getDay()] + ' ' + tm.getDate() + ' ' + MO[tm.getMonth()], meetings: todayAgenda(events, items, tm, deps.adapter || {}) },
66
+ } };
67
+ }
68
+
69
+ /** Today's non-private notes, read in full (their final notes hold the decisions). */
70
+ function todaysNotes(dir, today) {
71
+ let notes; try { notes = require('./notes.js'); } catch { return []; }
72
+ try {
73
+ const l = notes.list(dir);
74
+ const rows = l && l.ok ? l.notes.filter((n) => !n.private && localDate(new Date((n.meeting && n.meeting.start) || n.created_at)) === today) : [];
75
+ return rows.slice(0, 30).map((r) => { const one = notes.read(dir, r.id); return one && one.ok ? one.note : null; }).filter(Boolean);
76
+ } catch { return []; }
77
+ }
78
+
79
+ function readRun(dir, date) {
80
+ try { return require('./day-run.js').readRun(JSON.parse(fs.readFileSync(path.join(dir, 'goals', date + '.run.json'), 'utf8')), date); } catch { return null; }
81
+ }
82
+
83
+ module.exports = {
84
+ prefixes: ['/api/phone-day'],
85
+ build, finishedOf, nextWorkday,
86
+ handle: async (req, res, url, ctx) => {
87
+ if (url.pathname !== '/api/phone-day') return false;
88
+ if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
89
+ let now = url.searchParams.get('now') ? new Date(url.searchParams.get('now')) : new Date();
90
+ if (Number.isNaN(now.getTime())) now = new Date();
91
+ try {
92
+ const dir = ctx.runtimeDir;
93
+ const today = localDate(now);
94
+ const events = dir ? require('./calendar-events.js').readAll(dir, { now: now.getTime() }) : [];
95
+ const items = dir ? pmItems.fold(pmItems.readLines(path.join(dir, 'pm-items', 'items.jsonl'))) : [];
96
+ const out = build({ now: now.toISOString() }, { events, items, run: dir ? readRun(dir, today) : null, notes: dir ? todaysNotes(dir, today) : [], adapter: dir ? notesAdapter(dir) : {} });
97
+ ctx.json(res, out.body, out.status);
98
+ } catch (e) {
99
+ ctx.json(res, { ok: false, error: 'your day could not be read: ' + (e && e.message ? e.message : String(e)) }, 500);
100
+ }
101
+ return true;
102
+ },
103
+ };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.201.0",
3
+ "version": "0.203.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.201.0",
9
+ "version": "0.203.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.201.0",
3
+ "version": "0.203.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -15,7 +15,7 @@
15
15
  <meta name="apple-mobile-web-app-status-bar-style" content="black" />
16
16
  <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
17
17
  <!-- The console3 shell (header, tabs, three columns, dock), served as the default page at / (and still at /c3.html). It
18
- composes the real views from pm/; the older shell (index.html) is served at /classic.html. Tokens and fonts are shared, so light and dark both resolve. pm/c3-shell.js sets the
18
+ composes the real views from pm/; the older shell (index.html) is served at /classic.html. Tokens and fonts are shared, so light and dark both resolve. pm/c3-boot.js loads the phone companion alone at phone width (c3-phone-boot.js) and the desktop console (pm/c3-shell.js) otherwise; the shell sets the
19
19
  theme attribute (default light, like the mockup). The nonce meta is filled by the server the way it is for /, so a press
20
20
  from a view (Add, Accept, Allow) carries it. -->
21
21
  <link rel="preload" href="/vendor/ibm-plex-mono-400.woff2" as="font" type="font/woff2" crossorigin />
@@ -30,6 +30,6 @@
30
30
  <body>
31
31
  <div id="c3-root" class="c3 c3pg" data-pm-theme="light"></div>
32
32
  <noscript>This page needs JavaScript.</noscript>
33
- <script type="module" src="/pm/c3-shell.js"></script>
33
+ <script type="module" src="/pm/c3-boot.js"></script>
34
34
  </body>
35
35
  </html>
@@ -0,0 +1,10 @@
1
+ /* pm/c3-boot.js — the first script of /c3.html: it picks which page to load (initiative c3-phone-companion).
2
+ * At phone width the companion loads alone (c3-phone-boot.js): about 30 modules instead of the desktop console's 100 and its
3
+ * reads of every tab. The desktop console (c3-shell.js) loads only on a wider screen. Turning a phone to a wide screen reloads
4
+ * the page, so the desktop console starts from the beginning. pmos.c3.testDesk is for the browser suites that test the
5
+ * desktop layout at phone width (lib/c3-full-console.js), as in c3-phone.js phoneWanted. */
6
+ const narrow = typeof matchMedia === 'function' && matchMedia('(max-width:640px)').matches;
7
+ let desk = false;
8
+ try { desk = localStorage.getItem('pmos.c3.testDesk') === 'desk'; } catch { /* no storage */ }
9
+ if (narrow && !desk) import('./c3-phone-boot.js');
10
+ else import('./c3-shell.js');
@@ -28,3 +28,21 @@ const LIVE = {
28
28
  ],
29
29
  };
30
30
  export const demoLive = (st) => ({ ok: true, note: { ...LIVE, id: st.id, title: st.title } });
31
+
32
+ /* GET /api/phone-day for the demo day (Thu 8 Oct 2026; lib/routes-phone-day.js): the agent's drafts of the day, the evening recap and
33
+ * Friday at a glance. The meeting rows of tomorrow have the shape of GET /api/brief-page today[]. */
34
+ const at = (d, hh, mm) => new Date(2026, 9, d, hh, mm).toISOString();
35
+ export const demoDay = () => ({
36
+ ok: true, date: '2026-10-08',
37
+ finished: [
38
+ { id: 't-0000000000a1', title: 'Reply to Priya on the Checkout v2 date', summary: 'A reply that moves v2 to 31 Oct and keeps beta on 30 Oct, with the reason in one line.', body: 'Hi Priya,\nCheckout v2 moves to 31 Oct: the sandbox keys arrive two days late.\nBeta stays on 30 Oct for the two partners already set up.', at: at(8, 12, 52), status: 'review', link: null, source: 'my-work' },
39
+ { id: 't-0000000000a2', title: 'Weekly status for leadership', summary: 'Green on Checkout v2 with the new date; amber on Returns, where the interview slipped a week.', body: '', at: at(8, 12, 40), status: 'done', link: null, source: 'brief' },
40
+ ],
41
+ recap: { meetings: 5, decided: 2, done: 1, finished: 2,
42
+ moved: [{ kind: 'decision', text: 'Checkout v2 ships on 31 Oct, not 24 Oct', sub: 'Checkout sync' }, { kind: 'decision', text: 'Beta stays on 30 Oct for the two partners already set up', sub: 'Checkout sync' },
43
+ { kind: 'done', text: 'Send the interview guide to Marta', sub: 'Done by you' }, { kind: 'agent', text: 'Reply to Priya on the Checkout v2 date', sub: 'Drafted by the agent; it waits for you' }] },
44
+ tomorrow: { date: '2026-10-09', label: 'Fri 9 Oct', meetings: [
45
+ { id: 'f1', title: 'Standup', start: at(9, 9, 30), end: at(9, 10, 0), minutes: 30, people: ['Dara', 'Lee', 'Sam'], initiative: 'Checkout v2', prepared: [], before: [], state: 'later' },
46
+ { id: 'f2', title: 'Returns review', start: at(9, 11, 0), end: at(9, 12, 0), minutes: 60, people: ['Marta', 'Priya'], initiative: 'Returns experience', prepared: [{ kind: 'prep', text: 'The interview notes, and the two options for the refund flow.' }], before: [], state: 'later' },
47
+ ] },
48
+ });
@@ -0,0 +1,47 @@
1
+ /* pm/c3-phone-boot.js — the phone companion on its own (initiative c3-phone-companion). c3-boot.js loads it at phone width
2
+ * instead of the desktop console, so a phone reads only what the companion shows. It does what c3-shell.js boot() does for
3
+ * the companion: the demo, the workspace, the theme, then mountPhone. A wider screen reloads into the desktop console. */
4
+ import { pmGet } from './http.js';
5
+ import { ensureC3Css, c3Phone } from './pm-work-view.js';
6
+ import { mountPhone } from './c3-phone.js';
7
+
8
+ const q = new URLSearchParams(location.search);
9
+ const DEMO = q.get('demo') === '1';
10
+ const store = { get(k) { try { return localStorage.getItem(k); } catch { return null; } } };
11
+
12
+ /* A GET to /api/ with no ws is sent for this page's workspace, as c3-shell.js withWs() does. */
13
+ function withWs(ws) {
14
+ const f = window.fetch.bind(window);
15
+ window.fetch = (input, init) => {
16
+ try {
17
+ if (typeof input === 'string' && ws && input.startsWith('/api/') && !/[?&]ws=/.test(input) && (!init || !init.method || init.method === 'GET')) input += (input.includes('?') ? '&' : '?') + 'ws=' + encodeURIComponent(ws);
18
+ } catch { /* leave the request as it is */ }
19
+ return f(input, init);
20
+ };
21
+ }
22
+
23
+ async function boot() {
24
+ const root = document.getElementById('c3-root');
25
+ let ws = q.get('ws') || '';
26
+ if (DEMO) { const m = await import('./c3-demo.js'); m.installDemo(); if (window.__c3demoReady) await window.__c3demoReady; if (!ws) ws = m.DEMO_WS; root.setAttribute('data-c3-demo', '1'); }
27
+ const pref = q.get('theme') || store.get('pmos.c3.theme') || 'light';
28
+ const theme = pref === 'dark' || (pref === 'system' && typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
29
+ document.documentElement.setAttribute('data-pm-page-theme', theme);
30
+ root.setAttribute('data-pm-theme', theme);
31
+ try { if (localStorage.getItem('pmos.c3.phone')) localStorage.setItem('pmos.c3.phone', ''); } catch { /* no storage */ } // the dropped Full console choice
32
+ ensureC3Css();
33
+ c3Phone(root);
34
+ if (!DEMO || !ws) {
35
+ const list = await pmGet('/api/workspaces').then((j) => (j && j.workspaces) || []).catch(() => []);
36
+ if (!ws || !list.some((w) => w.id === ws)) ws = (list[0] && list[0].id) || ws;
37
+ }
38
+ withWs(ws);
39
+ root.replaceChildren();
40
+ root.classList.add('c3-phone-on');
41
+ mountPhone(root, { ws, demo: DEMO });
42
+ if (q.get('focus') || q.get('fdate')) import('./c3-focus.js').then((m) => m.openFocus({ event: { id: q.get('focus') || '' }, date: q.get('fdate') || '', phone: true })).catch(() => { /* the focus page is not available */ });
43
+ const mq = matchMedia('(max-width:640px)');
44
+ mq.addEventListener('change', () => { if (!mq.matches) location.reload(); });
45
+ }
46
+
47
+ boot().catch((e) => { const r = document.getElementById('c3-root'); if (r) r.textContent = 'PMOS could not start: ' + String((e && e.message) || e); });
@@ -6,6 +6,8 @@
6
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
7
  * Done and Later today when the row is the PM's own Work item. opts: { onDone(r), onLater(r) }
8
8
  * openAll(rows, opts) every row that waits, as a list; a press opens that row (openNeed). opts as openNeed.
9
+ * openDraft(t) a draft the agent finished today (GET /api/phone-day finished): what it is, its summary, its text and its
10
+ * link. Approving or revising it waits for the desk, where the whole draft and its sources are.
9
11
  * Nothing here writes: the buttons call back into c3-phone.js, which owns the writes and their Undo. */
10
12
  import { el } from './dom.js';
11
13
  import { button, row } from './primitives.js';
@@ -81,3 +83,13 @@ export function openAll(rows, opts = {}) {
81
83
  ...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
84
  return openSheet({ title: rows.length + (rows.length === 1 ? ' thing waits on you' : ' things wait on you'), icon: 'inbox', body: list });
83
85
  }
86
+
87
+ export function openDraft(t) {
88
+ const body = el('div', { class: 'c3ph-sheetbody', 'data-c3ph-draft': t.id },
89
+ el('span', { class: 'c3ph-step' }, (t.status === 'review' ? 'Waits for you' : t.status === 'sent' ? 'Sent' : 'Finished').toUpperCase() + (t.at ? ' · ' + hm(t.at) : '')),
90
+ line(t.summary),
91
+ ...String(t.body || '').split(/\n+/).map((x) => x.trim()).filter(Boolean).slice(0, 12).map((x) => el('div', { class: 'c3ph-prep' }, x)),
92
+ t.link ? el('div', { class: 'chips2' }, link(t.link, 'Open the source')) : null,
93
+ line(t.status === 'review' ? 'Approve it, or send it back with a note, on your computer. Nothing is sent from the phone.' : 'Nothing more is needed from you.'));
94
+ return openSheet({ title: t.title, icon: 'file-text', body });
95
+ }
@@ -7,8 +7,12 @@
7
7
  * 'after' a meeting ended less than AFTER_MIN minutes ago, nothing under way meeting = the one that ended
8
8
  * 'gap' a free stretch of GAP_MIN minutes or more before the next meeting minutes = free minutes, fits = items that fit
9
9
  * 'morning' before noon with no meeting under way or near next = the next meeting, or null
10
- * 'wrap' from WRAP_HOUR on, or no meeting left today after noon next = null
10
+ * 'work' working hours after noon with no meeting near next = the next meeting, or null
11
+ * 'recap' from WRAP_HOUR on: the evening recap of the day next = null
12
+ * 'night' from NIGHT_HOUR on, or before EARLY_HOUR: tomorrow at a glance next = null
11
13
  * later the meetings still to come after the one the moment is about (time order), for "I'll bring up later".
14
+ * part the part of the day the moment belongs to (PART_OF), drawn as one icon in the header:
15
+ * morning · work (gap, work) · meeting (before, meeting, after) · evening (recap) · night
12
16
  * fitsIn(needs, minutes) the own items worth doing in a free stretch: overdue first (most late first), then due today; at most two.
13
17
  * The meeting rows are the brief page's today[] rows: { id, title, start, end, people, initiative, prepared, before }. */
14
18
 
@@ -16,6 +20,9 @@ export const BEFORE_MIN = 30;
16
20
  export const AFTER_MIN = 20;
17
21
  export const GAP_MIN = 15;
18
22
  export const WRAP_HOUR = 17;
23
+ export const NIGHT_HOUR = 20;
24
+ export const EARLY_HOUR = 5;
25
+ export const PART_OF = Object.freeze({ morning: 'morning', gap: 'work', work: 'work', before: 'meeting', meeting: 'meeting', after: 'meeting', recap: 'evening', night: 'night' });
19
26
 
20
27
  const ms = (iso) => Date.parse(iso);
21
28
  const ok = (m) => m && m.id && !Number.isNaN(ms(m.start)) && !Number.isNaN(ms(m.end || m.start));
@@ -27,7 +34,12 @@ export function fitsIn(needs, minutes) {
27
34
  return own.slice(0, 2);
28
35
  }
29
36
 
30
- export function momentOf({ today, needs, now } = {}) {
37
+ export function momentOf(args = {}) {
38
+ const mo = kindOf(args);
39
+ return { ...mo, part: PART_OF[mo.kind] };
40
+ }
41
+
42
+ function kindOf({ today, needs, now } = {}) {
31
43
  const t = (now instanceof Date ? now : new Date(now || Date.now())).getTime();
32
44
  const hour = new Date(t).getHours();
33
45
  const rows = (today || []).filter(ok).slice().sort((a, b) => ms(a.start) - ms(b.start));
@@ -41,10 +53,11 @@ export function momentOf({ today, needs, now } = {}) {
41
53
  if (next && until <= BEFORE_MIN) return { kind: 'before', meeting: next, minutes: until, later: laterThan(next) };
42
54
  const ended = rows.filter((m) => end(m) <= t && t - end(m) < AFTER_MIN * 60000).pop();
43
55
  if (ended) return { kind: 'after', meeting: ended, later: upcoming };
44
- if (hour >= WRAP_HOUR || (!next && hour >= 12)) return { kind: 'wrap', next: null, later: upcoming };
56
+ if (hour >= NIGHT_HOUR || hour < EARLY_HOUR) return { kind: 'night', next: null, later: upcoming };
57
+ if (hour >= WRAP_HOUR) return { kind: 'recap', next: null, later: upcoming };
45
58
  if (next && until >= GAP_MIN && rows.some((m) => end(m) <= t)) return { kind: 'gap', next, minutes: until, fits: fitsIn(needs, until), later: upcoming };
46
59
  if (hour < 12) return { kind: 'morning', next, later: upcoming };
47
- return { kind: 'gap', next, minutes: until, fits: fitsIn(needs, until || 0), later: upcoming };
60
+ return { kind: 'work', next, minutes: until, fits: fitsIn(needs, until === null ? 120 : until), later: upcoming };
48
61
  }
49
62
 
50
63
  /** "in 25 min", "in 1 h 5 min" */
@@ -11,6 +11,9 @@
11
11
  .c3 .c3ph-big,.c3 .c3ph-tt,.c3 .c3ph-line,.c3 .c3ph-hd b{overflow-wrap:anywhere}
12
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}
13
13
  .c3 .c3ph-hd b{font-size:var(--fs-4);flex:1;min-width:0}
14
+ /* the part of the day: one icon before the date, only the current part (version 7) */
15
+ .c3 .c3ph-ttl{display:flex;align-items:center;gap:8px}
16
+ .c3 .c3ph-part{display:inline-grid;place-items:center;flex:none;width:28px;height:28px;border-radius:8px;background:color-mix(in oklab,var(--acc) 14%,transparent);color:var(--acc)}
14
17
  .c3 .c3ph-hdr{display:flex;align-items:center;gap:8px;flex:none}
15
18
  .c3 .c3ph-dotn{display:inline-flex;align-items:center;gap:4px;font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
16
19
  .c3 button.c3ph-dotn{height:auto;min-height:32px;padding:4px 8px;cursor:pointer}
@@ -51,3 +54,7 @@
51
54
  .c3 .c3ph-banner{display:grid;gap:2px;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:10px 12px;margin-bottom:8px;box-shadow:var(--sh3);cursor:pointer}
52
55
  /* the focus page in its compact form fills the phone */
53
56
  .c3.c3-phone-on .c3focus.fc-pop{position:fixed;inset:0;z-index:20}
57
+ .c3 .c3ph-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}
58
+ .c3 .c3ph-stats div{display:grid;gap:2px}
59
+ .c3 .c3ph-stats b{font-size:20px;font-variant-numeric:tabular-nums}
60
+ .c3 .c3ph-stats span{font-size:11px;color:var(--muted-foreground);line-height:1.25}
@@ -8,7 +8,13 @@
8
8
  * actions and questions picked out so far (dest-notes.js partsOf), as the desktop note shows them
9
9
  * before a meeting: when it starts, who, what is prepared, the "before you go" items with Done
10
10
  * just ended: tell PMOS what came out of it a free gap: the own items that fit, with Done and Later today
11
- * morning: the next meeting and how much waits wrap: what is still open, what waits for the desk
11
+ * morning: the next meeting and how much waits
12
+ * working hours, and a free gap: "Finished for you", the agent's drafts of today (GET /api/phone-day finished), each opens
13
+ * to read (c3-phone-detail.js openDraft); approving one waits for the desk
14
+ * evening (recap, from 17:00): how today went (GET /api/phone-day recap), what moved, what is still open, one thing tonight
15
+ * end of day (night, from 20:00): today in one line, the next working day's meetings (GET /api/phone-day tomorrow), what carries over
16
+ * part icon one icon before the date says the part of the day (c3-phone-model.js PART_OF): morning, working hours, a meeting,
17
+ * evening or the end of the day. Only the current one shows (Wawan, 2026-10-13).
12
18
  * 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
19
  * GET /api/briefing (assigned Jira and WBS items, Work items, morning brief items, decisions), one at a time, with See all.
14
20
  * An own Work item has Done (POST /api/pm-items/done, Undo: /reopen) and Later today (POST /api/pm-items/notyet); any other row
@@ -32,7 +38,7 @@ import { onBriefPage, loadBriefPage } from './c3-brief-data.js';
32
38
  import { onBriefing, loadBriefing } from './shell-ticker.js';
33
39
  import { toast } from './c3-toast.js';
34
40
  import { momentOf, inText } from './c3-phone-model.js';
35
- import { openMeeting, openNeed, openAll } from './c3-phone-detail.js';
41
+ import { openMeeting, openNeed, openAll, openDraft } from './c3-phone-detail.js';
36
42
  import { cardView } from './briefing-model.js';
37
43
  import { row } from './primitives.js';
38
44
 
@@ -43,6 +49,8 @@ const first = (n) => String(n || '').trim().split(/\s+/)[0];
43
49
  const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
44
50
  const MO = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
45
51
  const LIVE_MS = 5000; // how often the note being recorded is read again
52
+ /* the one icon of the part of the day (c3-phone-model.js PART_OF), and what a screen reader says for it */
53
+ const PART = { morning: ['sun', 'Morning'], work: ['bolt', 'Working hours'], meeting: ['cal', 'Meeting'], evening: ['report', 'Evening'], night: ['moon', 'End of day'] };
46
54
  const clockOf = (msec) => { const s = Math.max(0, Math.floor(msec / 1000)); return Math.floor(s / 60) + ':' + p2(s % 60); };
47
55
 
48
56
  function ensureCss() {
@@ -63,7 +71,7 @@ export function mountPhone(root, ctx = {}) {
63
71
  const q = new URLSearchParams(location.search);
64
72
  const at = demo && /^\d{1,2}:\d{2}$/.test(q.get('at') || '') ? q.get('at').split(':').map(Number) : null;
65
73
  const now = () => { const d = new Date(); if (at) d.setHours(at[0], at[1], 0, 0); return d; };
66
- const S = { page: null, briefing: null, gone: new Set(), skipped: new Set(), noRec: new Set(), debrief: {}, kindKey: '', banner: null, rec: null, recKey: '' };
74
+ const S = { page: null, briefing: null, day: null, gone: new Set(), skipped: new Set(), noRec: new Set(), debrief: {}, kindKey: '', banner: null, rec: null, recKey: '' };
67
75
 
68
76
  const host = el('div', { class: 'c3ph', 'data-c3ph': '', role: 'main', 'aria-label': 'PMOS companion' });
69
77
  const head = el('div', { class: 'c3ph-hd' });
@@ -74,7 +82,7 @@ export function mountPhone(root, ctx = {}) {
74
82
  iconButton({ icon: 'mic', ariaLabel: 'Dictate', size: 'sm', attrs: { 'data-c3ph-dictate': '' }, onClick: () => { input.focus(); toast('Use the mic on your keyboard to talk. PMOS files what you say.'); } }));
75
83
  const recBtn = button({ label: '', variant: 'outline', attrs: { class: 'c3ph-recdot', 'data-c3ph-record': '', 'aria-label': 'Record a conversation or meeting', title: 'Record a conversation or meeting' }, onClick: () => startRec(null) });
76
84
  recBtn.replaceChildren(el('span', { class: 'c3ph-reddot', 'aria-hidden': 'true' }));
77
- const foot = el('div', { class: 'c3ph-ft', 'data-c3ph-foot': '' });
85
+ const foot = el('div', { class: 'c3ph-ft', 'data-c3ph-foot': '' }, form, recBtn); // shown at once; syncRec swaps in the recording bar
78
86
  host.append(head, bannerHost, body, foot);
79
87
  root.append(host);
80
88
 
@@ -294,8 +302,19 @@ export function mountPhone(root, ctx = {}) {
294
302
  const fits = (mo.fits || []).filter((n) => !S.gone.has(n.id));
295
303
  return card(true, { 'data-c3ph-moment': 'gap' }, why(mo.next ? 'Until ' + mo.next.title + ' at ' + hm(mo.next.start) : 'No more meetings today'),
296
304
  big(mo.minutes ? mo.minutes + ' free minutes' : 'A free stretch'),
297
- ...(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.')]));
305
+ ...(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.')]),
306
+ ...finishedRows(2));
298
307
  }
308
+ if (mo.kind === 'work') {
309
+ const fin = finished();
310
+ const fits = (mo.fits || []).filter((n) => !S.gone.has(n.id));
311
+ return card(true, { 'data-c3ph-moment': 'work' }, why(mo.next ? 'Next: ' + mo.next.title + ' at ' + hm(mo.next.start) : 'No more meetings today'),
312
+ big(fin.length ? 'Finished for you' : 'Nothing new from the agent'),
313
+ ...(fin.length ? finishedRows(3) : [line(S.day ? 'When the agent finishes a draft today, it shows here.' : 'Reading what the agent finished.')]),
314
+ ...fits.map((n) => el('div', { class: 'c3ph-fit', 'data-c3ph-fit': n.id }, ...itemCard(n, false))));
315
+ }
316
+ if (mo.kind === 'recap') return recapCard();
317
+ if (mo.kind === 'night') return nightCard();
299
318
  if (mo.kind === 'morning') {
300
319
  const w = waitRows(); const late = w.filter((r) => /overdue|late/i.test(r.label)).length;
301
320
  return card(true, { 'data-c3ph-moment': 'morning' }, why('Your day'), big(mo.next ? mo.next.title + ' at ' + hm(mo.next.start) : 'No meetings today'),
@@ -303,12 +322,51 @@ export function mountPhone(root, ctx = {}) {
303
322
  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(' ')),
304
323
  mo.next ? acts(btn('The brief of this meeting', 'outline', 'calendar', { 'data-c3ph-meeting-open': mo.next.id }, () => meetingSheet(mo.next))) : null);
305
324
  }
306
- const o = waitRows().filter((r) => r.pm);
307
- return card(true, { 'data-c3ph-moment': 'wrap' }, why('Wrapping up'), big(o.length ? o.length + ' still open today' : 'Nothing left for today'),
308
- line(decisions() ? decisions() + (decisions() === 1 ? ' decision waits' : ' decisions wait') + ' for your desk. It opens first on your Briefing.' : 'Nothing waits for your desk.'));
325
+ return recapCard();
326
+ }
327
+
328
+ /* ── the agent's drafts of today (GET /api/phone-day finished) ── */
329
+ const finished = () => (S.day && S.day.ok !== false && S.day.finished) || [];
330
+ function finishedRows(max) {
331
+ return finished().slice(0, max).map((t) => el('div', { class: 'c3ph-fit', 'data-c3ph-fin': t.id },
332
+ el('span', { class: 'c3ph-step' }, (t.status === 'review' ? 'WAITS FOR YOU' : t.status === 'sent' ? 'SENT' : 'FINISHED') + (t.at ? ' · ' + hm(t.at) : '')),
333
+ el('b', { class: 'c3ph-tt' }, t.title), line(t.summary),
334
+ acts(btn('Read it', 'outline', 'file-text', { 'data-c3ph-fin-open': t.id }, () => openDraft(t)))));
335
+ }
336
+ const stat = (n, label, key) => el('div', { 'data-c3ph-stat': key }, el('b', null, String(n || 0)), el('span', null, label));
337
+ /* evening: how today went. Never empty: the numbers show even when they are all 0. */
338
+ function recapCard() {
339
+ const r = (S.day && S.day.ok !== false && S.day.recap) || null;
340
+ const open = waitRows().filter((x) => x.pm);
341
+ const one = open.find((x) => x.raw && (x.raw.kind === 'overdue' || x.raw.kind === 'due-today')) || null;
342
+ return card(true, { 'data-c3ph-moment': 'recap' }, why('Evening recap'), big('How today went'),
343
+ r ? el('div', { class: 'c3ph-stats', 'data-c3ph-stats': '' }, stat(r.meetings, 'meetings', 'meetings'), stat(r.decided, 'decided in meetings', 'decided'),
344
+ stat(r.done, 'done by you', 'done'), stat(r.finished, 'finished by the agent', 'finished')) : line(S.day ? 'Today could not be read.' : 'Reading your day.'),
345
+ r ? el('div', { class: 'c3ph-fit', 'data-c3ph-moved': String(r.moved.length) }, el('b', null, 'What moved'),
346
+ ...(r.moved.length ? r.moved.map((x) => line(x.text + (x.sub ? ' · ' + x.sub : ''))) : [line('Nothing was recorded as moved today.')])) : null,
347
+ el('div', { class: 'c3ph-fit', 'data-c3ph-open': String(open.length) }, el('b', null, 'Still open · ' + open.length),
348
+ ...open.slice(0, 3).map((x) => line(x.headline)),
349
+ line(open.length ? 'They come first on tomorrow\'s Briefing.' : 'Nothing of yours is still open.')),
350
+ one ? el('div', { class: 'c3ph-fit', 'data-c3ph-tonight': one.id }, el('b', null, 'One thing tonight, if you have a minute'), el('span', { class: 'c3ph-tt' }, one.headline),
351
+ acts(btn('Done', 'outline', 'check', { 'data-c3ph-done': one.id }, () => itemDone(one)), btn('Tomorrow', 'ghost', 'clock', { 'data-c3ph-tomorrow': one.id }, () => itemLater(one, 'tomorrow')))) : null);
352
+ }
353
+ /* end of the day: today in one line, the next working day at a glance, what carries over */
354
+ function nightCard() {
355
+ const d = S.day && S.day.ok !== false ? S.day : null;
356
+ const r = d && d.recap; const tm = d && d.tomorrow;
357
+ const open = waitRows().filter((x) => x.pm).length;
358
+ const n = (k, one, many) => (r[k] === 1 ? '1 ' + one : (r[k] || 0) + ' ' + many);
359
+ return card(true, { 'data-c3ph-moment': 'night' }, why('End of day'), big(tm ? 'Tomorrow, ' + tm.label : 'Tomorrow, at a glance'),
360
+ r ? line('Today: ' + [n('meetings', 'meeting', 'meetings'), n('decided', 'decision', 'decisions'), n('done', 'item done', 'items done'), n('finished', 'draft by the agent', 'drafts by the agent')].join(', ') + '.', { 'data-c3ph-today-line': '' }) : null,
361
+ tm ? el('div', { class: 'c3ph-fit', 'data-c3ph-tomorrow-list': String(tm.meetings.length) }, el('b', null, tm.meetings.length ? tm.meetings.length + (tm.meetings.length === 1 ? ' meeting' : ' meetings') : 'No meetings'),
362
+ ...tm.meetings.slice(0, 5).map((m) => row({ title: m.title, meta: [hm(m.start), who(m)], compact: true, onOpen: () => meetingSheet(m), openLabel: 'Open ' + m.title, attrs: { 'data-c3ph-tomorrow-row': m.id } })))
363
+ : line(S.day ? 'Tomorrow could not be read.' : 'Reading tomorrow.'),
364
+ line(open === 0 ? 'Nothing carries over.' : open === 1 ? '1 item carries over. It opens first on tomorrow\'s Briefing.' : open + ' items carry over. They open first on tomorrow\'s Briefing.', { 'data-c3ph-carried': String(open) }),
365
+ el('div', { class: 'c3ph-prep' }, c3ic('moon', 'sm'), 'Nothing needs you tonight.'));
309
366
  }
310
367
 
311
368
  function waitingCard(mo) {
369
+ if (mo.kind === 'recap' || mo.kind === 'night') return null; // the evening shows what is still open in the card; the count stays in the header
312
370
  const skipIds = new Set(((mo.kind === 'gap' && mo.fits) || []).map((n) => n.id));
313
371
  const all = waitRows().filter((r) => !skipIds.has(r.id));
314
372
  const list = all.filter((r) => !S.skipped.has(r.id));
@@ -328,14 +386,16 @@ export function mountPhone(root, ctx = {}) {
328
386
  ...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 } })));
329
387
  }
330
388
 
331
- function headLine() {
389
+ function headLine(mo) {
332
390
  const d = now();
333
391
  const n = waitRows().length;
334
- head.replaceChildren(el('b', null, WD[d.getDay()] + ' ' + d.getDate() + ' ' + MO[d.getMonth()]),
392
+ const pt = mo && PART[mo.part];
393
+ head.replaceChildren(el('b', { class: 'c3ph-ttl' }, pt ? el('span', { class: 'c3ph-part', role: 'img', 'data-c3ph-part': mo.part, title: pt[1], 'aria-label': pt[1] }, c3ic(pt[0], 'sm')) : null,
394
+ el('span', null, WD[d.getDay()] + ' ' + d.getDate() + ' ' + MO[d.getMonth()])),
335
395
  el('span', { class: 'c3ph-hdr' }, n ? button({ label: [c3ic('inbox', 'sm'), n + ' waiting'], variant: 'ghost', size: 'sm', attrs: { class: 'c3ph-dotn', 'data-c3ph-count': String(n), 'aria-label': n + ' waiting on you. See them all.' }, onClick: () => openAll(waitRows(), needOpts) }) : null));
336
396
  }
337
397
 
338
- 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' };
398
+ 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', work: () => 'Working hours', recap: () => 'Your evening recap', night: () => 'Tomorrow, at a glance', morning: () => 'Good morning' };
339
399
  function draw() {
340
400
  if (!S.page) { body.replaceChildren(card(false, { 'data-c3ph-state': 'loading' }, line('Reading your day.'))); headLine(); return; }
341
401
  if (S.page.ok === false) { body.replaceChildren(card(false, { 'data-c3ph-state': 'error' }, line('Your day could not be read: ' + (S.page.error || 'unreadable')))); headLine(); return; }
@@ -348,17 +408,20 @@ export function mountPhone(root, ctx = {}) {
348
408
  }
349
409
  S.kindKey = key;
350
410
  host.dataset.moment = mo.kind;
351
- headLine();
411
+ headLine(mo);
352
412
  const st = recState();
353
413
  body.replaceChildren(st.mode !== 'idle' ? liveCard(st, mo) : momentCard(mo), waitingCard(mo) || el('span'), laterList(mo) || el('span'));
354
414
  }
355
415
 
356
416
  const off1 = onBriefPage((j) => { S.page = j; draw(); });
357
417
  const off2 = onBriefing((b) => { S.briefing = b; if (S.page) draw(); });
358
- loadBriefPage(); loadBriefing();
418
+ /* the agent's drafts, the recap and tomorrow (GET /api/phone-day): read now and every 5 minutes */
419
+ const loadDay = () => (demo ? import('./c3-demo-phone.js').then((m) => m.demoDay()) : pmGet('/api/phone-day'))
420
+ .then((j) => { S.day = j || { ok: false }; if (S.page) draw(); }, () => { S.day = { ok: false }; if (S.page) draw(); });
421
+ loadBriefPage(); loadBriefing(); loadDay();
359
422
  draw();
360
423
  const t1 = setInterval(syncRec, 1000);
361
424
  const t2 = setInterval(draw, 60000);
362
- const t3 = setInterval(() => loadBriefPage(), 5 * 60000);
425
+ const t3 = setInterval(() => { loadBriefPage(); loadDay(); }, 5 * 60000);
363
426
  return { host, refresh: draw, dispose() { clearInterval(t1); clearInterval(t2); clearInterval(t3); off1(); off2(); host.remove(); } };
364
427
  }