create-pmos 0.223.0 → 0.225.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.223.0",
3
+ "version": "0.225.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@8cbfda9
1
+ kernel-source: pmos@6a10a1d
2
2
  assembled: 2026-10-14
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-14T05:39:31Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T06:35:37Z"}
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-14T05:39:31Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T06:35:37Z"}
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-14T05:39:31Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T06:35:37Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -10,6 +10,7 @@ const T = require('./test-run').configure({ name: 'c3-light-load' });
10
10
 
11
11
  const BASE = T.BASE;
12
12
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
13
+ // Settings and the dock tree get 30 s: a loaded CI runner took longer than 15 s to draw them (twice on one run, 33/33 locally)
13
14
  const until = async (fn, ms = 15000, step = 150) => { const t0 = Date.now(); for (;;) { const v = await fn(); if (v) return v; if (Date.now() - t0 > ms) return v; await sleep(step); } };
14
15
  let pass = 0; let fail = 0;
15
16
  const ok = (name, cond, extra = '') => { if (cond) pass++; else fail++; console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + extra : ''}`); };
@@ -74,18 +75,18 @@ process.on('unhandledRejection', (e) => { console.error(e); fail++; finish(); })
74
75
  /* each place still opens */
75
76
  await page.click('[data-c3-tab="cal"]');
76
77
  ok('Calendar: its code loads on the first press', !!(await until(() => scripts.has('calendar-views.js'))));
77
- ok('Calendar: the page draws', !!(await until(() => page.evaluate(() => { const p = document.querySelector('[data-c3-pane="cal"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }))));
78
+ ok('Calendar: the page draws', !!(await until(() => page.evaluate(() => { const p = document.querySelector('[data-c3-pane="cal"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }), 30000)));
78
79
  await page.click('[data-c3-tab="init"]');
79
- ok('Initiatives: its code loads and the page draws', !!(await until(() => scripts.has('c3-initiatives.js') && page.evaluate(() => { const p = document.querySelector('[data-c3-pane="init"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }))));
80
+ ok('Initiatives: its code loads and the page draws', !!(await until(() => scripts.has('c3-initiatives.js') && page.evaluate(() => { const p = document.querySelector('[data-c3-pane="init"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }), 30000)));
80
81
  await page.click('[data-c3-tab="rep"]');
81
82
  ok('Reports: its code loads and the page draws', !!(await until(() => scripts.has('c3-reports.js') && page.evaluate(() => !!document.querySelector('[data-c3-pane="rep"] [data-rp3]')))));
82
83
  await page.click('[data-c3pg="settings"]');
83
- ok('Settings: its code loads and the page draws', !!(await until(() => scripts.has('c3-settings.js') && page.evaluate(() => { const p = document.querySelector('[data-c3-pane="settings"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }))));
84
+ ok('Settings: its code loads and the page draws', !!(await until(() => scripts.has('c3-settings.js') && page.evaluate(() => { const p = document.querySelector('[data-c3-pane="settings"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }), 30000)));
84
85
  await page.click('[data-c3-tab="brief"]');
85
86
  const before = gets.length;
86
87
  await page.keyboard.press('Control+j');
87
88
  ok('Dock: opening it reads the sessions and the working trees', !!(await until(() => gets.slice(before).some((g) => g.path === '/api/sessions') && gets.slice(before).some((g) => g.path === '/api/git/worktrees'))));
88
- ok('Dock: the tree draws', !!(await until(() => page.evaluate(() => { const t = document.querySelector('[data-c3pg="dock-tree"]'); return !!t && !document.querySelector('[data-c3pg="dock"]').hidden && !/Reading the repository/.test(t.textContent); }))));
89
+ ok('Dock: the tree draws', !!(await until(() => page.evaluate(() => { const t = document.querySelector('[data-c3pg="dock-tree"]'); return !!t && !document.querySelector('[data-c3pg="dock"]').hidden && !/Reading the repository/.test(t.textContent); }), 30000)));
89
90
  await page.keyboard.press('Control+j');
90
91
  await page.evaluate(() => { if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); });
91
92
  await page.keyboard.press('c'); // the key C opens the calendar popup (the header button may be in the quiet layout's overflow)
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Guide: the Calendar tab of the console page"
4
4
  description: "The Calendar tab of the console page (/): Calendar and Notes, the five zooms, the highlights, the layer chips, the markers, the context of a meeting, free time, the tidy-up suggestions and the empty states."
5
- timestamp: 2026-10-13
5
+ timestamp: 2026-10-14
6
6
  ---
7
7
 
8
8
  # Guide: the Calendar tab of the console page
@@ -79,11 +79,13 @@ Press a name to open the item in the detail column. When nothing significant fal
79
79
 
80
80
  ## The grid
81
81
 
82
- - **Day** shows the timeline of the day. The markers of the day are in a strip above it.
82
+ - **Day** shows the timeline of the day. The markers of the day are in a strip above it, side by side on one line.
83
83
  - **Week** shows a timeline for each day. Each day name has the hours of meetings of that day and a small bar. The bar has three parts: time tied to a promise or a decision, routine time, and the rest of a 6 hour day. The markers of each day are in a strip under the day name. Press a day name to open that day.
84
84
  - **Month** shows a cell for each day. A cell shows the highlight of the day in one sentence, for example "SSO general release ships." Under it, a quiet line counts the rest, for example "1 due · 2 routine". Press that line or the day number to open the day. A line under the grid says what is counted and not named. With **Routine** on, a cell shows the number of meetings and up to three markers instead, and "+2" opens the day with the other markers.
85
85
  - **Quarter** shows a column for each month. **Year** shows a column for each quarter, with a button for each of its three months. Each column shows its highlight and a quiet line, for example "24 meetings · 3 due · 9 routine". Quarter and Year are stricter than Month: they name only milestones, releases, reports that go out and late items. Press a column's title to open that month or quarter. With **Routine** on, a column shows its markers instead.
86
86
 
87
+ On Day and Week, the reports of a day are in one **Reports** chip with their number, for example "Reports 2". On Day it also says what they are, for example "2 scheduled · 1 not issued". Press the chip to show the reports under it, each with its state: **scheduled**, **not issued** or **issued**. Press it again to fold them. A report that was not issued after its day is late for a week: it stays out of the chip with an amber edge. The reports of a horizon scan are documents, not report dates, so they are not on the calendar. Two report schedules with the same name show once on a day.
88
+
87
89
  Titles wrap onto more lines. They are never cut.
88
90
 
89
91
  A marker has a symbol and a short title. A red edge means late or at risk of missing. An amber edge means at risk.
@@ -96,7 +98,9 @@ Press a marker to show its detail in the side column. The detail shows the title
96
98
 
97
99
  ## Select a meeting
98
100
 
99
- Press a meeting to show its context in the side column. Under the title, a row shows where the meeting lives, the people and the initiative. Each line has a link to its source.
101
+ Press a meeting to open it in a popup at the right of the screen, as a session opens. The popup has four sizes: **Normal**, **Half**, **2/3** and **Whole**. It opens at the size you used last. Press `Esc`, **Close** or the page behind it to close it. When the meeting has a note, at **Half** and wider the note is beside the meeting: its title, the first lines of its summary and **Open the note**. **Focus**, **Open the note** and a link to another page close the popup first.
102
+
103
+ In the popup, a row shows where the meeting lives, the people and the initiative. Each line has a link to its source. When all the lines come from one note, the note is named once under them. The calendar popup over other pages shows the meeting in its side column instead.
100
104
 
101
105
  - For a past meeting, **What came out of it** lists the decisions and actions of its note. **What moved after it** lists the items that changed within 48 hours and refer to the meeting or its initiative.
102
106
  - For a coming meeting, the detail shows these parts. A part with no record is left out.
@@ -137,7 +141,7 @@ When PMOS holds nothing for a past meeting, the detail says so. For a coming mee
137
141
 
138
142
  ### Meeting focus
139
143
 
140
- **Focus** opens one calm page for a meeting that is today. It shows on the detail of a meeting that has not ended, and next to the recording on the header while a meeting is recorded. The page covers the console. **Leave focus** gives the console back. Leaving does not stop the recording. The page stays open when you reload it. From the recording, the page finds the meeting by its start time and title, so it also finds a meeting that has no calendar id, or that moved on the same day after you started to record.
144
+ **Focus** opens one calm page for a meeting that is today. It shows on the detail of a meeting that has not ended, and next to the recording on the header while a meeting is recorded. An untitled note that records has **Focus** too: the page opens on the note, with the conversation and the captured items, and no topics or material. The page covers the console. **Leave focus** gives the console back. Leaving does not stop the recording. The page stays open when you reload it. From the recording, the page finds the meeting by its start time and title, so it also finds a meeting that has no calendar id, or that moved on the same day after you started to record.
141
145
 
142
146
  The page has these parts:
143
147
 
@@ -16,7 +16,7 @@ Every button in the top bar and in the note is an icon. Hold the pointer on an i
16
16
  The Record control is at the right of the top bar. It has two groups, always in the same place.
17
17
 
18
18
  - **The meeting group** shows only while a meeting is recording or paused. It starts with the meeting's name and its clock (paused: **paused · 48 min left**). Press the name to open the note. While recording, the group has a highlighter icon, **Pause** and **Stop**. While paused, it has **Resume** and **Stop**. When there is nothing to resume, the group is not there.
19
- - **The new group** is the red microphone with a plus, and an arrow. The microphone starts an untitled note and listens at once. The arrow lists the meetings of today that have not ended and have no note, soonest first. A meeting that has started stays in the list until 10 minutes after it ends, so you can still record it. It shows "Now · started 5 min ago", or "Ended 3 min ago" in those last 10 minutes. **Next** marks the first meeting that has not started. Each shows who is coming and how many agenda items it has. Press one to start a note for that meeting, with its context. Both are unavailable while a meeting is recording. Pause or stop it first.
19
+ - **The new group** is the red microphone with a plus, and an arrow. The microphone starts an untitled note and listens at once. The arrow lists the meetings of today that have not ended and have no note, soonest first. These are the meetings the Calendar shows, including the ones you imported in Settings. A meeting that has started stays in the list until 10 minutes after it ends, so you can still record it. It shows "Now · started 5 min ago", or "Ended 3 min ago" in those last 10 minutes. **Next** marks the first meeting that has not started. Each shows who is coming and how many agenda items it has. Press one to start a note for that meeting, with its context. Both are unavailable while a meeting is recording. Pause or stop it first.
20
20
 
21
21
  Starting a note, from the Record control or from **Take notes** on a calendar event, shows the Notes page with that note.
22
22
 
@@ -7,8 +7,9 @@
7
7
  * 1. <runtime>/calendar-import/events.jsonl the import ledger (Settings › Import now, `pmos calendar import`);
8
8
  * 2. <runtime>/calendar-memory.json the calendar the morning brief submits (lib/calendar-memory.js), 90 days back and 31 ahead;
9
9
  * 3. <runtime>/briefs/<today>.json today's brief, whose calendar is today's meetings with their plan.
10
- * The Agenda and the Record menu read 2 and 3 (GET /api/proactive/agenda). Calendar and the Briefing's Today read 1 only, so a PM whose
11
- * meetings came in with the morning brief saw them under Record and nowhere else. This reader joins the three. An imported event wins
10
+ * The Agenda reads 2 and 3 (GET /api/proactive/agenda). Calendar and the Briefing's Today read 1 only, so a PM whose
11
+ * meetings came in with the morning brief saw them under Record and nowhere else. This reader joins the three. The Record menu reads it
12
+ * too (todayOf, GET /api/calendar-today): it read the Agenda once, so an imported meeting was on the Calendar and missing under Record. An imported event wins
12
13
  * over a remembered one with the same start minute and title, because it carries the PM's notes; today's brief replaces the remembered
13
14
  * events of today, as agendaMonth does.
14
15
  *
@@ -71,4 +72,22 @@ function readAll(runtimeDir, { now = Date.now() } = {}) {
71
72
  return out.sort((a, b) => (Date.parse(a.at) || 0) - (Date.parse(b.at) || 0));
72
73
  }
73
74
 
74
- module.exports = { readAll, asEvent, localIso };
75
+ /** Today's meetings as the Record menu reads them (GET /api/calendar-today): every event readAll joins, kept when its start falls on
76
+ * the local day of `now`, in the shape the menu and a note's meeting link use: { id, start, end, title, time, minutes, with, src, ... }. Pure. */
77
+ function todayOf(events, now = Date.now()) {
78
+ const day = localDate(now);
79
+ const out = [];
80
+ for (const e of events || []) {
81
+ const s = Date.parse(e && e.at);
82
+ if (!e || !e.title || Number.isNaN(s) || localDate(s) !== day) continue;
83
+ const f = e.end && !Number.isNaN(Date.parse(e.end)) ? Date.parse(e.end) : s + 30 * 60000;
84
+ const people = [].concat(e.attendees || []).map((p) => (p && typeof p === 'object' ? p.name || '' : String(p || ''))).filter(Boolean);
85
+ const d = new Date(s);
86
+ out.push({ id: String(e.id || ''), start: localIso(s), end: localIso(f), title: String(e.title), time: `${pad(d.getHours())}:${pad(d.getMinutes())}`,
87
+ minutes: Math.round((f - s) / 60000), ...(people.length ? { with: people.slice(0, 6).join(', ') } : {}),
88
+ src: e.src || 'import', ...(e.initiative ? { initiative: e.initiative } : {}) });
89
+ }
90
+ return out.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
91
+ }
92
+
93
+ module.exports = { readAll, asEvent, localIso, todayOf };
@@ -32,6 +32,7 @@ const PER_DAY = 60; // minutes of a whole working day befor
32
32
  const DAY_START = 9 * 60;
33
33
  const DAY_END = 18 * 60;
34
34
  const WARN_DAYS = 2;
35
+ const LATE_DAYS = 7; // a report not issued after its day is late (warn) for this many days
35
36
  const SUGGEST_DAYS = 7;
36
37
  const WINDOW_MS = 48 * 3600000;
37
38
  const PER_KIND = { decision: 6, action: 6, moved: 8, prepared: 8, todo: 8 };
@@ -132,15 +133,23 @@ function layersFrom(dated, range, today) {
132
133
  if (!r || !r.cadence || !isDay(r.slot)) continue;
133
134
  const date = r.issued && isDay(r.issued.at) ? day10(r.issued.at) : day10(r.slot);
134
135
  n++;
136
+ // a report left unissued after its day is late for a week, then it is history (a daily draft nobody issued would otherwise warn forever)
137
+ const late = !r.issued && date < today && diffDays(today, date) <= LATE_DAYS;
135
138
  put('reports', { id: `report:${r.cadence}:${day10(r.slot)}`, date, kind: 'report', layer: 'reports', title: `${r.cadence} report${r.issued ? '' : ' (not issued)'}`,
136
- tone: r.issued ? 'ok' : null, cadence: r.cadence, routine: routineCadence(r.cadence), ref: { type: 'report', id: `${r.cadence}/${day10(r.slot)}` } });
139
+ tone: r.issued ? 'ok' : late ? 'warn' : null, state: r.issued ? 'issued' : 'not issued', cadence: r.cadence, routine: routineCadence(r.cadence), ref: { type: 'report', id: `${r.cadence}/${day10(r.slot)}` } });
137
140
  }
141
+ // two schedules with the same name that run on the same day are one run on the calendar
142
+ const runs = new Set();
138
143
  for (const s of d.reportSchedules || []) {
139
144
  if (!s || !s.id) continue;
145
+ const name = str(s.name) || s.cadence || s.id;
140
146
  for (const dt of s.dates || []) {
141
147
  if (!isDay(dt)) continue;
148
+ const key = name.toLowerCase() + '\u0000' + day10(dt);
149
+ if (runs.has(key)) continue;
150
+ runs.add(key);
142
151
  n++;
143
- put('reports', { id: `report-sched:${s.id}:${day10(dt)}`, date: day10(dt), kind: 'report', layer: 'reports', title: `${str(s.name) || s.cadence || s.id} (scheduled)`, tone: null,
152
+ put('reports', { id: `report-sched:${s.id}:${day10(dt)}`, date: day10(dt), kind: 'report', layer: 'reports', title: `${name} (scheduled)`, tone: null, state: 'scheduled',
144
153
  ...(s.cadence ? { cadence: s.cadence } : {}), routine: routineCadence(s.cadence) || routineCadence(s.name), ref: { type: 'report', id: s.cadence || s.id } });
145
154
  }
146
155
  }
@@ -5,6 +5,7 @@
5
5
  * layers, layers_total, layers_note, free, context, context_total, (lib/calendar-layers.js)
6
6
  * link_boards } every board a meeting can be linked to: [{ ws, id, name }]
7
7
  * &only=gantt → { ok, view, range, gantt, no_plan, strategy, today } the rows with a plan date or a Jira read, and how many have neither
8
+ * GET /api/calendar-today → { ok, date, events } today's meetings for the Record menu (calendar-events.js todayOf)
8
9
  * POST /api/calendar-links → { ok, link } what a meeting is about (lib/calendar-links.js, below)
9
10
  * tidy, tidy_total, tidy_note } (lib/calendar-tidy.js: suggestions in words, no write)
10
11
  *
@@ -117,7 +118,8 @@ function readers(root, runtimeDir) {
117
118
  } catch (e) { unavailable.releases = 'the releases file could not be read: ' + reason(e); }
118
119
  try {
119
120
  const RN = require('./report-needs.js');
120
- out.reports = RN.reportsOf(root).reports.map((r) => ({ cadence: r.cadence, slot: r.slot, issued: r.issued ? { at: r.issued.at } : null }));
121
+ // a horizon scan writes reports/scan-<type>/<day>.md (lib/scan.js planPath): a finished document, not a report slot to issue
122
+ out.reports = RN.reportsOf(root).reports.filter((r) => !/^scan-/.test(r.cadence)).map((r) => ({ cadence: r.cadence, slot: r.slot, issued: r.issued ? { at: r.issued.at } : null }));
121
123
  out.reportSchedules = reportSchedules(RN, range);
122
124
  } catch (e) { unavailable.reports = 'the report folders could not be read: ' + reason(e); }
123
125
  try { out.own = ownRows().map((o) => ({ id: o.id, title: o.title, due: o.due, done: !!o.done })); } catch (e) { unavailable.own = 'your items could not be read: ' + reason(e); }
@@ -220,13 +222,22 @@ async function postLink(req, res, ctx) {
220
222
 
221
223
  module.exports = {
222
224
  readers,
223
- prefixes: ['/api/calendar-views', '/api/calendar-links'],
225
+ prefixes: ['/api/calendar-views', '/api/calendar-links', '/api/calendar-today'],
224
226
  handlePure: handle,
225
227
  handle: async (req, res, url, ctx) => {
226
228
  if (url.pathname === '/api/calendar-links') {
227
229
  if (req.method !== 'POST') { ctx.json(res, { ok: false, error: 'POST only' }, 405); return true; }
228
230
  return postLink(req, res, ctx);
229
231
  }
232
+ if (url.pathname === '/api/calendar-today') {
233
+ if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
234
+ const cal = require('./calendar-events.js');
235
+ const now = Date.now();
236
+ let events = [];
237
+ try { events = cal.todayOf(cal.readAll(ctx.runtimeDir, { now }), now); } catch { events = []; }
238
+ ctx.json(res, { ok: true, date: today(), events });
239
+ return true;
240
+ }
230
241
  if (url.pathname !== '/api/calendar-views') return false;
231
242
  if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
232
243
  const w = ctx.getWs(url.searchParams.get('ws'));
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.223.0",
3
+ "version": "0.225.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.223.0",
9
+ "version": "0.225.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.223.0",
3
+ "version": "0.225.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",
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * Top to bottom: the page line and the zoom bar (Day, Week, Month, Quarter, Year), the layer chips (Meetings, Milestones
8
8
  * and due dates, Releases, Report dates, My due items, then Free time and Tidy-up), the grid of the zoom with the layer
9
- * markers on it, and the detail column (under the grid on a narrow screen, at the right from 1100px).
9
+ * markers on it, and the detail column (under the grid on a narrow screen, at the right from 1100px). A meeting opens in a popup
10
+ * sheet instead, as a session does (initiative c3-cal-strip-popup); a dated marker keeps its card in the column.
10
11
  *
11
12
  * What it reads from the answer, all optional and additive: `cells` (the meetings), `layers` (the dated items, each with
12
13
  * kind, layer, tone and a reference to its home), `free` (free blocks with a suggested use or none), `context` (what came
@@ -24,6 +25,7 @@ import { toast } from './c3-toast.js';
24
25
  import { pmPost } from './http.js';
25
26
  import { notesView, openNote, noteOfEvent, primeNotes } from './c3-notes.js'; // Calendar › Notes (initiative c3-notes-place)
26
27
  import { onNotes } from './notes-links.js';
28
+ import { openSheet } from './c3-sheet.js';
27
29
 
28
30
  const LAYERS = [
29
31
  ['meetings', 'Meetings'],
@@ -109,6 +111,7 @@ export function drawCalendarPage(host, r, h) {
109
111
  const redraw0 = () => h.redraw(host, r);
110
112
  const modes = r.ctx.overlay ? null : modeSeg(st, redraw0);
111
113
  if (st.mode === 'notes' && modes) { // Calendar › Notes: the list and the open note; the zoom and the period have nothing to step here
114
+ if (st.sel && st.sel.type === 'event') { st.sel = null; closeMeetingSheet(r); }
112
115
  return page(el('div', { class: 'calbar' }, el('div', { class: 'lens' }, modes)), notesView({ host, r, h, st, redraw: redraw0 }));
113
116
  }
114
117
  if (!r.notesOff) { // a meeting's "Open the note" needs the shared notes read; a new note redraws the open detail
@@ -122,7 +125,10 @@ export function drawCalendarPage(host, r, h) {
122
125
  if (!r.data) return pageBar(fold('clock', 'Reading the repository', { 'data-cv': 'loading', role: 'status' }));
123
126
 
124
127
  const d = r.data; const M = model(d, h);
125
- const redraw = (focusSel) => { h.redraw(host, r); if (focusSel) { const n = r.root.querySelector(focusSel); if (n) n.focus(); } };
128
+ const redraw = (focusSel) => {
129
+ h.redraw(host, r);
130
+ if (focusSel) { const n = r.root.querySelector(focusSel) || (meet && meet.r === r ? meet.root.querySelector(focusSel) : null); if (n) n.focus(); }
131
+ };
126
132
  const select = (sel, focusSel) => {
127
133
  st.sel = sel; redraw(focusSel);
128
134
  const det = r.root.querySelector('[data-cal3="detail"]');
@@ -150,9 +156,64 @@ export function drawCalendarPage(host, r, h) {
150
156
  el('div', { class: 'cal3-wrap' },
151
157
  el('div', { class: 'cal3-main' }, empty, empty ? null : highlights(ui, range), grid),
152
158
  el('div', { class: 'cal3-side' }, detail(ui), st.tidy ? tidyList(ui) : null, st.free ? freeList(ui, range) : null, lanes(ui, range))));
159
+ syncMeetingSheet(ui);
153
160
  return pageBar(chips(ui, range), body);
154
161
  }
155
162
 
163
+ /* ── a meeting in a popup (initiative c3-cal-strip-popup): the session sheet, with its Normal, Half, 2/3 and Whole sizes ── */
164
+ const MEET_SIZE_KEY = 'pmos.workspace.c3.meeting-size';
165
+ const MEET_SIZES = [['normal', 'Normal', 'Normal'], ['half', 'Half', 'Half of the window'], ['two-thirds', '2/3', 'Two-thirds of the window'], ['whole', 'Whole', 'Whole window']];
166
+ const meetStore = {
167
+ get() { try { return localStorage.getItem(MEET_SIZE_KEY); } catch { return null; } },
168
+ set(v) { try { localStorage.setItem(MEET_SIZE_KEY, v); } catch { /* no storage */ } },
169
+ };
170
+ let meet = null; // the open popup: { r, id, close, root, sheet, body, title }
171
+
172
+ export function closeMeetingSheet(r) {
173
+ if (!meet || (r && meet.r !== r)) return;
174
+ const m = meet; meet = null; m.quiet = true; m.close();
175
+ }
176
+
177
+ /* open, refresh or close the popup so it matches the selection; the page calls it on every draw */
178
+ function syncMeetingSheet(ui) {
179
+ const { st, M, r, h } = ui;
180
+ const hit = !r.ctx.overlay && st.sel && st.sel.type === 'event' ? M.findEvent(st.sel.id) : null;
181
+ if (!hit) { closeMeetingSheet(r); return; }
182
+ const box = el('section', { class: 'cal3 cal3-detail cal3-meet', 'data-cal3': 'detail', 'aria-label': 'Meeting', 'data-cal3-sel': hit.e.id }, ...meetingDetail(ui, hit, true));
183
+ const title = hit.e.title || hit.e.id;
184
+ if (meet && meet.r === r && meet.root.isConnected) {
185
+ meet.body.replaceChildren(box);
186
+ if (meet.id !== hit.e.id) { meet.id = hit.e.id; meet.title.textContent = title; meet.sheet.setAttribute('aria-label', title); meet.body.parentElement.scrollTop = 0; }
187
+ return;
188
+ }
189
+ let size = MEET_SIZES.some((x) => x[0] === meetStore.get()) ? meetStore.get() : 'half';
190
+ const btns = MEET_SIZES.map(([k, label, tip]) => button({ label, variant: 'ghost', attrs: { 'aria-pressed': String(k === size), 'data-cal3-size': k, title: tip, 'aria-label': tip },
191
+ onClick: () => setSize(k) }));
192
+ const seg = el('div', { class: 'seg cal3-msize', role: 'group', 'aria-label': 'Size', 'data-cal3': 'meeting-size' }, ...btns);
193
+ const self = {};
194
+ const o = openSheet({ title, icon: 'cal', wide: true, body: box, actions: [seg], cls: 'cal3-ms tsz-' + size,
195
+ onClose: () => { if (meet === self) meet = null; if (self.quiet) return; if (st.sel && st.sel.type === 'event') { st.sel = null; h.redraw(ui.host, r); } } });
196
+ Object.assign(self, { r, id: hit.e.id, close: o.close, root: o.root, sheet: o.sheet, body: o.body, title: o.sheet.querySelector('.sh2 b') });
197
+ o.root.setAttribute('data-cal3-meeting', '');
198
+ meet = self;
199
+ function setSize(k) {
200
+ size = k; meetStore.set(k);
201
+ MEET_SIZES.forEach(([x]) => o.sheet.classList.toggle('tsz-' + x, x === k));
202
+ btns.forEach((b) => b.setAttribute('aria-pressed', String(b.getAttribute('data-cal3-size') === k)));
203
+ }
204
+ }
205
+
206
+ /* the note of a meeting beside what came out of it: its title, its first summary lines and "Open the note" */
207
+ function noteCard(ui, e) {
208
+ const n = noteOfEvent(e); if (!n) return null;
209
+ const lines = (n.summary_lines || []).filter(Boolean);
210
+ return el('div', { class: 'cal3-ctx cal3-mnote', 'data-cal3-ctx': 'note' }, el('h4', null, 'Note'),
211
+ el('div', { class: 'cal3-mnote-b' }, el('b', null, n.title || 'Untitled note'),
212
+ ...(lines.length ? lines.map((t) => el('p', null, t)) : [el('p', { class: 'dim' }, n.private ? 'A private note. Its summary stays in the note.' : 'No summary yet.')])),
213
+ el('a', { href: '#', class: 'cal3-ref', 'data-cal3': 'open-note', 'data-cal3-open-note': n.id, title: 'Open the note: ' + (n.title || 'Untitled note'),
214
+ onClick: (ev) => { ev.preventDefault(); openNote(ui.r, n.id); ui.redraw(); } }, c3ic('notes', 'sm'), 'Open the note'));
215
+ }
216
+
156
217
  /* "Open the note": the note recorded for this meeting, on Calendar › Notes; nothing when no note names the meeting */
157
218
  function noteLink(ui, e) {
158
219
  if (ui.r.ctx.overlay) return null;
@@ -196,18 +257,49 @@ const tidyCount = (M, d) => (M.tidy ? Math.max(0, (typeof d.tidy_total === 'numb
196
257
 
197
258
  /* ── a marker: glyph and short title, tone from the tokens; pressing it selects it (the detail has the reference) ─── */
198
259
 
199
- function marker(ui, m, compact) {
260
+ function marker(ui, m, compact, tagged) {
200
261
  const [word, icon] = kindOf(m);
201
262
  const sel = ui.st.sel && ui.st.sel.type === 'marker' && ui.st.sel.id === m.id;
202
263
  const name = word + ': ' + m.title + ', ' + ui.h.dayLabel(m.date) + (m.tone && TONE[m.tone] ? '. ' + TONE[m.tone] : '');
264
+ // inside the Reports fold the state is a tag after the name: "Daily report draft scheduled"
265
+ const label = tagged && m.state ? [c3ic(icon, 'sm'), el('span', { class: 'cal3-mk-t' }, untag(m.title)), el('span', { class: 'cal3-mk-s' }, m.state)]
266
+ : [c3ic(icon, 'sm'), el('span', { class: 'cal3-mk-t' }, m.title)];
203
267
  return button({
204
- label: [c3ic(icon, 'sm'), el('span', { class: 'cal3-mk-t' }, m.title)], variant: 'ghost', size: 'sm',
268
+ label, variant: 'ghost', size: 'sm',
205
269
  attrs: { class: 'cal3-mk cal3-k-' + (KIND[m.kind] ? m.kind : 'other') + (m.tone ? ' cal3-tone-' + m.tone : '') + (compact ? ' compact' : '') + (sel ? ' sel' : ''),
206
270
  'data-cal3-mk': m.id, 'data-cal3-layer': m.layer, 'data-cal3-kind': m.kind, 'aria-pressed': String(!!sel), 'aria-label': name, title: name },
207
271
  onClick: () => ui.select({ type: 'marker', id: m.id }, '[data-cal3-mk="' + cssId(m.id) + '"]'),
208
272
  });
209
273
  }
210
274
  const cssId = (s) => String(s).replace(/["\\]/g, '\\$&');
275
+ const untag = (t) => String(t).replace(/\s*\((scheduled|not issued)\)$/, '');
276
+
277
+ /* the dates strip of one day (initiative c3-cal-strip-popup): every report of the day folds into one Reports chip that opens in place,
278
+ * except a late one, which stays out with the other dated items. A Day lays them in one wrapping line; a Week column stacks them. */
279
+ function stripMarks(ui, date, one) {
280
+ const all = marksIn(ui.M, ui.st, date, date);
281
+ const folded = all.filter((m) => m.kind === 'report' && m.tone !== 'warn' && m.tone !== 'bad');
282
+ const rest = all.filter((m) => !folded.includes(m));
283
+ const max = one ? 12 : 4;
284
+ const out = [...rest.slice(0, max).map((m) => marker(ui, m, !one)), rest.length > max ? moreBtn(ui, rest.length - max, date, date, 'day') : null];
285
+ if (!folded.length) return out;
286
+ const st = ui.st;
287
+ if (!st.repOpen) st.repOpen = new Set();
288
+ const held = st.sel && st.sel.type === 'marker' && folded.some((m) => m.id === st.sel.id); // a selected report keeps its fold open
289
+ const open = st.repOpen.has(date) || held;
290
+ const n = (s) => folded.filter((m) => m.state === s).length;
291
+ const words = ['scheduled', 'not issued', 'issued'].filter((s) => n(s)).map((s) => n(s) + ' ' + s).join(' · ');
292
+ const say = plural(folded.length, 'report') + ' on ' + ui.h.dayLabel(date) + (words ? ': ' + words : '') + '. ' + (open ? 'Hide them' : 'Show them');
293
+ out.push(button({
294
+ label: [c3ic('report', 'sm'), el('span', { class: 'cal3-rf-t' }, 'Reports'), el('span', { class: 'cal3-n' }, String(folded.length)),
295
+ one && words ? el('span', { class: 'cal3-rf-bd' }, words) : null],
296
+ variant: 'ghost', size: 'sm',
297
+ attrs: { class: 'cal3-rf', 'data-cal3-rf': date, 'aria-expanded': String(open), 'aria-label': say, title: say },
298
+ onClick: () => { if (open) { st.repOpen.delete(date); if (held) st.sel = null; } else st.repOpen.add(date); ui.redraw('[data-cal3-rf="' + date + '"]'); },
299
+ }));
300
+ if (open) out.push(el('div', { class: 'cal3-rf-open', 'data-cal3-rf-open': date }, ...folded.map((m) => marker(ui, m, false, true))));
301
+ return out;
302
+ }
211
303
 
212
304
  /* the markers of a span, the most important first, at most `max`, and a "+N" for the rest */
213
305
  function markRow(ui, a, b, max, compact, view) {
@@ -273,8 +365,8 @@ function timeGrid(ui, days) {
273
365
 
274
366
  const strip = el('div', { class: 'cal3-row cal3-strip', 'data-cal3': 'strip' }, el('div', { class: 'cal3-gut' }, 'dates'),
275
367
  ...days.map((date) => {
276
- const kids = [...allDayOn(date).map((m) => meetingButton(ui, m, 'cal3-allday')), ...markRow(ui, date, date, days.length === 1 ? 12 : 4, days.length > 1, 'day')];
277
- return el('div', { class: 'cal3-sc', 'data-cal3-strip': date }, ...(kids.filter(Boolean).length ? kids : [el('span', { class: 'cal3-none' }, days.length === 1 ? 'No dated item on this day' : '')]));
368
+ const kids = [...allDayOn(date).map((m) => meetingButton(ui, m, 'cal3-allday')), ...stripMarks(ui, date, days.length === 1)];
369
+ return el('div', { class: 'cal3-sc' + (days.length === 1 ? ' line' : ''), 'data-cal3-strip': date }, ...(kids.filter(Boolean).length ? kids : [el('span', { class: 'cal3-none' }, days.length === 1 ? 'No dated item on this day' : '')]));
278
370
  }));
279
371
 
280
372
  const hours = el('div', { class: 'cal3-hours', 'aria-hidden': 'true' });
@@ -519,6 +611,8 @@ function yearCards(ui) {
519
611
 
520
612
  function refLink(ui, ref, extra) {
521
613
  if (!ref || !ref.type || !ref.id) return el('span', { class: 'dim cal3-noptr' }, 'no pointer');
614
+ if (ref.type === 'note' && !ui.r.ctx.overlay) return el('a', { href: '#', class: 'cal3-ref', 'data-cal3-ref': 'note:' + ref.id, title: 'Open the note',
615
+ onClick: (ev) => { ev.preventDefault(); openNote(ui.r, ref.id); ui.redraw(); } }, c3ic('notes', 'sm'), 'Open the note');
522
616
  if (!REF[ref.type]) return el('span', { class: 'dim mono cal3-noptr' }, ref.type + ' ' + ref.id);
523
617
  return el('a', { href: '#', class: 'cal3-ref', 'data-cal3-ref': ref.type + ':' + ref.id, title: REF[ref.type] + ': ' + ref.id,
524
618
  onClick: (ev) => { ev.preventDefault(); openRef(ui, ref, extra || {}); } }, c3ic('out', 'sm'), REF[ref.type]);
@@ -527,6 +621,7 @@ function refLink(ui, ref, extra) {
527
621
  /* open the home of a reference: an initiative on Initiatives, a work item on Work, a report on Reports */
528
622
  function openRef(ui, ref, extra) {
529
623
  const { h, host, r } = ui;
624
+ if (ui.st.sel && ui.st.sel.type === 'event') { ui.st.sel = null; closeMeetingSheet(r); } // the popup does not follow you to another page
530
625
  if (ref.type === 'initiative') { if (!h.goInitiative(ref.id)) h.openInit(host, r, ref.id, extra.title); return; }
531
626
  if (ref.type === 'plan-item' || ref.type === 'release') { const ini = extra.initiative || ref.initiative; if (!(ini && h.goInitiative(ini))) h.gotoTab('init'); return; }
532
627
  if (ref.type === 'item') { h.gotoTab('work'); return; }
@@ -562,18 +657,19 @@ function detail(ui) {
562
657
  }
563
658
  if (st.sel && st.sel.type === 'event') {
564
659
  const hit = M.findEvent(st.sel.id);
565
- if (hit) return box(meetingDetail(ui, hit), { 'data-cal3-sel': hit.e.id });
660
+ if (hit && ui.r.ctx.overlay) return box(meetingDetail(ui, hit), { 'data-cal3-sel': hit.e.id }); // the calendar popup is a drawer already
566
661
  }
567
662
  return box([el('div', { class: 'cal3-dt' }, el('h3', null, c3ic('info'), 'Detail')),
568
663
  el('p', { class: 'dim cal3-note' }, 'Select a meeting to see what came out of it or what to prepare. Select a dated marker to see its date and open its home.')]);
569
664
  }
570
665
 
571
666
  /** The Meeting focus page (initiative c3-meeting-focus): one calm full-screen page for a meeting that is today. It loads when pressed. */
572
- const focusBtn = (e, date) => button({ label: [c3ic('target', 'sm'), 'Focus'], variant: 'primary', size: 'sm',
667
+ /* from the popup, Focus closes it first: the focus page is the whole screen and has its own sheets (c3-cal-strip-popup) */
668
+ const focusBtn = (e, date, ui) => button({ label: [c3ic('target', 'sm'), 'Focus'], variant: 'primary', size: 'sm',
573
669
  attrs: { 'data-cal3': 'focus', title: 'Open the meeting focus page: the clock, the topics, the material and PMOS beside the conversation' },
574
- onClick: () => { import('./c3-focus.js').then((m) => m.openFocus({ event: e, date })).catch(() => toast({ text: 'The focus page could not load.', tone: 'error' })); } });
670
+ onClick: () => { if (ui) { ui.st.sel = null; closeMeetingSheet(ui.r); ui.redraw(); } import('./c3-focus.js').then((m) => m.openFocus({ event: e, date })).catch(() => toast({ text: 'The focus page could not load.', tone: 'error' })); } });
575
671
 
576
- function meetingDetail(ui, hit) {
672
+ function meetingDetail(ui, hit, inSheet) {
577
673
  const { M, h } = ui; const e = hit.e;
578
674
  const s = h.timeOf(e.at); const f = s === null ? null : h.endH(e);
579
675
  const rec = M.ctx ? (M.ctx[e.id] || M.ctx[h.baseId(e)] || null) : null;
@@ -582,11 +678,15 @@ function meetingDetail(ui, hit) {
582
678
  const items = rec && Array.isArray(rec.items) ? rec.items : [];
583
679
  const list = (kinds, title, key, none) => {
584
680
  const rows = items.filter((x) => kinds.includes(x.kind));
681
+ // lines that all come from one note name it once, under the list, instead of a pointer on every line
682
+ const r0 = rows.length > 1 && rows[0].ref && rows[0].ref.type === 'note' ? rows[0].ref : null;
683
+ const one = r0 && rows.every((x) => x.ref && x.ref.type === 'note' && x.ref.id === r0.id) ? r0 : null;
585
684
  return el('div', { class: 'cal3-ctx', 'data-cal3-ctx': key }, el('h4', null, title),
586
685
  rows.length ? el('ul', { class: 'cal3-cl', role: 'list' }, ...rows.map((x) => el('li', { class: 'cal3-ci', 'data-cal3-item': x.kind },
587
686
  el('span', { class: 'tag' + (x.done ? ' cal3-up' : '') }, x.done && x.kind === 'todo' ? 'Done' : CTXWORD[x.kind] || x.kind), el('span', { class: 'cal3-ct' }, x.text),
588
687
  x.at ? el('span', { class: 'dim mono cal3-at' }, h.dayLabel(x.at) + (h.timeOf(x.at) !== null ? ' ' + h.hhmm(h.timeOf(x.at)) : '')) : null,
589
- refLink(ui, x.ref, { title: x.text })))) : el('p', { class: 'dim cal3-note' }, none));
688
+ one ? null : refLink(ui, x.ref, { title: x.text })))) : el('p', { class: 'dim cal3-note' }, none),
689
+ one ? el('p', { class: 'dim cal3-note cal3-src', 'data-cal3': 'from-note' }, 'From the meeting note. ', refLink(ui, one)) : null);
590
690
  };
591
691
  let body;
592
692
  if (!M.ctx) body = [fold('info', 'The calendar sent no meeting context, so PMOS shows nothing for this meeting.', { 'data-cal3': 'no-context' })];
@@ -599,18 +699,31 @@ function meetingDetail(ui, hit) {
599
699
  const iniTitle = iniId ? (b && b.initiative && b.initiative.id === iniId && b.initiative.title) || (((ui.d.gantt || []).find((g) => g.id === iniId) || {}).title) || iniId : null;
600
700
  const iniStatus = b && b.initiative && b.initiative.id === iniId ? b.initiative.status : (((ui.d.gantt || []).find((g) => g.id === iniId) || {}).status || null);
601
701
  const sources = b && b.sources && b.sources.length ? 'Put together from ' + listWords(b.sources) + '. Each line opens its source. ' : '';
702
+ const chipsRow = () => el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag', 'data-cal3': 'person' }, n)),
703
+ iniId ? el('span', { class: 'tag', 'data-cal3': 'meeting-initiative' }, 'Initiative: ' + iniTitle) : null,
704
+ iniStatus && STATUS_TONE[iniStatus] ? el('span', { class: 'tag cal3-tone-' + STATUS_TONE[iniStatus] }, iniStatus) : null);
705
+ const tail = el('p', { class: 'dim cal3-note', 'data-cal3': past ? 'past-note' : 'sources' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : sources + 'PMOS proposes changes. It never edits your calendar.');
706
+ if (inSheet) {
707
+ // the popup: the sheet's header carries the title and Close; at Half and wider the note sits beside what came out of it
708
+ const side = noteCard(ui, e);
709
+ return [
710
+ el('div', { class: 'cal3-dt' }, el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), !past && s !== null && hit.date === ui.d.today ? focusBtn(e, hit.date, ui) : null),
711
+ el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
712
+ chipsRow(),
713
+ past || !M.ctx ? null : levelRow(ui, e, lv),
714
+ el('div', { class: 'cal3-mcols' + (side ? '' : ' one') }, el('div', { class: 'cal3-mcol' }, ...body, tail), side ? el('div', { class: 'cal3-mcol' }, side) : null),
715
+ ];
716
+ }
602
717
  return [
603
718
  el('div', { class: 'cal3-dt' }, el('h3', null, c3ic('cal'), 'Meeting'), el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'),
604
719
  !past && s !== null && hit.date === ui.d.today ? focusBtn(e, hit.date) : null, closeBtn(ui)),
605
720
  el('b', { class: 'cal3-title' }, e.title || e.id),
606
721
  el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
607
- el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag', 'data-cal3': 'person' }, n)),
608
- iniId ? el('span', { class: 'tag', 'data-cal3': 'meeting-initiative' }, 'Initiative: ' + iniTitle) : null,
609
- iniStatus && STATUS_TONE[iniStatus] ? el('span', { class: 'tag cal3-tone-' + STATUS_TONE[iniStatus] }, iniStatus) : null),
722
+ chipsRow(),
610
723
  past || !M.ctx ? null : levelRow(ui, e, lv),
611
724
  noteLink(ui, e),
612
725
  ...body,
613
- el('p', { class: 'dim cal3-note', 'data-cal3': past ? 'past-note' : 'sources' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : sources + 'PMOS proposes changes. It never edits your calendar.'),
726
+ tail,
614
727
  ];
615
728
  }
616
729
 
@@ -603,6 +603,21 @@
603
603
  .c3 .cal3-dh.today b,.c3 .cal3-dh.today button.cal3-dbtn{color:var(--acc)}
604
604
  .c3 .cal3-gut{font:var(--fs-1) var(--mono);color:var(--muted-foreground);padding:6px 4px}
605
605
  .c3 .cal3-sc{border-left:1px solid var(--border);padding:3px;display:grid;gap:2px;align-content:start;min-height:28px;min-width:0}
606
+ .c3 .cal3-sc.line{display:flex;flex-wrap:wrap;align-items:center;gap:4px}
607
+ .c3 .cal3-sc.line button.cal3-mk,.c3 .cal3-sc.line button.cal3-allday{width:auto;max-width:280px;flex:0 1 auto}
608
+ /* the Reports fold of the dates strip (initiative c3-cal-strip-popup) */
609
+ .c3 .cal3 button.cal3-rf{display:inline-flex;align-items:center;gap:6px;width:auto;max-width:100%;height:auto;min-height:22px;padding:1px 8px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--foreground);font:500 var(--fs-1)/1.4 var(--sans);cursor:pointer;white-space:nowrap;overflow:hidden}
610
+ .c3 .cal3-sc:not(.line) button.cal3-rf{width:100%}
611
+ .c3 .cal3 button.cal3-rf>span{display:flex;gap:6px;align-items:center;min-width:0}
612
+ .c3 .cal3-rf .ic{color:var(--muted-foreground)}
613
+ .c3 .cal3-rf .cal3-n{font:500 var(--fs-1) var(--mono);color:var(--acc)}
614
+ .c3 .cal3-rf-bd{font:var(--fs-1) var(--mono);color:var(--muted-foreground);overflow:hidden;text-overflow:ellipsis;min-width:0}
615
+ .c3 .cal3 button.cal3-rf[aria-expanded="true"]{background:var(--accw)}
616
+ .c3 .cal3-rf-open{display:grid;gap:2px;min-width:0}
617
+ .c3 .cal3-sc.line .cal3-rf-open{flex-basis:100%;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:3px}
618
+ .c3 .cal3-mk-s{margin-left:auto;padding-left:8px;font:var(--fs-1) var(--mono);color:var(--muted-foreground);flex:none}
619
+ .c3 .cal3 button.cal3-mk:has(.cal3-mk-s)>span{width:100%}
620
+ @media (hover:hover){.c3 .cal3 button.cal3-rf:hover{background:var(--accw)}}
606
621
  .c3 .cal3-none{font:var(--fs-1) var(--mono);color:var(--muted-foreground);padding:4px}
607
622
  .c3 .cal3-hours{position:relative;height:var(--cal3-hgt)}
608
623
  .c3 .cal3-hours span{position:absolute;top:var(--cal3-y);right:6px;font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
@@ -714,7 +729,7 @@
714
729
  .c3 .cal3 button.cal3-mk:hover{background:var(--accw)}
715
730
  }
716
731
  @media (pointer:coarse){
717
- .c3 .cal3 button.cal3-chip,.c3 .cal3 button.cal3-mk,.c3 .cal3 button.cal3-dn,.c3 .cal3 button.cal3-dbtn,.c3 .cal3 button.cal3-close,.c3 .cal3 button.cal3-allday,.c3 .cal3 a.cal3-ref{min-height:44px}
732
+ .c3 .cal3 button.cal3-chip,.c3 .cal3 button.cal3-mk,.c3 .cal3 button.cal3-dn,.c3 .cal3 button.cal3-dbtn,.c3 .cal3 button.cal3-close,.c3 .cal3 button.cal3-allday,.c3 .cal3 button.cal3-rf,.c3 .cal3 a.cal3-ref{min-height:44px}
718
733
  }
719
734
  @media (prefers-reduced-motion:reduce){
720
735
  .c3 .cal3 *{animation:none;transition:none}
@@ -2369,3 +2384,14 @@
2369
2384
  .c3ns-err { color: var(--destructive); font-size: var(--fs-2); margin: 0; }
2370
2385
  .c3ns-err:empty { display: none; }
2371
2386
  .c3ns-go { display: flex; justify-content: flex-end; }
2387
+ /* a meeting in a popup (initiative c3-cal-strip-popup): the session sheet's sizes; at Half and wider the note sits beside what came out of it */
2388
+ .c3 .cal3-meet{display:grid;gap:var(--sp-3);min-width:0}
2389
+ .c3 .cal3-msize{flex:none}
2390
+ .c3 .cal3-msize button{padding:4px 8px}
2391
+ .c3.ph .cal3-msize{display:none}
2392
+ .c3 .cal3-mcols{display:grid;gap:var(--sp-4);grid-template-columns:minmax(0,1fr);align-items:start}
2393
+ .c3 .cal3-mcol{display:grid;gap:var(--sp-3);min-width:0;align-content:start}
2394
+ @media (min-width:761px){.c3:not(.ph) .sheet.cal3-ms:not(.tsz-normal) .cal3-mcols:not(.one){grid-template-columns:minmax(0,1.2fr) minmax(0,1fr)}}
2395
+ .c3 .cal3-mnote-b{display:grid;gap:4px;padding:var(--sp-2) var(--sp-3);border:1px solid var(--border);border-radius:8px;background:var(--muted);min-width:0;overflow-wrap:anywhere}
2396
+ .c3 .cal3-mnote-b p{margin:0;font-size:var(--fs-2)}
2397
+ .c3 .cal3-src{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
@@ -63,7 +63,7 @@ export const notesRows = () => rows || [];
63
63
 
64
64
  /** Today's meetings that have not ended and have no note yet, soonest first (the Record control's "upcoming" menu). A meeting that has
65
65
  * started stays in the list until its end plus RECORD_GRACE_MS, so it can still be recorded once it is under way.
66
- * Read from the Agenda: GET /api/proactive/agenda?month=… The answer is kept for UPCOMING_MS; the time filter runs on every call. */
66
+ * Read from the calendar: GET /api/calendar-today (the meetings the Calendar shows). The answer is kept for UPCOMING_MS; the time filter runs on every call. */
67
67
  const UPCOMING_MS = 60000;
68
68
  export const RECORD_GRACE_MS = 10 * 60000;
69
69
  const endOf = (e) => { const f = Date.parse(e.end); if (Number.isFinite(f)) return f; const s = Date.parse(e.start); return Number.isFinite(s) ? s + (Number(e.minutes) || 30) * 60000 : NaN; };
@@ -84,12 +84,11 @@ let upcoming = null; let upcomingAt = 0; let upcomingReading = null;
84
84
  export function upcomingToday({ force = false } = {}) {
85
85
  if (!force && upcoming && Date.now() - upcomingAt < UPCOMING_MS) return Promise.resolve(live(upcoming));
86
86
  if (upcomingReading) return upcomingReading.then(live);
87
- const d = new Date(); const pad = (n) => String(n).padStart(2, '0');
88
- const day = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
89
- // the Agenda gives the event; today's brief gives the plan for it (purpose, how to be ready, steps, links, related items)
90
- upcomingReading = Promise.all([pmGet('/api/proactive/agenda?month=' + day.slice(0, 7)), pmGet('/api/proactive/today')]).then(([r, t]) => {
87
+ // the calendar gives the event (every meeting the Calendar shows: imported, remembered and today's brief); today's brief gives the plan for it
88
+ // (purpose, how to be ready, steps, links, related items). The Agenda alone missed the imported meetings.
89
+ upcomingReading = Promise.all([pmGet('/api/calendar-today'), pmGet('/api/proactive/today')]).then(([r, t]) => {
91
90
  upcomingReading = null; upcomingAt = Date.now();
92
- const evs = r && r.days && r.days[day] ? r.days[day].events || [] : [];
91
+ const evs = r && r.ok && Array.isArray(r.events) ? r.events : [];
93
92
  const cal = t && t.ok && t.brief && Array.isArray(t.brief.calendar) ? t.brief.calendar : [];
94
93
  const planOf = (e) => cal.find((c) => minuteOf(c.start) === minuteOf(e.start) && normTitle(c.title) === normTitle(e.title));
95
94
  const merged = evs.map((e) => { const p = planOf(e); return p ? { ...e, with: e.with || p.with, purpose: p.purpose, prep: typeof p.prep === 'string' ? p.prep : e.prep, steps: p.steps, links: p.links, refs: p.refs, initiative: p.initiative } : e; });
@@ -178,6 +178,12 @@ function openFocusFor(m) { // no date: the page takes the local day of the sta
178
178
  .catch(() => toast({ text: 'The focus page could not load.', tone: 'error' }));
179
179
  }
180
180
 
181
+ /** Meeting focus for a recording that is not on the calendar (an untitled note): the page opens on the note, with no topics or brief. */
182
+ function openFocusNote(id, title) {
183
+ import('./c3-focus.js').then((f) => f.openFocus({ note: id, event: { title: title || 'Untitled note' } }))
184
+ .catch(() => toast({ text: 'The focus page could not load.', tone: 'error' }));
185
+ }
186
+
181
187
  function mountReal(host) {
182
188
  ensureC3Css();
183
189
  let ready = false;
@@ -232,7 +238,7 @@ function mountReal(host) {
232
238
  group.push(btn(hl ? 'Highlight in progress: show it' : 'Highlight the last 30 seconds and what is said next', 'highlighter', () => dock.highlightNow(), { 'data-fr-rec-highlight': hl ? hl.state : '', 'data-shell-rec-mark': '' }),
233
239
  btn('Pause (you can resume within 1 hour)', 'pause', () => dock.pauseNow(), { 'data-fr-rec-pause': '', 'data-shell-rec-pause': '' }));
234
240
  } else group.push(btn('Resume ' + st.title, 'play', () => dock.resumeNow(), { 'data-fr-rec-resume': '', 'data-shell-rec-resume': '' }));
235
- if (mt) group.push(btn('Open the focus page for this meeting', 'focus', () => openFocusFor(mt), { 'data-fr-rec-focus': '', 'data-shell-rec-focus': '' }));
241
+ group.push(btn('Open the focus page for this meeting', 'focus', () => (mt ? openFocusFor(mt) : openFocusNote(st.id, st.title)), { 'data-fr-rec-focus': '', 'data-shell-rec-focus': '' }));
236
242
  group.push(btn('Stop for good (cannot be resumed)', 'square', () => dock.stopNow(), { 'data-fr-rec-stop': '', 'data-shell-rec-stop': '' }));
237
243
  kids.push(el('span', { class: 'recg c3rec__live' + (recording ? '' : ' paused'), role: 'group', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + st.title }, group));
238
244
  }