create-pmos 0.143.0 → 0.145.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-ib-triage-backlog-test.js +1 -0
- package/workspace/c3-updates-test.js +131 -0
- package/workspace/guide/console-calendar.md +20 -2
- package/workspace/guide/notes.md +2 -2
- package/workspace/guide/settings.md +9 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-calendar.js +35 -4
- package/workspace/public/pm/c3-notes.js +316 -0
- package/workspace/public/pm/c3-settings.js +42 -9
- package/workspace/public/pm/calendar-views.js +15 -1
- package/workspace/public/pm/console3.css +40 -0
- package/workspace/public/pm/shell-recording.js +7 -3
- package/workspace/test-files.json +4 -2
- package/workspace/test-lists/c3-notes-place.json +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.145.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-12T01:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T01:59:40Z"}
|
|
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-12T01:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T01:59:40Z"}
|
|
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-12T01:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T01:59:40Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -37,6 +37,7 @@ const { harness } = require('./c3-ini-harness');
|
|
|
37
37
|
await page.fill('[data-ibt="f-more"]', 'Which trials need SSO first?');
|
|
38
38
|
await page.click('[data-ibt="more-go"]');
|
|
39
39
|
await until(() => store(() => window.__c3boards.asks.find((a) => a.id === 'ask-1').needMore === true));
|
|
40
|
+
await until(async () => /prompted for more detail/.test(await toast()));
|
|
40
41
|
ok('T6 Ask for more records the question; the ask stays open', await store(() => { const a = window.__c3boards.asks.find((x) => x.id === 'ask-1'); return a.state === 'open' && a.moreNote === 'Which trials need SSO first?'; }) && /prompted for more detail/.test(await toast()));
|
|
41
42
|
|
|
42
43
|
await page.click('[data-ibt-ask="ask-1"] .tt').catch(() => {});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/* c3-updates-test.js — console3 Settings › Updates runs the app update itself, and the kit Update sends the checked version
|
|
4
|
+
* (initiative c3-app-update).
|
|
5
|
+
*
|
|
6
|
+
* The page used to say "run it from the main app" and link to /, and its kit Update posted no version, which the server
|
|
7
|
+
* refuses with "Check for updates first". Against a real server; the kit routes and /api/sessions are stood in by the
|
|
8
|
+
* page's route table, so nothing is installed and npm is never asked. Hooks only (data-c3set-*).
|
|
9
|
+
*/
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { spawn } = require('child_process');
|
|
12
|
+
const { findBrowser } = require('./lib/find-browser');
|
|
13
|
+
const T = require('./test-run').configure({ name: 'c3-updates' });
|
|
14
|
+
|
|
15
|
+
const BASE = T.BASE;
|
|
16
|
+
const results = [];
|
|
17
|
+
const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + extra : ''}`); };
|
|
18
|
+
let srv = null; let browser = null;
|
|
19
|
+
const finish = async (code) => {
|
|
20
|
+
if (browser) await browser.close().catch(() => {});
|
|
21
|
+
if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
|
|
22
|
+
T.killTmux();
|
|
23
|
+
const bad = results.filter((x) => !x.cond).length;
|
|
24
|
+
console.log(`\n${results.length - bad}/${results.length} c3-updates assertions passed`);
|
|
25
|
+
process.exit(code !== undefined ? code : (bad ? 1 : 0));
|
|
26
|
+
};
|
|
27
|
+
process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
28
|
+
|
|
29
|
+
async function waitFor(page, fn, arg, ms = 8000) {
|
|
30
|
+
try { await page.waitForFunction(fn, arg, { timeout: ms }); return true; } catch { return false; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
(async () => {
|
|
34
|
+
const exe = findBrowser();
|
|
35
|
+
if (!exe) { console.log('✗ no Chrome/Chromium on this machine — the c3-updates suite cannot run'); return finish(1); }
|
|
36
|
+
let chromium;
|
|
37
|
+
try { ({ chromium } = require('playwright-core')); } catch { console.log('✗ playwright-core is not installed — the c3-updates suite cannot run'); return finish(1); }
|
|
38
|
+
|
|
39
|
+
srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
|
|
40
|
+
await T.waitForServer(BASE);
|
|
41
|
+
browser = await chromium.launch({ executablePath: exe });
|
|
42
|
+
const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
|
43
|
+
const page = await ctx.newPage();
|
|
44
|
+
const errors = [];
|
|
45
|
+
page.on('pageerror', (e) => errors.push(String(e && e.message || e)));
|
|
46
|
+
|
|
47
|
+
// The stand-ins: an installed app 0.1.0 and a kit 0.1.0 in every workspace, both behind 0.2.0.
|
|
48
|
+
const S = { app: '0.1.0', kit: '0.1.0', checkout: false, upgrade: { ok: true }, down: 0 };
|
|
49
|
+
const calls = [];
|
|
50
|
+
const body = (ws) => ({ ok: true, ws, app: { version: S.app, fromCheckout: S.checkout }, kit: { present: true, version: S.kit }, source: { kind: 'npm' } });
|
|
51
|
+
const wsOf = (route) => { try { return JSON.parse(route.request().postData() || '{}'); } catch { return {}; } };
|
|
52
|
+
await page.route((u) => u.pathname === '/api/kit/status', (route) => route.fulfill({ json: { ...body(new URL(route.request().url()).searchParams.get('ws')), busy: null } }));
|
|
53
|
+
await page.route((u) => u.pathname === '/api/kit/check', (route) => { const b = wsOf(route); calls.push(['check', b]); route.fulfill({ json: { ...body(b.ws), latest: '0.2.0', appBehind: S.app !== '0.2.0', kitBehind: S.kit !== '0.2.0' } }); });
|
|
54
|
+
await page.route((u) => u.pathname === '/api/kit/update', (route) => {
|
|
55
|
+
const b = wsOf(route); calls.push(['kit', b]);
|
|
56
|
+
if (b.version !== '0.2.0') return route.fulfill({ status: 502, json: { ok: false, error: 'Check for updates first' } });
|
|
57
|
+
route.fulfill({ json: { ...body(b.ws), kit: { present: true, version: '0.2.0' }, output: 'kit updated' } });
|
|
58
|
+
});
|
|
59
|
+
await page.route((u) => u.pathname === '/api/kit/upgrade-app', (route) => {
|
|
60
|
+
const b = wsOf(route); calls.push(['app', b]);
|
|
61
|
+
if (!S.upgrade.ok) return route.fulfill({ status: 403, json: S.upgrade });
|
|
62
|
+
S.down = 2; // the server is away for two polls, then answers on the new version
|
|
63
|
+
route.fulfill({ json: { ok: true, ws: b.ws, from: '0.1.0', to: b.version, log: '/tmp/app-update.log' } });
|
|
64
|
+
});
|
|
65
|
+
let reloaded = false;
|
|
66
|
+
await page.route((u) => u.pathname === '/api/sessions', async (route) => {
|
|
67
|
+
if (!calls.some((c) => c[0] === 'app' && S.upgrade.ok)) return route.continue();
|
|
68
|
+
if (S.down > 0) { S.down -= 1; return route.abort('connectionrefused'); }
|
|
69
|
+
S.app = '0.2.0';
|
|
70
|
+
const r = await route.fetch(); const j = await r.json().catch(() => ({}));
|
|
71
|
+
return route.fulfill({ json: { ...j, version: S.app } });
|
|
72
|
+
});
|
|
73
|
+
page.on('framenavigated', (f) => { if (f === page.mainFrame() && calls.some((c) => c[0] === 'app')) reloaded = true; });
|
|
74
|
+
const named = (kind) => calls.filter((c) => c[0] === kind);
|
|
75
|
+
|
|
76
|
+
const open = async () => {
|
|
77
|
+
await page.goto(BASE + '/c3.html?tab=settings');
|
|
78
|
+
await waitFor(page, () => !!document.querySelector('[data-c3set-nav] [data-c3set-sec="updates"]'));
|
|
79
|
+
await page.click('[data-c3set-nav] [data-c3set-sec="updates"]');
|
|
80
|
+
return waitFor(page, () => !!document.querySelector('[data-c3set-section="updates"] [data-c3set-check]:not([disabled])'));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/* ── A: before a check, nothing is offered and npm is not asked ── */
|
|
84
|
+
ok('A1 Updates opens with Check now', await open());
|
|
85
|
+
ok('A2 no app Update and no Open the main app before a check', await page.evaluate(() => !document.querySelector('[data-c3set-app-update]') && !/Open the main app/.test(document.querySelector('[data-c3set-section="updates"]').textContent)));
|
|
86
|
+
ok('A3 nothing was asked of npm before the press', calls.length === 0, JSON.stringify(calls));
|
|
87
|
+
|
|
88
|
+
/* ── B: the kit Update sends the version the check found ── */
|
|
89
|
+
await page.click('[data-c3set-check]');
|
|
90
|
+
ok('B1 after the check, the app row offers Update to 0.2.0', await waitFor(page, () => { const b = document.querySelector('[data-c3set-app-update]'); return !!b && /Update to 0\.2\.0/.test(b.textContent); }));
|
|
91
|
+
ok('B2 the page no longer sends you to the main app', await page.evaluate(() => !/Open the main app/.test(document.querySelector('[data-c3set-section="updates"]').textContent)));
|
|
92
|
+
ok('B3 each workspace row offers a kit Update', await waitFor(page, () => !!document.querySelector('[data-c3set-kit-update]')));
|
|
93
|
+
const kitWs = await page.getAttribute('[data-c3set-kit-update]', 'data-c3set-kit-update');
|
|
94
|
+
await page.click(`[data-c3set-kit-update="${kitWs}"]`);
|
|
95
|
+
ok('B4 kit Update posts that workspace and the checked version', await waitFor(page, () => !!document.querySelector('[data-c3-toast]') && /Updated the kit/.test(document.querySelector('[data-c3-toast]').textContent)) && named('kit').length === 1 && named('kit')[0][1].ws === kitWs && named('kit')[0][1].version === '0.2.0', JSON.stringify(named('kit')));
|
|
96
|
+
ok('B5 that row no longer offers Update', await waitFor(page, (w) => !document.querySelector(`[data-c3set-kit-update="${w}"]`), kitWs));
|
|
97
|
+
|
|
98
|
+
/* ── C: the app update asks first, then runs, waits for the restart and reloads ── */
|
|
99
|
+
await page.click('[data-c3set-app-update]');
|
|
100
|
+
ok('C1 it asks first, and says PMOS stops while the new version installs', await waitFor(page, () => { const c = document.querySelector('[data-c3set-app-confirm]'); return !!c && /stops/.test(c.textContent) && !!document.querySelector('[data-c3set-app-go]'); }));
|
|
101
|
+
ok('C2 nothing runs before the answer', named('app').length === 0);
|
|
102
|
+
await page.click('[data-c3set-app-cancel]');
|
|
103
|
+
ok('C3 Cancel puts the Update button back', await waitFor(page, () => !document.querySelector('[data-c3set-app-confirm]') && !!document.querySelector('[data-c3set-app-update]')) && named('app').length === 0);
|
|
104
|
+
await page.click('[data-c3set-app-update]');
|
|
105
|
+
await page.click('[data-c3set-app-go]');
|
|
106
|
+
for (let i = 0; i < 40 && !named('app').length; i++) await new Promise((r) => setTimeout(r, 100));
|
|
107
|
+
ok('C4 the update posts the checked version', named('app').length === 1 && named('app')[0][1].version === '0.2.0', JSON.stringify(named('app')));
|
|
108
|
+
ok('C5 the row says it is updating and that the page reloads', await waitFor(page, () => { const b = document.querySelector('[data-c3set-app-busy]'); return !!b && /reloads/.test(b.textContent); }, null, 3000) || reloaded);
|
|
109
|
+
for (let i = 0; i < 200 && !reloaded; i++) await new Promise((r) => setTimeout(r, 100));
|
|
110
|
+
ok('C6 the page reloads by itself when the new version answers', reloaded);
|
|
111
|
+
|
|
112
|
+
/* ── D: a refusal is shown on the row ── */
|
|
113
|
+
S.app = '0.1.0'; S.upgrade = { ok: false, error: 'This page signed in with a token that changes when the workspace restarts' };
|
|
114
|
+
calls.length = 0; reloaded = false;
|
|
115
|
+
await open();
|
|
116
|
+
await page.click('[data-c3set-check]');
|
|
117
|
+
await waitFor(page, () => !!document.querySelector('[data-c3set-app-update]'));
|
|
118
|
+
await page.click('[data-c3set-app-update]');
|
|
119
|
+
await page.click('[data-c3set-app-go]');
|
|
120
|
+
ok('D1 a refused update says why, on the app row', await waitFor(page, () => { const r = document.querySelector('[data-c3set-row="upd.app"]'); return !!r && /token that changes/.test(r.textContent); }));
|
|
121
|
+
ok('D2 a refused update does not reload the page', !reloaded);
|
|
122
|
+
|
|
123
|
+
/* ── E: a checkout updates with git ── */
|
|
124
|
+
S.checkout = true; S.upgrade = { ok: true }; calls.length = 0;
|
|
125
|
+
await open();
|
|
126
|
+
await page.click('[data-c3set-check]');
|
|
127
|
+
ok('E1 a checkout says git pull and offers no app Update', await waitFor(page, () => /git pull/.test(document.querySelector('[data-c3set-row="upd.app"]').textContent)) && !(await page.$('[data-c3set-app-update]')));
|
|
128
|
+
|
|
129
|
+
ok('F1 the page throws no error', errors.length === 0, errors.slice(0, 3).join(' | '));
|
|
130
|
+
finish();
|
|
131
|
+
})();
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: the Calendar tab of the console page"
|
|
4
|
-
description: "The Calendar tab of the console page (/c3.html): the five zooms, the layer chips, the markers, the context of a meeting, free time, the tidy-up suggestions and the empty states."
|
|
5
|
-
timestamp: 2026-10-
|
|
4
|
+
description: "The Calendar tab of the console page (/c3.html): Calendar and Notes, the five zooms, the layer chips, the markers, the context of a meeting, free time, the tidy-up suggestions and the empty states."
|
|
5
|
+
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: the Calendar tab of the console page
|
|
9
9
|
|
|
10
10
|
The Calendar tab of the console page (`/c3.html`) shows time. It shows what happens and what falls due, by date. At the top it shows a short deck of headline cards about your calendar, for example a conflict or a meeting with no time to prepare. The deck shows nothing when PMOS has no card about your calendar. The tab shows no task list and no strategy panel. Your due items live in Work. The strategy and progress panels live in Reports and Initiatives.
|
|
11
11
|
|
|
12
|
+
## Calendar and Notes
|
|
13
|
+
|
|
14
|
+
The switch at the start of the bar has two parts. **Calendar** shows the grid. **Notes** shows your notes.
|
|
15
|
+
|
|
16
|
+
In **Notes**, the list is at the left. The notes are in groups, one for each initiative. **No initiative** is the last group. The number shows how many notes the list shows. Type in **Search notes** to keep only the notes whose title, initiative or summary has the words. A row shows when the note was made and its length. A row also shows **recording** or **paused** while the note is held, **no meeting** when the note has no meeting, and a warning mark when a statement differs from a file.
|
|
17
|
+
|
|
18
|
+
Press a row to open the note at the right. The note has three stages. Press **Before**, **During** or **After** to change the stage.
|
|
19
|
+
|
|
20
|
+
- **Before** shows what the note kept about its meeting: the agenda, what the meeting is for, what to do to be ready, what to read first and the people. A tick shows an agenda item that was heard in the transcript. A note with no meeting says so, and has **Link to a meeting** and **Link to an initiative**.
|
|
21
|
+
- **During** shows the live notes while the note is recorded or paused. At other times it shows the transcript.
|
|
22
|
+
- **After** shows the overview, the decisions with why, the next steps with owner and due date, the open questions, the statements that differ from a file and the highlights. The transcript is folded at the bottom. When the final notes are written, click a text to correct it. Leave the text to save it. Press `Esc` to cancel. **Make task** adds a next step to the note's initiative.
|
|
23
|
+
|
|
24
|
+
A note that is recorded now opens on **During**. Other notes open on **After**. **Show in the calendar** goes back to the grid, on the day of the meeting, with the meeting selected. The copy button puts the note on the clipboard as Markdown.
|
|
25
|
+
|
|
26
|
+
In **Notes**, the zoom, the arrows and the calendar keys do nothing.
|
|
27
|
+
|
|
12
28
|
## Zoom and move
|
|
13
29
|
|
|
14
30
|
Use **Day**, **Week**, **Month**, **Quarter** or **Year** to change the zoom. Use the arrows to go to the previous or the next period. Press **Today** to come back to today.
|
|
@@ -53,6 +69,8 @@ Press a meeting to show its context in the side column. Under the title, a row s
|
|
|
53
69
|
- 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.
|
|
54
70
|
- For a coming meeting, **Prepared** lists what is ready, and **To prepare** lists what is still open.
|
|
55
71
|
|
|
72
|
+
When the meeting has a note, the detail has **Open the note**. It opens the note in **Notes**.
|
|
73
|
+
|
|
56
74
|
When PMOS holds nothing for a meeting, the detail says so. It does not guess.
|
|
57
75
|
|
|
58
76
|
## Free time
|
package/workspace/guide/notes.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: notes"
|
|
4
4
|
description: "How to take notes in a meeting: one note top to bottom, typed notes as timed lines, key points written each minute and labelled by Jev, delete a note; final notes written after Stop from the whole recording, with speakers and a quote for each item, decisions with their why and who, next steps with an open question, a discussion paragraph, and Wispr Flow's notes merged in and fetched by hand when the fetch fails; listen to your microphone and the meeting on this machine, see decisions, actions and questions caught as they are said, check them against the repository for conflicts, capture highlights during the meeting, create tasks from next steps, correct and tag lines, see what changed in your notes, replay the meeting, see screenshots of the shared screen beside the transcript, link a note to its meeting and its initiative, and get a summary."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: notes
|
|
@@ -22,7 +22,7 @@ Starting a note, from the Record control or from **Take notes** on a calendar ev
|
|
|
22
22
|
|
|
23
23
|
### The Record control in the console
|
|
24
24
|
|
|
25
|
-
The console (the page at `/c3.html`) has the same Record control in its top bar, beside the Sessions button. It is always there, also when nothing is recording. **Record** (the microphone) starts an untitled note and listens at once. The arrow lists the meetings of today that have not started. Press one to start a note linked to that calendar event. While a meeting records, the red group shows its name and clock, **Highlight**, **Pause** and **Stop**. While it is paused, the group shows **Resume** and **Stop**. **Stop** asks first and offers **Pause instead**. A red dot shows on the Calendar tab while a meeting is held.
|
|
25
|
+
The console (the page at `/c3.html`) has the same Record control in its top bar, beside the Sessions button. It is always there, also when nothing is recording. **Record** (the microphone) starts an untitled note and listens at once. The arrow lists the meetings of today that have not started. Press one to start a note linked to that calendar event. While a meeting records, the red group shows its name and clock, **Highlight**, **Pause** and **Stop**. While it is paused, the group shows **Resume** and **Stop**. **Stop** asks first and offers **Pause instead**. A red dot shows on the Calendar tab while a meeting is held. Press the name in the red group to open the note on the console's Calendar tab, in **Notes**, on the **During** stage. See [the Calendar tab of the console page](console-calendar.md).
|
|
26
26
|
|
|
27
27
|
When Whisper or Qwen3-ASR transcribes the meeting, the red group also names the engine and the language, for example **Whisper · English**. Check it when lines come out in the wrong language: the language is set in the Notes settings.
|
|
28
28
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Settings in console3: one home for each setting"
|
|
4
4
|
description: "How to open Settings in console3, what each section holds, what the scope tags and the environment notes mean, and where the settings are kept."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Settings in console3: one home for each setting
|
|
@@ -55,6 +55,14 @@ When a section has changed settings, **Reset section** at the top of the section
|
|
|
55
55
|
|
|
56
56
|
Some rows show a feature that PMOS does not have yet, such as choosing calendars, mapping Jira projects, pairing a phone, the device list, and export or reset of every setting. The control on these rows is off, and the row says why.
|
|
57
57
|
|
|
58
|
+
## Update PMOS and the kit
|
|
59
|
+
|
|
60
|
+
Open **Updates** and press **Check now**. PMOS asks the npm registry for the latest version only when you press it.
|
|
61
|
+
|
|
62
|
+
- **Update to** a version on the **PMOS app** row updates the app. The row asks first: press **Restart and update**, or **Cancel**. PMOS stops, installs the new version, and starts again. Open terminals keep running. The page reloads when the new version starts. If the update is refused, or PMOS starts again on the old version, or PMOS does not come back, the row says so and names the update log.
|
|
63
|
+
- When PMOS runs from a PMOS checkout, the row says to update it with `git pull`. There is no Update button.
|
|
64
|
+
- **Update** on a workspace row updates the kit in that repository. It replaces the kit files in its `pmos/` folder (AGENTS.md, skills, scripts, templates, `okf/core`, the mod and KIT-VERSION). It does not change `state/`, `planning/` or `okf/product/`, and it does not commit. Review the change in the repository as a normal pull request.
|
|
65
|
+
|
|
58
66
|
## Where the settings are kept
|
|
59
67
|
|
|
60
68
|
The settings that had a home before this page keep it. For example, the recording engine is kept by the notes engine, Validate consent by the validate route, and the theme and the new-session defaults in this browser.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.145.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.145.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -21,6 +21,8 @@ import { el } from './dom.js';
|
|
|
21
21
|
import { c3ic } from './pm-work-view.js';
|
|
22
22
|
import { button, ensureComponentsCss } from './primitives.js';
|
|
23
23
|
import { toast } from './c3-toast.js';
|
|
24
|
+
import { notesView, openNote, noteOfEvent, primeNotes } from './c3-notes.js'; // Calendar › Notes (initiative c3-notes-place)
|
|
25
|
+
import { onNotes } from './notes-links.js';
|
|
24
26
|
|
|
25
27
|
const LAYERS = [
|
|
26
28
|
['meetings', 'Meetings'],
|
|
@@ -63,6 +65,8 @@ const setVar = (node, name, value) => { node.style.setProperty('--cal3-' + name,
|
|
|
63
65
|
function state(r) {
|
|
64
66
|
const st = r.page || (r.page = { on: new Set(LAYERS.map((l) => l[0]).filter((id) => !(r.ctx && r.ctx.overlay && mem.hide.includes(id)))), free: true, tidy: false, sel: null });
|
|
65
67
|
if (r.sel) { st.sel = { type: 'event', id: r.sel }; r.sel = null; } // a meeting chosen on the Briefing (openDay)
|
|
68
|
+
if (r.pendingMode) { st.mode = r.pendingMode; r.pendingMode = null; } // a note opened before the page drew (the Record chip)
|
|
69
|
+
if (!st.mode) st.mode = 'cal';
|
|
66
70
|
return st;
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -99,10 +103,21 @@ export function drawCalendarPage(host, r, h) {
|
|
|
99
103
|
ensureComponentsCss(); // the Tidy-up buttons are pmc-btn
|
|
100
104
|
const st = state(r);
|
|
101
105
|
const phone = r.root.classList.contains('ph');
|
|
106
|
+
const page = (b, ...kids) => el('div', { class: 'cal3' + (r.ctx.overlay ? ' ov' : ''), 'data-cal3': 'page', 'data-cal3-show': st.mode }, r.ctx.deck || null, b, ...kids);
|
|
107
|
+
const redraw0 = () => h.redraw(host, r);
|
|
108
|
+
const modes = r.ctx.overlay ? null : modeSeg(st, redraw0);
|
|
109
|
+
if (st.mode === 'notes' && modes) { // Calendar › Notes: the list and the open note; the zoom and the period have nothing to step here
|
|
110
|
+
return page(el('div', { class: 'calbar' }, el('div', { class: 'lens' }, modes)), notesView({ host, r, h, st, redraw: redraw0 }));
|
|
111
|
+
}
|
|
112
|
+
if (!r.notesOff) { // a meeting's "Open the note" needs the shared notes read; a new note redraws the open detail
|
|
113
|
+
r.notesOff = onNotes(() => { if (r.root.isConnected && st.mode === 'cal' && st.sel) h.redraw(host, r); });
|
|
114
|
+
primeNotes();
|
|
115
|
+
}
|
|
102
116
|
const bar = h.bar(host, r, phone);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (
|
|
117
|
+
if (modes) { const lens = bar.querySelector('.lens'); if (lens) lens.prepend(modes); }
|
|
118
|
+
const pageBar = (...kids) => page(bar, ...kids);
|
|
119
|
+
if (r.error) return pageBar(fold('alert', 'The calendar could not be read: ' + r.error, { 'data-cv': 'error', role: 'status' }));
|
|
120
|
+
if (!r.data) return pageBar(fold('clock', 'Reading the repository', { 'data-cv': 'loading', role: 'status' }));
|
|
106
121
|
|
|
107
122
|
const d = r.data; const M = model(d, h);
|
|
108
123
|
const redraw = (focusSel) => { h.redraw(host, r); if (focusSel) { const n = r.root.querySelector(focusSel); if (n) n.focus(); } };
|
|
@@ -133,7 +148,22 @@ export function drawCalendarPage(host, r, h) {
|
|
|
133
148
|
el('div', { class: 'cal3-wrap' },
|
|
134
149
|
el('div', { class: 'cal3-main' }, empty, grid),
|
|
135
150
|
el('div', { class: 'cal3-side' }, detail(ui), st.tidy ? tidyList(ui) : null, st.free ? freeList(ui, range) : null, lanes(ui, range))));
|
|
136
|
-
return
|
|
151
|
+
return pageBar(chips(ui, range), body);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* "Open the note": the note recorded for this meeting, on Calendar › Notes; nothing when no note names the meeting */
|
|
155
|
+
function noteLink(ui, e) {
|
|
156
|
+
if (ui.r.ctx.overlay) return null;
|
|
157
|
+
const n = noteOfEvent(e); if (!n) return null;
|
|
158
|
+
return el('div', { class: 'cal3-ctx', 'data-cal3-ctx': 'note' }, el('h4', null, 'Notes'),
|
|
159
|
+
el('a', { href: '#', class: 'cal3-ref', 'data-cal3': 'open-note', 'data-cal3-open-note': n.id, title: 'Open the note: ' + (n.title || 'Untitled note'),
|
|
160
|
+
onClick: (ev) => { ev.preventDefault(); openNote(ui.r, n.id); ui.redraw(); } }, c3ic('notes', 'sm'), 'Open the note'));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* the Calendar | Notes switch, before the zoom (the mockup the PM approved on 2026-10-12) */
|
|
164
|
+
function modeSeg(st, redraw) {
|
|
165
|
+
const b = (id, icon, label) => { const x = button({ variant: 'ghost', attrs: { class: 'cal3-b', 'aria-pressed': String(st.mode === id), 'data-cal3-mode': id, title: label }, onClick: () => { if (st.mode !== id) { st.mode = id; redraw(); } } }); x.replaceChildren(c3ic(icon, 'sm'), el('span', null, label)); return x; };
|
|
166
|
+
return el('div', { class: 'seg', role: 'group', 'aria-label': 'Show', 'data-cal3': 'mode' }, b('cal', 'cal', 'Calendar'), b('notes', 'notes', 'Notes'));
|
|
137
167
|
}
|
|
138
168
|
|
|
139
169
|
function fold(icon, text, attrs) { return el('div', { class: 'fold', ...(attrs || {}) }, c3ic(icon, 'sm'), text); }
|
|
@@ -424,6 +454,7 @@ function meetingDetail(ui, hit) {
|
|
|
424
454
|
el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
|
|
425
455
|
el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag' }, n)),
|
|
426
456
|
e.initiative ? el('span', { class: 'tag' }, 'Initiative: ' + (((ui.d.gantt || []).find((g) => g.id === e.initiative) || {}).title || e.initiative)) : null),
|
|
457
|
+
noteLink(ui, e),
|
|
427
458
|
...body,
|
|
428
459
|
el('p', { class: 'dim cal3-note' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : 'PMOS proposes changes. It never edits your calendar.'),
|
|
429
460
|
];
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/* pm/c3-notes.js — Calendar › Notes on the console3 Calendar tab (initiative c3-notes-place, PRD done-criteria 1 to 7).
|
|
2
|
+
*
|
|
3
|
+
* c3-calendar.js draws it when the Calendar | Notes switch is on Notes: notesView(ui) returns one persistent box, which this
|
|
4
|
+
* file redraws on its own (a read, a choice, a stage, a search), so the grid's data call is not repeated.
|
|
5
|
+
*
|
|
6
|
+
* the list "By initiative" with a count and a search field, one group per initiative and "No initiative" last (dest-notes.js
|
|
7
|
+
* groupNotes). A row: the time or day, the length, recording or paused, "no meeting", and a dot for a conflict.
|
|
8
|
+
* the note the Calendar stage's layout (calendar-views.js stageNode, the mockup the PM approved on 2026-10-12): a head with
|
|
9
|
+
* Before, During and After, then the stage's cards.
|
|
10
|
+
* Before the meeting context the note kept (meeting.context): agenda (an item heard is ticked), what it is for, to be ready,
|
|
11
|
+
* links and references, people. A note with no meeting says so and offers the main app's link pickers.
|
|
12
|
+
* During the main app's live block while the note is recorded or paused (view-notes.js renderLive), else the transcript.
|
|
13
|
+
* After the Notes card (decisions with why, next steps with owner, due and Make task, questions, conflicts, highlights,
|
|
14
|
+
* the folded transcript) from final_notes, else from what was caught live (dest-notes.js partsOf).
|
|
15
|
+
*
|
|
16
|
+
* Nothing is invented: a part the note does not hold is left out or says so. The mockup's Rejected column, Settle, told and
|
|
17
|
+
* not told, Route actions and the drafts have no data or route yet, so they are not drawn.
|
|
18
|
+
*
|
|
19
|
+
* openNote(r, id, phase) shows a note (from the Record chip or a meeting's detail) on the instance r
|
|
20
|
+
* noteOfEvent(e) the note linked to a calendar event, from the shared notes read (notes-links.js), or null
|
|
21
|
+
*/
|
|
22
|
+
import { el } from './dom.js';
|
|
23
|
+
import { button } from './primitives.js';
|
|
24
|
+
import { c3ic } from './pm-work-view.js';
|
|
25
|
+
import { pmGet, pmPost } from './http.js';
|
|
26
|
+
import { toast } from './c3-toast.js';
|
|
27
|
+
import { onNotes, notesFor, refreshNotes } from './notes-links.js';
|
|
28
|
+
import { agendaHeard, peopleOf } from './notes-context.js';
|
|
29
|
+
import { groupNotes, partsOf, conflictsOf, whenShort } from './dest-notes.js';
|
|
30
|
+
import * as dock from './note-dock.js';
|
|
31
|
+
import * as V from './view-notes.js';
|
|
32
|
+
import { makeTask } from './notes-task.js';
|
|
33
|
+
|
|
34
|
+
const PHASES = [['before', 'clip', 'Before'], ['during', 'mic', 'During'], ['after', 'notes', 'After']];
|
|
35
|
+
const startOf = (n) => (n.meeting && n.meeting.start) || n.recorded_from || n.created_at || n.updated_at;
|
|
36
|
+
const iniName = (i) => (i ? i.title || i.id : null);
|
|
37
|
+
const mins = (ms) => (ms >= 60000 ? Math.round(ms / 60000) + ' min' : '');
|
|
38
|
+
const lenOf = (n) => n.meeting_ms || Math.max(0, ...(n.transcript || []).map((x) => x.t || 0));
|
|
39
|
+
const dayOf = (iso) => { const t = new Date(iso); return Number.isFinite(t.getTime()) ? t.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' }) : ''; };
|
|
40
|
+
const hm = (iso) => { const t = new Date(iso); return Number.isFinite(t.getTime()) ? String(t.getHours()).padStart(2, '0') + ':' + String(t.getMinutes()).padStart(2, '0') : ''; };
|
|
41
|
+
const avatar = (name) => el('span', { class: 'av', title: name }, String(name).split(/\s+/).map((w) => w[0]).slice(0, 2).join('').toUpperCase());
|
|
42
|
+
/* A native button from primitives.js drawn as the console3 control `cls` (chip2 lk, ib, sgb, …); cal3-b takes the primitive's own
|
|
43
|
+
* height, padding and hover off (console3.css), as c3-ini-ui.js btn() does with ini-b. */
|
|
44
|
+
function btn(cls, attrs, kids, onClick) {
|
|
45
|
+
const b = button({ variant: 'ghost', onClick, attrs: { ...(attrs || {}), class: 'cal3-b ' + cls } });
|
|
46
|
+
b.replaceChildren(...[].concat(kids).filter((k) => k != null && k !== false));
|
|
47
|
+
return b;
|
|
48
|
+
}
|
|
49
|
+
const fold = (icon, text, attrs) => el('div', { class: 'fold', ...(attrs || {}) }, c3ic(icon, 'sm'), text);
|
|
50
|
+
const held = (id) => { const s = dock.recordState(); return s.mode !== 'idle' && s.id === id ? s.mode : null; };
|
|
51
|
+
const linked = (n) => !!(n.meeting && (n.meeting.kind === 'event' || n.meeting.kind === 'adhoc'));
|
|
52
|
+
const transcriptOf = (n) => (n.final && n.final.segments && n.final.segments.length ? n.final.segments.map((g) => ({ t: g.t, text: g.text, who: g.spk })) : (n.transcript || []).filter((x) => !x.dropped).map((x) => ({ t: x.t, text: x.text, who: x.source })));
|
|
53
|
+
|
|
54
|
+
/** The notes state on the calendar instance r. */
|
|
55
|
+
function stateOf(r) {
|
|
56
|
+
if (!r.notes) r.notes = { box: el('div', { class: 'cal3-notes', 'data-cal3': 'notes' }), rows: null, err: null, open: null, note: null, phase: null, q: '', seq: 0, live: null, unsub: null };
|
|
57
|
+
return r.notes;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The note the Calendar shows next: from the Record chip (phase During) or a meeting's detail (its default stage). */
|
|
61
|
+
export function openNote(r, id, phase) {
|
|
62
|
+
const s = stateOf(r);
|
|
63
|
+
s.open = id; s.note = null; s.phase = phase || null;
|
|
64
|
+
r.page = r.page || null;
|
|
65
|
+
if (r.page) r.page.mode = 'notes';
|
|
66
|
+
else r.pendingMode = 'notes';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The note linked to this calendar event (its id, its id without the day of a repeat, or its start and title), from the
|
|
70
|
+
* shared read. The calendar's events carry their start as `at`. Null when none is known. */
|
|
71
|
+
export function noteOfEvent(e) {
|
|
72
|
+
if (!e) return null;
|
|
73
|
+
const start = e.start || e.at; const base = String(e.id || '').split('@')[0];
|
|
74
|
+
for (const x of [{ id: e.id, start, title: e.title }, { id: base, start, title: e.title }]) { const f = notesFor(x); if (f.length) return f[0]; }
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The Notes view: the list and the note, in one box that keeps its identity across the page's redraws. */
|
|
79
|
+
export function notesView(ui) {
|
|
80
|
+
const s = stateOf(ui.r);
|
|
81
|
+
s.ui = ui;
|
|
82
|
+
if (!s.unsub) s.unsub = onNotes(() => { if (s.box.isConnected) read(s); });
|
|
83
|
+
if (!s.rows || (s.open && (!s.note || s.note.id !== s.open))) read(s);
|
|
84
|
+
draw(s);
|
|
85
|
+
return s.box;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function read(s) {
|
|
89
|
+
const seq = ++s.seq;
|
|
90
|
+
const [l, one] = await Promise.all([pmGet('/api/notes'), s.open ? pmGet('/api/notes/one?id=' + encodeURIComponent(s.open)) : null]);
|
|
91
|
+
if (seq !== s.seq) return;
|
|
92
|
+
if (l && l.ok) { s.rows = l.notes || []; s.err = null; } else { s.rows = s.rows || []; s.err = (l && l.error) || 'The notes could not be read.'; }
|
|
93
|
+
if (!s.open && s.rows.length) { s.open = s.rows[0].id; return read(s); }
|
|
94
|
+
if (one && one.ok && one.note && one.note.id === s.open) s.note = one.note;
|
|
95
|
+
draw(s);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function choose(s, id) {
|
|
99
|
+
if (s.open === id) return;
|
|
100
|
+
s.open = id; s.note = null; s.phase = null;
|
|
101
|
+
draw(s); read(s);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function draw(s) {
|
|
105
|
+
if (!s.box.isConnected && s.drawn) { s.drawn = false; }
|
|
106
|
+
// a field being typed in is not redrawn under the cursor; the live block keeps its own fields
|
|
107
|
+
const active = typeof document !== 'undefined' ? document.activeElement : null;
|
|
108
|
+
if (active && s.box.contains(active) && active.matches('textarea, [contenteditable="true"]') && !active.closest('[data-nt-part="live"]')) return;
|
|
109
|
+
const q = s.box.querySelector('[data-cal3-notes="search"]');
|
|
110
|
+
const typing = !!q && active === q; const caret = typing ? q.selectionStart : null;
|
|
111
|
+
s.box.replaceChildren(el('div', { class: 'cal3-nwrap' }, listCard(s), noteCol(s)));
|
|
112
|
+
s.drawn = true;
|
|
113
|
+
if (typing) { const nq = s.box.querySelector('[data-cal3-notes="search"]'); if (nq) { nq.focus(); nq.setSelectionRange(caret, caret); } }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ── the list ─────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
117
|
+
|
|
118
|
+
function listCard(s) {
|
|
119
|
+
const qtext = s.q.trim().toLowerCase();
|
|
120
|
+
const shown = (s.rows || []).filter((r) => !qtext || String(r.title || '').toLowerCase().includes(qtext) || String(iniName(r.initiative) || '').toLowerCase().includes(qtext)
|
|
121
|
+
|| (r.summary_lines || []).some((x) => String(x).toLowerCase().includes(qtext)));
|
|
122
|
+
const search = el('input', { type: 'text', class: 'cal3-nsearch', placeholder: 'Search notes', 'aria-label': 'Search notes', value: s.q, 'data-cal3-notes': 'search' });
|
|
123
|
+
search.addEventListener('input', () => { s.q = search.value; draw(s); });
|
|
124
|
+
let body;
|
|
125
|
+
if (!s.rows) body = fold('clock', 'Reading the notes', { role: 'status' });
|
|
126
|
+
else if (!s.rows.length) body = fold('mic', s.err || 'No notes yet. Press Record at the top right to start one.', { 'data-cal3-notes': 'none' });
|
|
127
|
+
else if (!shown.length) body = fold('search', 'No note matches “' + s.q.trim() + '”.', { 'data-cal3-notes': 'no-match' });
|
|
128
|
+
else body = groupNotes(shown).map((g) => el('div', { class: 'cal3-ctx', 'data-cal3-notes-group': g.label }, el('h4', null, g.label),
|
|
129
|
+
el('ul', { class: 'list', role: 'list' }, g.rows.map((r) => noteRow(s, r)))));
|
|
130
|
+
return el('section', { class: 'card sect', 'data-cal3-notes': 'list', 'aria-label': 'Notes' },
|
|
131
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('notes', 'sm'), 'By initiative ', el('span', { class: 'bdg g', 'data-cal3-notes': 'count' }, String(shown.length)))),
|
|
132
|
+
el('div', { class: 'cal3-nsearchw' }, search), body);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function noteRow(s, r) {
|
|
136
|
+
const mode = held(r.id) || (r.state === 'recording' || r.state === 'paused' ? r.state : null);
|
|
137
|
+
const on = s.open === r.id;
|
|
138
|
+
const len = mins(r.meeting_ms || 0);
|
|
139
|
+
return el('li', { class: 'nrow cal3-nrow' + (on ? ' on' : ''), 'data-cal3-note-row': r.id },
|
|
140
|
+
el('span', { class: 'ki' + (mode ? ' hot' : '') }, c3ic(mode ? 'mic' : 'doc')),
|
|
141
|
+
btn('cal3-nopen', { title: 'Open ' + (r.title || 'Untitled note'), 'aria-current': on ? 'true' : 'false' }, [
|
|
142
|
+
el('span', { class: 'tt' }, r.title || 'Untitled note'),
|
|
143
|
+
el('span', { class: 'chips' }, el('span', { class: 'dim mono cal3-xs' }, [mode ? 'now' : whenShort(startOf(r)), len].filter(Boolean).join(' · ')),
|
|
144
|
+
mode ? el('span', { class: 'tag', 'data-cal3-notes': 'live' }, mode === 'paused' ? 'paused' : 'recording') : null,
|
|
145
|
+
!linked(r) && !mode ? el('span', { class: 'tag', title: 'Not linked to a meeting', 'data-cal3-notes': 'no-meeting' }, 'no meeting') : null,
|
|
146
|
+
r.conflicts ? el('span', { class: 'st warn', title: r.conflicts + (r.conflicts === 1 ? ' statement differs' : ' statements differ') + ' from a file' }, c3ic('alert', 'sm'), String(r.conflicts)) : null)],
|
|
147
|
+
() => choose(s, r.id)),
|
|
148
|
+
el('span'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ── the note ─────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
152
|
+
|
|
153
|
+
function noteCol(s) {
|
|
154
|
+
const n = s.note;
|
|
155
|
+
if (!s.open) return el('div', { 'data-cal3-notes': 'note' }, el('section', { class: 'card' }, fold('doc', s.rows && s.rows.length ? 'Choose a note to read it.' : 'Notes you record show here.')));
|
|
156
|
+
if (!n) return el('div', { 'data-cal3-notes': 'note' }, el('section', { class: 'card' }, fold('clock', 'Reading the note', { role: 'status' })));
|
|
157
|
+
const mode = held(n.id);
|
|
158
|
+
const ph = s.phase || (mode ? 'during' : 'after');
|
|
159
|
+
const ev = n.meeting && n.meeting.kind === 'event' ? n.meeting : null;
|
|
160
|
+
const len = mins(lenOf(n));
|
|
161
|
+
const head = el('section', { class: 'card', 'data-cv': 'stage-head' },
|
|
162
|
+
el('div', { class: 'cal3-nhead' },
|
|
163
|
+
el('span', { class: 'chips2' }, el('b', { 'data-cal3-notes': 'title' }, n.title || 'Untitled note'), el('span', { class: 'mono dim cal3-sm' }, mode ? 'now' : hm(startOf(n)))),
|
|
164
|
+
el('div', { class: 'stg', role: 'group', 'aria-label': 'Note stage', 'data-cv': 'phase' },
|
|
165
|
+
PHASES.map(([k, ic, l]) => btn('sgb', { 'data-cal3-phase': k, 'aria-pressed': String(ph === k), title: l }, [c3ic(ic), el('span', null, l)], () => { s.phase = k; draw(s); })))),
|
|
166
|
+
el('div', { class: 'chips2 dim cal3-sm' },
|
|
167
|
+
[dayOf(startOf(n)), len, iniName(n.initiative) || 'No initiative'].filter(Boolean).join(' · '),
|
|
168
|
+
ev ? btn('chip2 lk', { 'data-cal3-notes': 'show-meeting', title: 'Show the meeting in the calendar' }, [c3ic('cal', 'sm'), 'Show in the calendar'], () => showMeeting(s, ev))
|
|
169
|
+
: linked(n) ? null : el('span', { class: 'tag' }, 'no meeting'),
|
|
170
|
+
btn('ib sm', { title: 'Copy as Markdown (with the meeting context)', 'aria-label': 'Copy as Markdown', 'data-cal3-notes': 'copy' }, c3ic('clip'), () => copy(n))));
|
|
171
|
+
const body = ph === 'before' ? before(s, n) : ph === 'during' ? during(s, n, mode) : after(s, n, mode);
|
|
172
|
+
return el('div', { class: 'cal3-ncol', 'data-cal3-notes': 'note', 'data-cal3-note': n.id, 'data-cal3-stage': ph }, head, ...[].concat(body));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const vctx = (s) => ({ state: {}, setState: () => { if (s.box.isConnected) { read(s); } } });
|
|
176
|
+
const linkButtons = (s, n) => el('div', { class: 'chips2' },
|
|
177
|
+
btn('chip2 lk', { 'data-cal3-notes': 'link-meeting' }, [c3ic('cal', 'sm'), 'Link to a meeting'], () => V.linkMeeting(vctx(s), n.id)),
|
|
178
|
+
btn('chip2 lk', { 'data-cal3-notes': 'link-initiative' }, [c3ic('init', 'sm'), 'Link to an initiative'], () => V.linkInitiative(vctx(s), n.id)));
|
|
179
|
+
|
|
180
|
+
/* a row of the stage's prepared list: the icon, the label, what it holds */
|
|
181
|
+
const prepRow = (icon, label, detail, done) => el('div', { class: 'chips2 cal3-prep', 'data-cal3-prep': label },
|
|
182
|
+
el('span', { class: 'ki' }, c3ic(icon)),
|
|
183
|
+
el('div', { class: 'cal3-prepb' }, el('b', null, label), el('div', { class: 'dim' }, detail)),
|
|
184
|
+
done ? el('span', { class: 'st ok', title: done }, c3ic('check', 'sm')) : null);
|
|
185
|
+
|
|
186
|
+
function before(s, n) {
|
|
187
|
+
const ev = n.meeting && n.meeting.kind === 'event' ? n.meeting : null;
|
|
188
|
+
const h3 = el('h3', null, c3ic('spark'), 'Before the meeting');
|
|
189
|
+
if (!ev) {
|
|
190
|
+
return el('section', { class: 'card', 'data-cv': 'stage-before' }, h3,
|
|
191
|
+
fold('info', n.meeting && n.meeting.kind === 'adhoc' ? 'A meeting that is not on the calendar, so nothing was prepared for it.' : 'Not linked to a meeting, so nothing was prepared. Link it to a meeting to bring in its people and agenda.', { 'data-cal3-notes': 'no-meeting-before' }),
|
|
192
|
+
linkButtons(s, n));
|
|
193
|
+
}
|
|
194
|
+
const c = ev.context || null;
|
|
195
|
+
const lines = transcriptOf(n).map((x) => ({ t: x.t, text: x.text }));
|
|
196
|
+
const heard = c ? agendaHeard(c, lines) : [];
|
|
197
|
+
const rows = [];
|
|
198
|
+
const isHeard = (i) => heard[i] !== null && heard[i] !== undefined;
|
|
199
|
+
if (c && c.agenda && c.agenda.length) rows.push(prepRow('spark', 'Agenda', el('span', null, c.agenda.map((a, i) => el('span', { class: 'cal3-agi', 'data-cal3-heard': isHeard(i) ? 'true' : 'false', title: isHeard(i) ? 'Heard in the transcript' : null }, (i ? ' · ' : '') + a, isHeard(i) ? c3ic('check', 'sm') : null))), heard.some((x) => x !== null) ? heard.filter((x) => x !== null).length + ' of ' + c.agenda.length + ' heard' : null));
|
|
200
|
+
if (c && c.purpose) rows.push(prepRow('target', 'What it is for', c.purpose));
|
|
201
|
+
if (c && c.prep) rows.push(prepRow('scale', 'To be ready', c.prep));
|
|
202
|
+
const files = [...((c && c.links) || []).map((l) => el('a', { class: 'chip2 lk', href: l.url, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), l.label)),
|
|
203
|
+
...((c && c.refs) || []).map((x) => (x.link ? el('a', { class: 'chip2 lk', href: x.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('doc', 'sm'), x.label) : el('span', { class: 'chip2' }, c3ic('doc', 'sm'), x.label)))];
|
|
204
|
+
if (files.length) rows.push(prepRow('doc', 'Read first', el('span', { class: 'chips2 cal3-wrapl' }, files)));
|
|
205
|
+
const names = peopleOf(c);
|
|
206
|
+
if (names.length) rows.push(prepRow('user', 'People', el('span', { class: 'chips2 cal3-wrapl' }, names.map(avatar))));
|
|
207
|
+
else if (c && c.attendees) rows.push(prepRow('user', 'People', c.attendees + ' attendees. Names are not kept while calendar names are off.'));
|
|
208
|
+
const when = (ev.start ? dayOf(ev.start) + ' ' + hm(ev.start) : '') + (ev.end ? ' to ' + hm(ev.end) : '');
|
|
209
|
+
rows.push(prepRow('cal', 'Meeting', [ev.title, when, c && c.series ? 'series: ' + c.series : null, c && c.role ? 'your role: ' + c.role : null].filter(Boolean).join(' · ')));
|
|
210
|
+
return el('section', { class: 'card', 'data-cv': 'stage-before' }, h3, ...rows,
|
|
211
|
+
c ? null : el('p', { class: 'dim cal3-sm' }, 'The calendar gave no more about this meeting.'));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function during(s, n, mode) {
|
|
215
|
+
const head = el('div', { class: 'c3cv-head' }, el('h3', null, c3ic('mic'), mode ? 'Live notes' : 'Transcript'), recordButton(s, n, mode));
|
|
216
|
+
if (mode) {
|
|
217
|
+
if (!s.live || s.live.id !== n.id) s.live = { id: n.id, host: el('div', { 'data-cal3-notes': 'live' }) };
|
|
218
|
+
s.live.host.replaceChildren();
|
|
219
|
+
const drawn = V.renderLive(s.live.host, vctx(s), n.id);
|
|
220
|
+
return el('section', { class: 'card', 'data-cv': 'stage-during' }, head,
|
|
221
|
+
drawn ? s.live.host : fold('mic', 'This note is being recorded in another window.'),
|
|
222
|
+
el('div', { class: 'chips2 dim mono cal3-sm' }, c3ic('lock', 'sm'), 'transcribed on this machine' + (mode === 'paused' ? ' · paused' : '')));
|
|
223
|
+
}
|
|
224
|
+
const tr = transcriptOf(n);
|
|
225
|
+
return el('section', { class: 'card', 'data-cv': 'stage-during' }, head,
|
|
226
|
+
tr.length ? el('div', { class: 'term cal3-term', 'data-cal3-notes': 'transcript' }, tr.map((x) => V.fmtTime(x.t || 0) + ' ' + (x.who ? V.speakerOf(n, x.who) + ': ' : '') + x.text).join('\n'))
|
|
227
|
+
: fold('mic', 'Nothing was transcribed for this note.', { 'data-cal3-notes': 'no-transcript' }),
|
|
228
|
+
el('div', { class: 'chips2 dim mono cal3-sm' }, c3ic('lock', 'sm'), 'transcribed on this machine' + (tr.length ? ' · ' + tr.length + (tr.length === 1 ? ' line' : ' lines') : '')));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* Record, Resume or Stop, as the note allows (dest-notes.js noteParts): a note being recorded stops; a paused one resumes;
|
|
232
|
+
* a note never recorded records; a stopped meeting has none. */
|
|
233
|
+
function recordButton(s, n, mode) {
|
|
234
|
+
if (n.private) return null;
|
|
235
|
+
const rb = (label, icon, title, on) => btn('ib pri cal3-recb', { 'data-cal3-notes': 'rec', title, 'aria-label': label }, [c3ic(icon), label], async () => { await on(); setTimeout(() => read(s), 400); });
|
|
236
|
+
if (mode === 'recording') return rb('Stop', 'stop', 'Stop and write the final notes', () => dock.stopNow());
|
|
237
|
+
if (mode === 'paused') return rb('Resume', 'play', 'Resume the paused meeting', () => dock.resumeNow());
|
|
238
|
+
if (n.state === 'idle' || (n.state === undefined && !(n.transcript || []).length)) {
|
|
239
|
+
const busy = dock.recordState().mode === 'recording';
|
|
240
|
+
const b = rb('Record', 'mic', busy ? 'Pause or stop the meeting being recorded first' : 'Record this note. Notes stay on this machine.', () => (busy ? null : dock.recordNote(n.id)));
|
|
241
|
+
if (busy) b.disabled = true;
|
|
242
|
+
return b;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function after(s, n, mode) {
|
|
248
|
+
const p = partsOf(n);
|
|
249
|
+
const names = n.meeting && n.meeting.context ? peopleOf(n.meeting.context) : [];
|
|
250
|
+
const cf = conflictsOf(n);
|
|
251
|
+
const td = (text) => el('td', null, text);
|
|
252
|
+
const kids = [
|
|
253
|
+
el('div', { class: 'c3cv-head' }, el('h3', null, c3ic('notes'), 'Notes'), recordButton(s, n, mode)),
|
|
254
|
+
el('div', { class: 'chips2' }, el('b', null, n.title || 'Untitled note'), el('span', { class: 'dim mono cal3-sm' }, [dayOf(startOf(n)), mins(lenOf(n))].filter(Boolean).join(' · ')), ...names.map(avatar)),
|
|
255
|
+
...cf.map((c) => el('div', { class: 'fold w', 'data-cal3-notes': 'conflict' }, c3ic('alert', 'sm'), '“' + c.text + '” differs from ', el('span', { class: 'mono' }, c.doc + (c.line ? ':' + c.line : '')))),
|
|
256
|
+
p.overview ? editable(p.from === 'final', 'p', p.overview, 'Overview', (t) => saveFinal(s, n, 'overview', 0, t), { class: 'cal3-ov', 'data-cal3-notes': 'overview' })
|
|
257
|
+
: el('p', { class: 'dim cal3-ov', 'data-cal3-notes': 'no-overview' }, mode ? 'The overview is written when you stop recording.' : 'No overview yet.'),
|
|
258
|
+
];
|
|
259
|
+
if (p.decisions.length) kids.push(el('table', { class: 'tbl', 'data-cv': 'decisions' }, el('thead', null, el('tr', null, el('th', null, 'Decision'), el('th', null, 'Why'))),
|
|
260
|
+
el('tbody', null, p.decisions.map((d) => el('tr', { 'data-cal3-notes': 'decision' }, el('td', null, editable(p.from === 'final', 'span', d.text, 'Decision', (t) => saveFinal(s, n, d.sec, d.i, t))), el('td', { class: 'dim' }, d.why || d.by || ''))))));
|
|
261
|
+
if (p.actions.length) kids.push(el('table', { class: 'tbl', 'data-cv': 'actions' }, el('thead', null, el('tr', null, el('th', null, 'Next step'), el('th', null, 'Owner'), el('th', null, 'Due'), el('th'))),
|
|
262
|
+
el('tbody', null, p.actions.map((a) => el('tr', { 'data-cal3-notes': 'action' }, el('td', null, editable(p.from === 'final', 'span', a.text, 'Next step', (t) => saveFinal(s, n, a.sec, a.i, t))),
|
|
263
|
+
td(a.owner || el('span', { class: 'dim' }, 'No owner')), td(a.due || el('span', { class: 'dim' }, '—')),
|
|
264
|
+
el('td', { class: 'cal3-tdr' }, btn('chip2 lk', { 'data-cal3-notes': 'make-task', title: n.initiative ? 'Add this step to ' + iniName(n.initiative) + ' as a plan item' : 'Link the note to an initiative first' },
|
|
265
|
+
[c3ic('plus', 'sm'), 'Make task'], (e) => makeTask(n, a, { btn: e.currentTarget }))))))));
|
|
266
|
+
if (p.questions.length) kids.push(el('div', { class: 'cal3-ctx', 'data-cal3-notes': 'questions' }, el('h4', null, 'Open questions'),
|
|
267
|
+
el('ul', { class: 'cal3-cl', role: 'list' }, p.questions.map((x) => el('li', { class: 'cal3-ci' }, el('span', { class: 'tag' }, 'Question'), el('span', { class: 'cal3-ct' }, x.text))))));
|
|
268
|
+
const hl = n.highlights || [];
|
|
269
|
+
if (hl.length) kids.push(el('div', { class: 'cal3-ctx', 'data-cal3-notes': 'highlights' }, el('h4', null, 'Highlights'),
|
|
270
|
+
el('ul', { class: 'cal3-cl', role: 'list' }, hl.map((x) => el('li', { class: 'cal3-ci' }, el('span', { class: 'dim mono cal3-at' }, typeof x.t === 'number' ? V.fmtTime(x.t) : ''), el('span', { class: 'tag' }, x.label || 'Highlight'), el('span', { class: 'cal3-ct' }, x.text))))));
|
|
271
|
+
if (!p.overview && !p.decisions.length && !p.actions.length && !p.questions.length && !hl.length && !cf.length) kids.push(fold('moon', mode ? 'Decisions and next steps show here as they are caught, and in full after you stop.' : 'No decisions, next steps or questions are held for this note.', { 'data-cal3-notes': 'nothing' }));
|
|
272
|
+
const tr = transcriptOf(n);
|
|
273
|
+
if (!mode && tr.length) kids.push(el('details', { 'data-cal3-notes': 'transcript-fold' }, el('summary', { class: 'dim cal3-sm' }, 'Transcript · ' + tr.length + (tr.length === 1 ? ' line' : ' lines')),
|
|
274
|
+
el('div', { class: 'term cal3-term' }, tr.map((x) => V.fmtTime(x.t || 0) + ' ' + x.text).join('\n'))));
|
|
275
|
+
return el('section', { class: 'card cal3-ncard', 'data-cv': 'notes' }, kids);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A text the PM corrects in place once the notes are final: a click puts the cursor in it, leaving it saves, Esc puts it back. */
|
|
279
|
+
function editable(on, tag, text, label, save, attrs) {
|
|
280
|
+
if (!on) return el(tag, attrs || null, text);
|
|
281
|
+
const node = el(tag, { ...(attrs || {}), contenteditable: 'true', spellcheck: 'true', role: 'textbox', 'aria-label': 'Edit ' + label, 'data-cal3-notes-edit': '' }, text);
|
|
282
|
+
let last = text;
|
|
283
|
+
node.addEventListener('keydown', (e) => {
|
|
284
|
+
if (e.key === 'Enter' && tag !== 'p') { e.preventDefault(); node.blur(); }
|
|
285
|
+
if (e.key === 'Escape') { e.preventDefault(); node.textContent = last; node.blur(); }
|
|
286
|
+
});
|
|
287
|
+
node.addEventListener('blur', async () => {
|
|
288
|
+
const v = node.textContent.replace(/\s+/g, ' ').trim();
|
|
289
|
+
if (!v || v === last) { node.textContent = last; return; }
|
|
290
|
+
if (await save(v)) last = v; else node.textContent = last;
|
|
291
|
+
});
|
|
292
|
+
return node;
|
|
293
|
+
}
|
|
294
|
+
async function saveFinal(s, n, sec, i, text) {
|
|
295
|
+
const r = await pmPost('/api/notes/final-edit', { id: n.id, sec, i, text });
|
|
296
|
+
if (!r || !r.ok) { toast('Not saved' + ((r && r.error) ? ': ' + r.error : '')); return false; }
|
|
297
|
+
n.final_notes = r.final_notes;
|
|
298
|
+
toast('Saved');
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
async function copy(n) {
|
|
302
|
+
try { await navigator.clipboard.writeText(V.noteMarkdown(n)); toast('The note is on the clipboard as Markdown, with its meeting context.'); }
|
|
303
|
+
catch { toast('The clipboard was not available.'); }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* "Show in the calendar": the Calendar switch back to the grid, on the meeting's day, with the meeting selected */
|
|
307
|
+
function showMeeting(s, ev) {
|
|
308
|
+
const ui = s.ui; if (!ui) return;
|
|
309
|
+
ui.st.mode = 'cal';
|
|
310
|
+
const date = String(ev.start || '').slice(0, 10);
|
|
311
|
+
ui.r.sel = ev.event_id || null;
|
|
312
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(date)) ui.h.go(ui.host, ui.r, { view: 'day', date, anim: 'zi' }); else ui.redraw();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Read the shared notes list now (a meeting's "Open the note" needs it on the first draw). */
|
|
316
|
+
export const primeNotes = () => refreshNotes();
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* TypeSafe key GET /api/typesafe (present or not; the key is never read)
|
|
16
16
|
* about you GET and POST /api/proactive/settings {name, about:{role, work, duties}} (the Welcome page writes the same)
|
|
17
17
|
* running sessions GET /api/sessions calendar import POST /api/sources/refresh {source:'calendar'}
|
|
18
|
-
* versions GET /api/kit/status · POST /api/kit/check, /api/kit/update
|
|
18
|
+
* versions GET /api/kit/status · POST /api/kit/check, /api/kit/update, /api/kit/upgrade-app (then GET /api/sessions until the new version)
|
|
19
19
|
* Every other setting is in the settings store: GET /api/settings and POST /api/settings (lib/settings-store.js).
|
|
20
20
|
* A board's own workflow is edited on the board; here are only the templates new boards start from.
|
|
21
21
|
* Nothing here asks for or shows a key or a password. */
|
|
@@ -161,7 +161,7 @@ export function mountSettings(host, ctx = {}) {
|
|
|
161
161
|
ensureCss();
|
|
162
162
|
const M = {
|
|
163
163
|
sec: SECTIONS.some((s) => s[0] === ctx.section) ? ctx.section : 'about', q: '', ws: ctx.ws || '', data: null, err: {},
|
|
164
|
-
engine: null, people: null, consent: null, online: null, profiles: null, typesafe: null, kits: {}, about: null, running: null,
|
|
164
|
+
engine: null, people: null, consent: null, online: null, profiles: null, typesafe: null, kits: {}, kitBusy: null, app: { phase: null, error: '' }, about: null, running: null,
|
|
165
165
|
tplEdit: null, wsEdit: null, wsDrop: null, addPath: '',
|
|
166
166
|
};
|
|
167
167
|
const root = el('div', { class: 'brief c3set', 'data-c3set': '' });
|
|
@@ -607,6 +607,28 @@ export function mountSettings(host, ctx = {}) {
|
|
|
607
607
|
lockedRow('look.icons', 'Card icons', 'Chosen from the built-in icon set by the card kind (D7).', 'Chosen by code rules', 'computer')], 'eye')];
|
|
608
608
|
}
|
|
609
609
|
|
|
610
|
+
/* ── the app update (workspace-kit-update): the server stops, reinstalls and starts again, so the page waits for it ── */
|
|
611
|
+
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
612
|
+
async function updateApp(ws, from, to) {
|
|
613
|
+
M.app = { phase: 'busy', error: '', to }; draw();
|
|
614
|
+
const r = await pmPost('/api/kit/upgrade-app', { ws, version: to });
|
|
615
|
+
if (!r || r.ok === false) { M.app = { phase: null, error: (r && r.error) || 'The update did not start.' }; draw(); return; }
|
|
616
|
+
await waitForRestart(r.from || from, r.log || '');
|
|
617
|
+
}
|
|
618
|
+
/** Reload when the server is back on a new version; otherwise say what happened and name the log. */
|
|
619
|
+
async function waitForRestart(from, log) {
|
|
620
|
+
let sawDown = false;
|
|
621
|
+
const until = Date.now() + 5 * 60000;
|
|
622
|
+
while (Date.now() < until) {
|
|
623
|
+
await pause(2000);
|
|
624
|
+
const j = await pmGet('/api/sessions');
|
|
625
|
+
if (!j || j.ok === false || !j.version) { sawDown = true; continue; }
|
|
626
|
+
if (j.version !== from) { location.reload(); return; }
|
|
627
|
+
if (sawDown) { M.app = { phase: null, error: 'PMOS started again on ' + j.version + ', so the update did not finish. The update log is ' + log + '.' }; draw(); return; }
|
|
628
|
+
}
|
|
629
|
+
M.app = { phase: null, error: 'PMOS did not come back on this address. Type pmos in a terminal to open it. The update log is ' + log + '.' }; draw();
|
|
630
|
+
}
|
|
631
|
+
|
|
610
632
|
function updatesSec() {
|
|
611
633
|
const rows = (ctx.workspaces || []).map((w) => {
|
|
612
634
|
const k = M.kits[w.id];
|
|
@@ -616,20 +638,31 @@ export function mountSettings(host, ctx = {}) {
|
|
|
616
638
|
el('div', { class: 'chips' }, chip(!k ? 'Reading' : k.error ? k.error : kit && kit.present ? 'kit ' + (kit.version || 'vendored by hand') : 'no kit in this workspace', 'mono'),
|
|
617
639
|
k && k.latest ? (behind ? tag(k.latest + ' available', 'warn') : tag('Up to date', 'ok')) : null),
|
|
618
640
|
M.err['kit:' + w.id] ? fold('alert', M.err['kit:' + w.id], 'w') : null),
|
|
619
|
-
el('div', { class: 'acts' }, behind ? btn('Update', { variant: 'primary', onclick: async () => { const r = await pmPost('/api/kit/update', { ws: w.id }); if (r && r.ok) { M.kits[w.id] = { ...M.kits[w.id], ...r, kitBehind: false }; toast('Updated the kit in ' + (w.label || w.name || w.id)); } else M.err['kit:' + w.id] = (r && r.error) || 'Not updated.'; draw(); } }) : null));
|
|
641
|
+
el('div', { class: 'acts' }, behind ? btn('Update', { variant: 'primary', disabled: !!M.kitBusy, attrs: { 'data-c3set-kit-update': w.id }, onclick: async () => { M.kitBusy = w.id; delete M.err['kit:' + w.id]; draw(); const r = await pmPost('/api/kit/update', { ws: w.id, version: k.latest }); M.kitBusy = null; if (r && r.ok) { M.kits[w.id] = { ...M.kits[w.id], ...r, kitBehind: false }; toast('Updated the kit in ' + (w.label || w.name || w.id)); } else M.err['kit:' + w.id] = (r && r.error) || 'Not updated.'; draw(); } }) : null));
|
|
620
642
|
});
|
|
621
643
|
const first = (ctx.workspaces || [])[0];
|
|
622
644
|
const st = first && M.kits[first.id];
|
|
623
645
|
const app = st && st.app; const appBehind = st && st.appBehind;
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
646
|
+
// A checkout updates with git, unless PMOS_UPDATE_SOURCE names a local package (server.js /api/kit/upgrade-app).
|
|
647
|
+
const gitOnly = !!(app && app.fromCheckout && !(st.source && st.source.kind === 'local'));
|
|
648
|
+
const U = M.app; const busy = U.phase === 'busy';
|
|
649
|
+
const appHelp = !appBehind ? 'Press Check now to ask the npm registry for the latest version.'
|
|
650
|
+
: gitOnly ? 'This workspace runs from a PMOS checkout. Update it with git pull.'
|
|
651
|
+
: 'The update stops PMOS, installs ' + st.latest + ' in place of ' + ((app && app.version) || '?') + ', and starts it again. Sessions keep running in tmux. This page reloads when the new version starts.';
|
|
652
|
+
const asking = appBehind && !gitOnly && U.phase === 'confirm';
|
|
653
|
+
const appRow = frame({ key: 'upd.app', label: 'PMOS app ' + ((app && app.version) || ''), help: appHelp, err: U.error || null,
|
|
654
|
+
note: asking ? el('span', { 'data-c3set-app-confirm': '' }, fold('alert', 'PMOS stops for about a minute while ' + st.latest + ' installs. Open terminals keep running.', 'w'))
|
|
655
|
+
: busy ? el('span', { 'data-c3set-app-busy': '', role: 'status', 'aria-live': 'polite' }, fold('cloud', 'Updating PMOS to ' + U.to + '. This page reloads when the new version starts.')) : null,
|
|
656
|
+
control: asking ? [btn('Cancel', { variant: 'ghost', attrs: { 'data-c3set-app-cancel': '' }, onclick: () => { M.app = { phase: null, error: '' }; draw(); } }),
|
|
657
|
+
btn('Restart and update', { variant: 'primary', attrs: { 'data-c3set-app-go': '' }, onclick: () => updateApp(first.id, (app && app.version) || '', st.latest) })]
|
|
658
|
+
: [st && st.latest ? tag(appBehind ? st.latest + ' available' : 'Up to date', appBehind ? 'acc' : 'ok') : null,
|
|
659
|
+
btn('Check now', { ic: 'undo', disabled: !first || busy, attrs: { 'data-c3set-check': '' }, onclick: async () => {
|
|
660
|
+
M.app = { phase: null, error: '' };
|
|
628
661
|
for (const w of ctx.workspaces || []) { const r = await pmPost('/api/kit/check', { ws: w.id }); M.kits[w.id] = r && r.ok ? r : { ...(M.kits[w.id] || {}), error: (r && r.error) || 'The check failed.' }; }
|
|
629
662
|
const s2 = first && M.kits[first.id]; toast(s2 && s2.appBehind ? 'PMOS ' + s2.latest + ' is available' : 'Checked. PMOS is up to date'); draw();
|
|
630
663
|
} }),
|
|
631
|
-
appBehind ? btn('
|
|
632
|
-
return [sect('PMOS app', [list([appRow])], { ic: 'cloud' }), sect('Kit in each workspace', [list(rows)], { ic: 'repo', count: rows.length }), groupOf('updates', 'When', 'cloud')];
|
|
664
|
+
appBehind && !gitOnly && !busy ? btn('Update to ' + st.latest, { variant: 'primary', attrs: { 'data-c3set-app-update': '' }, onclick: () => { M.app = { phase: 'confirm', error: '' }; draw(); } }) : null] });
|
|
665
|
+
return [sect('PMOS app', [list([appRow])], { ic: 'cloud', attrs: { 'data-c3set-app': '' } }), sect('Kit in each workspace', [list(rows)], { ic: 'repo', count: rows.length }), groupOf('updates', 'When', 'cloud')];
|
|
633
666
|
}
|
|
634
667
|
|
|
635
668
|
const BUILD = { about: aboutSec, workspaces: workspacesSec, boards: boardsSec, connectors: connectorsSec, recording: recordingSec, time: timeSec, sharing: sharingSec, access: accessSec, sessions: sessionsSec, privacy: privacySec, appearance: appearanceSec, updates: updatesSec };
|
|
@@ -20,6 +20,7 @@ import { mountQuarter, mountYear, dispose as disposeStrategy } from './c3-cal-st
|
|
|
20
20
|
import { simDock, simStart, simState, isDemo } from './shell-recording.js';
|
|
21
21
|
import * as dock from './note-dock.js';
|
|
22
22
|
import { drawCalendarPage } from './c3-calendar.js'; // the Calendar tab as a page about time (ctx.page)
|
|
23
|
+
import { openNote as showNote } from './c3-notes.js'; // the Record chip opens Calendar › Notes (c3-open-note)
|
|
23
24
|
|
|
24
25
|
/* ── shared state of the /c3.html page (console3 wave C3) ──────────────────────────────────────────────────────────
|
|
25
26
|
* `live` holds every mounted instance, so a press in the Briefing's ribbon can select a meeting on the Calendar tab's instance.
|
|
@@ -975,12 +976,13 @@ function wire(host, r) {
|
|
|
975
976
|
const up = (e) => {
|
|
976
977
|
delete pts[e.pointerId]; if (Object.keys(pts).length) return; pinch = null;
|
|
977
978
|
if (!s) return; const dx = e.clientX - s.x; const dy = e.clientY - s.y; s = null;
|
|
979
|
+
if (r.page && r.page.mode === 'notes') return;
|
|
978
980
|
if (Math.abs(dx) > 80 && Math.abs(dy) < 45) { hush(); go(host, r, { date: shift(r.view, r.date, dx < 0 ? 1 : -1), anim: dx < 0 ? 'sn' : 'sp' }); }
|
|
979
981
|
};
|
|
980
982
|
root.addEventListener('pointerup', up); root.addEventListener('pointercancel', (e) => { delete pts[e.pointerId]; s = null; pinch = null; });
|
|
981
983
|
root.addEventListener('click', (e) => { if (sup) { sup = false; e.stopPropagation(); e.preventDefault(); } }, true);
|
|
982
984
|
root.addEventListener('wheel', (e) => {
|
|
983
|
-
if (!(e.ctrlKey || e.metaKey)) return; e.preventDefault(); const n = Date.now(); if (n - wt < 350) return; wt = n; zoomBy(e.deltaY > 0 ? 1 : -1);
|
|
985
|
+
if (!(e.ctrlKey || e.metaKey) || (r.page && r.page.mode === 'notes')) return; e.preventDefault(); const n = Date.now(); if (n - wt < 350) return; wt = n; zoomBy(e.deltaY > 0 ? 1 : -1);
|
|
984
986
|
}, { passive: false });
|
|
985
987
|
const onKey = (e) => {
|
|
986
988
|
if (e.metaKey || e.ctrlKey || e.altKey || e.defaultPrevented) return;
|
|
@@ -988,6 +990,7 @@ function wire(host, r) {
|
|
|
988
990
|
const t = e.target; if (t && t.closest && t.closest('input,select,textarea,[contenteditable="true"]')) return;
|
|
989
991
|
if (document.querySelector('[data-shell="sheet"]')) return; // a sheet is open
|
|
990
992
|
if (!root.closest('[data-shell="calpop"]') && document.querySelector('[data-shell="calpop"]')) return; // the calendar popup is open: it takes the keys, not the tab behind it
|
|
993
|
+
if (r.page && r.page.mode === 'notes') return; // Calendar › Notes has no zoom or period to step
|
|
991
994
|
if ((e.key === '+' || e.key === '=') && zoomBy(-1)) e.preventDefault();
|
|
992
995
|
else if ((e.key === '-' || e.key === '_') && zoomBy(1)) e.preventDefault();
|
|
993
996
|
else if (rib) return;
|
|
@@ -998,6 +1001,16 @@ function wire(host, r) {
|
|
|
998
1001
|
r.off = () => { document.removeEventListener('keydown', onKey); clearTimeout(supT); };
|
|
999
1002
|
}
|
|
1000
1003
|
|
|
1004
|
+
/* the Record chip (shell-recording.js) asks for the note being recorded: Calendar › Notes on the During stage of the Calendar tab's
|
|
1005
|
+
* instance; if that tab has not mounted yet, its instance opens the note when it does */
|
|
1006
|
+
let PENDING_NOTE = null;
|
|
1007
|
+
if (typeof window !== 'undefined') window.addEventListener('c3-open-note', (ev) => {
|
|
1008
|
+
const id = (ev.detail && ev.detail.id) || null;
|
|
1009
|
+
const cal = [...live].find((x) => isCalTab(x) && x.ctx.page);
|
|
1010
|
+
if (cal) { showNote(cal, id, 'during'); draw(cal.host, cal); } else PENDING_NOTE = { id };
|
|
1011
|
+
gotoTab('cal');
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1001
1014
|
/* the simulated recorder (shell-recording.js, demo only): a start, a stop or a pause redraws the stage under the day's cards, and the group's title opens the During stage */
|
|
1002
1015
|
let simOnWin = false;
|
|
1003
1016
|
function installSim() {
|
|
@@ -1014,6 +1027,7 @@ export function mountCalendarViews(host, ctx) {
|
|
|
1014
1027
|
if (!r) {
|
|
1015
1028
|
r = { ctx: ctx || {}, root: el('section', { class: 'c3 calwrap', 'data-board': 'calendar-views', 'aria-label': 'Calendar' }), view: (ctx && ctx.view) || 'week', zoom: (ctx && ctx.view === 'day') ? 'today' : (ctx && ctx.view) || 'week', date: todayStr(), data: null, error: null, host, done: {}, ready: {}, sel: null, phase: null };
|
|
1016
1029
|
if (PENDING && ctx && ctx.plan) { r.sel = PENDING.sel; if (PENDING.date) r.date = PENDING.date; PENDING = null; }
|
|
1030
|
+
if (PENDING_NOTE && isCalTab(r)) { showNote(r, PENDING_NOTE.id, 'during'); PENDING_NOTE = null; }
|
|
1017
1031
|
live.add(r);
|
|
1018
1032
|
r.stopPhone = c3Phone(r.root);
|
|
1019
1033
|
roots.set(host, r);
|
|
@@ -507,6 +507,46 @@
|
|
|
507
507
|
.c3 .cal3-sw-mine{background:transparent;border:1px dashed var(--foreground)}
|
|
508
508
|
.c3 .cal3-wrap{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--sp-3);align-items:start}
|
|
509
509
|
@container (min-width:1100px){.c3 .cal3-wrap{grid-template-columns:minmax(0,1fr) 340px}}
|
|
510
|
+
/* Calendar › Notes (c3-notes.js): the list beside the open note; one column on a narrow page */
|
|
511
|
+
.c3 .cal3-nwrap{display:grid;grid-template-columns:minmax(260px,340px) minmax(0,1fr);gap:var(--sp-3);align-items:start}
|
|
512
|
+
.c3 .cal3-nwrap [data-cal3-notes="note"] .card{display:grid;gap:var(--sp-2)}
|
|
513
|
+
.c3 .cal3-nwrap details>summary{cursor:pointer;padding:var(--sp-1) 0}
|
|
514
|
+
.c3 .cal3-ncol{display:grid;gap:var(--sp-3);align-content:start;min-width:0}
|
|
515
|
+
.c3 .cal3-nhead{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}
|
|
516
|
+
.c3 .cal3-nsearchw{padding:0 0 var(--sp-2)}
|
|
517
|
+
.c3 .cal3-nsearch{width:100%}
|
|
518
|
+
.c3 .cal3-nrow{cursor:pointer}
|
|
519
|
+
.c3 .cal3-nrow.on{background:var(--muted)}
|
|
520
|
+
.c3 .cal3-sm{font-size:var(--fs-2)}
|
|
521
|
+
.c3 .cal3-xs{font-size:var(--fs-1)}
|
|
522
|
+
.c3 .cal3-prep{align-items:flex-start;padding:6px 0;border-bottom:1px solid var(--border)}
|
|
523
|
+
.c3 .cal3-prep .ki{flex:none}
|
|
524
|
+
.c3 .cal3-prepb{flex:1;min-width:0;font-size:var(--fs-3)}
|
|
525
|
+
.c3 .cal3-agi{display:inline-flex;align-items:center;gap:2px}
|
|
526
|
+
.c3 .cal3-agi[data-cal3-heard="true"] svg{color:var(--acc)}
|
|
527
|
+
.c3 .cal3-wrapl{display:inline-flex;flex-wrap:wrap}
|
|
528
|
+
.c3 .cal3-term{max-height:60vh;overflow:auto}
|
|
529
|
+
.c3 .cal3-ov{margin:0}
|
|
530
|
+
.c3 .cal3-tdr{text-align:right}
|
|
531
|
+
.c3 .cal3-ncard{display:grid;gap:var(--sp-2)}
|
|
532
|
+
/* a primitives.js button drawn as a console3 control: the primitive's own height, padding, nowrap and hover come off */
|
|
533
|
+
.c3 .cal3-b{height:auto;min-height:0;line-height:inherit;font-weight:inherit;justify-content:flex-start;text-align:left}
|
|
534
|
+
.c3 .cal3-b.chip2{padding:0;border:0;background:transparent;white-space:nowrap}
|
|
535
|
+
.c3 .cal3-b.sgb{height:auto;padding:4px 8px}
|
|
536
|
+
.c3 .cal3-b.ib{padding:0;justify-content:center}
|
|
537
|
+
.c3 .cal3-b.cal3-recb{width:auto;height:34px;padding:0 14px;gap:6px;display:inline-flex;align-items:center}
|
|
538
|
+
.c3 .cal3-b.cal3-nopen{display:grid;gap:2px;min-width:0;padding:0;border:0;background:transparent;white-space:normal;font:inherit;color:inherit}
|
|
539
|
+
.c3 .seg .cal3-b{height:auto;padding:4px 10px;border:0;white-space:nowrap}
|
|
540
|
+
@media (hover:hover){
|
|
541
|
+
.c3 .cal3-b.chip2:hover:not(:disabled),.c3 .cal3-b.cal3-nopen:hover:not(:disabled),.c3 .seg .cal3-b:hover:not(:disabled){background:transparent}
|
|
542
|
+
.c3 .cal3-b.chip2.lk:hover:not(:disabled){color:var(--acc)}
|
|
543
|
+
.c3 .cal3-b.sgb:hover:not(:disabled){background:var(--card)}
|
|
544
|
+
.c3 .cal3-b.sgb[aria-pressed="true"]:hover:not(:disabled){background:var(--accw)}
|
|
545
|
+
.c3 .cal3-b.ib:hover:not(:disabled){background:var(--background);color:var(--foreground)}
|
|
546
|
+
.c3 .cal3-b.ib.pri:hover:not(:disabled){background:var(--foreground);color:var(--background)}
|
|
547
|
+
.c3 .seg .cal3-b[aria-pressed="true"]:hover:not(:disabled){background:var(--background);color:var(--foreground)}
|
|
548
|
+
}
|
|
549
|
+
@media (max-width:760px){.c3 .cal3-nwrap{grid-template-columns:minmax(0,1fr)}}
|
|
510
550
|
.c3 .cal3-main,.c3 .cal3-side{display:grid;gap:var(--sp-3);min-width:0;align-content:start}
|
|
511
551
|
.c3 .cal3-scroll{overflow-x:auto;min-width:0}
|
|
512
552
|
/* a marker: glyph and short title; the left edge carries the tone */
|
|
@@ -143,8 +143,12 @@ const NS_SVG = 'http://www.w3.org/2000/svg';
|
|
|
143
143
|
const glyph = (d) => { const s = document.createElementNS(NS_SVG, 'svg'); s.setAttribute('class', 'ic sm'); s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('aria-hidden', 'true'); const p = document.createElementNS(NS_SVG, 'path'); p.setAttribute('d', d); s.appendChild(p); return s; };
|
|
144
144
|
const RGL = { highlighter: () => c3ic('flag', 'sm'), pause: () => glyph('M8 5v14M16 5v14'), play: () => c3ic('play', 'sm'), square: () => c3ic('stop', 'sm'), mic: () => c3ic('mic', 'sm'), down: () => glyph('M6 9l6 6 6-6') };
|
|
145
145
|
|
|
146
|
-
/** The chip opens the note
|
|
147
|
-
|
|
146
|
+
/** The chip opens the note: on console3, Calendar › Notes on the During stage (calendar-views.js listens for c3-open-note);
|
|
147
|
+
* elsewhere the main app, where the full note is. */
|
|
148
|
+
const openNote = (id) => {
|
|
149
|
+
if (document.querySelector('[data-c3-tab="cal"]')) { window.dispatchEvent(new CustomEvent('c3-open-note', { detail: { id: id || dock.activeId() || null } })); return; }
|
|
150
|
+
try { window.open('/', '_blank', 'noopener'); } catch { /* a blocked pop-up */ }
|
|
151
|
+
};
|
|
148
152
|
|
|
149
153
|
/* The engine and language the live transcript uses, on the chip (notes-recording-reliability): a meeting transcribed in the wrong
|
|
150
154
|
* language because of a setting is visible while it records. Read once per recording from GET /api/notes/engine. */
|
|
@@ -206,7 +210,7 @@ function mountReal(host) {
|
|
|
206
210
|
const v = recView(st, reducedMotion());
|
|
207
211
|
rec.clock = el('span', { class: 'recclk', 'data-fr-rec-clock': '' }, chipText(st));
|
|
208
212
|
const chip = button({ variant: 'ghost', onClick: () => openNote(st.id),
|
|
209
|
-
attrs: { class: 'c3rec__chip', title: (recording ? 'Recording: ' : 'Paused: ') + v.title + '. Open the note
|
|
213
|
+
attrs: { class: 'c3rec__chip', title: (recording ? 'Recording: ' : 'Paused: ') + v.title + '. Open the note', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + v.title + '. Open the note.', 'data-fr-rec': st.mode, 'data-shell-rec-open': '' },
|
|
210
214
|
label: [el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title),
|
|
211
215
|
...(eng.text ? [el('span', { class: 'c3rec__eng', 'data-fr-rec-engine': '', title: 'The live transcript: engine and language (Notes settings)' }, eng.text)] : [])] });
|
|
212
216
|
const group = [chip];
|
|
@@ -133,7 +133,8 @@
|
|
|
133
133
|
"pm-card-layout-test.js",
|
|
134
134
|
"pm-portfolio-inline-test.js",
|
|
135
135
|
"pm-brief-page-test.js",
|
|
136
|
-
"c3-settings-test.js"
|
|
136
|
+
"c3-settings-test.js",
|
|
137
|
+
"c3-updates-test.js"
|
|
137
138
|
],
|
|
138
139
|
"ui": [
|
|
139
140
|
"ui-test.js"
|
|
@@ -263,7 +264,8 @@
|
|
|
263
264
|
"pm-card-layout-test.js": 20,
|
|
264
265
|
"pm-portfolio-inline-test.js": 15,
|
|
265
266
|
"settings-store.test.js": 1,
|
|
266
|
-
"c3-settings-test.js": 25
|
|
267
|
+
"c3-settings-test.js": 25,
|
|
268
|
+
"c3-updates-test.js": 20
|
|
267
269
|
},
|
|
268
270
|
"_comment_seconds": "Measured wall time of each suite run alone (4-CPU sandbox, 2026-10-05). Only the ORDER uses it: the slowest suites start first, and --shard deals suites to the least-loaded shard so the shards finish together. A wrong or missing number never skips a suite; a missing one counts as 10."
|
|
269
271
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Initiative c3-notes-place: Calendar › Notes on the console3 Calendar tab (the notes list, the note's Before, During and After, the Record chip and a meeting's detail opening the note).",
|
|
3
|
+
"panels": ["pm-c3-notes-test.js"],
|
|
4
|
+
"seconds": { "pm-c3-notes-test.js": 45 }
|
|
5
|
+
}
|