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 +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-light-load-test.js +5 -4
- package/workspace/guide/console-calendar.md +8 -4
- package/workspace/guide/notes.md +1 -1
- package/workspace/lib/calendar-events.js +22 -3
- package/workspace/lib/calendar-layers.js +11 -2
- package/workspace/lib/routes-calendar-views.js +13 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-calendar.js +128 -15
- package/workspace/public/pm/console3.css +27 -1
- package/workspace/public/pm/notes-links.js +5 -6
- package/workspace/public/pm/shell-recording.js +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "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": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
package/workspace/guide/notes.md
CHANGED
|
@@ -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
|
|
11
|
-
* meetings came in with the morning brief saw them under Record and nowhere else. This reader joins the three.
|
|
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
|
-
|
|
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: `${
|
|
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
|
-
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.225.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -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) => {
|
|
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
|
|
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')), ...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
}
|