create-pmos 0.72.1 → 0.73.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/guide/notes.md +1 -1
- package/workspace/guide/today.md +1 -1
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +2 -0
- package/workspace/public/pm/components.css +65 -8
- package/workspace/public/pm/note-dock.js +137 -0
- package/workspace/public/pm/notes-links.js +16 -37
- package/workspace/public/pm/view-notes.js +39 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.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-07T05:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T05:41:12Z"}
|
|
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-07T05:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T05:41:12Z"}
|
|
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-07T05:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T05:41:12Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/guide/notes.md
CHANGED
|
@@ -95,7 +95,7 @@ Under the title, a note shows two lines: **Meeting** and **Initiative**.
|
|
|
95
95
|
|
|
96
96
|
When a note has no initiative, the summary suggests one: the open initiative that shares 3 or more words with what was said, when no other initiative shares as many. A suggestion never replaces an initiative you chose.
|
|
97
97
|
|
|
98
|
-
To take notes for a calendar event, open the event's detail on the Agenda and press **Take notes**. The note is linked to the event, and to the event's initiative when it has one. The note
|
|
98
|
+
To take notes for a calendar event, open the event's detail on the Agenda and press **Take notes**. PMOS starts to listen at once, to your microphone and to the meeting when local transcription is set up. Pick the meeting's tab or window when the browser asks. Let people know that you are taking notes. The note is linked to the event, and to the event's initiative when it has one. The note stays small at the bottom right of the workspace, with a red dot, its title and its time, on every page. Press it to open the note as a pop-up: type your notes, read the transcript, and press **Stop** when the meeting ends. Press **Minimize** or Esc to make it small again, and **Open in Notes** to see it on the full screen. One note is taken at a time. If you press **Take notes** on another event while a note listens, PMOS asks first: **Keep it**, or **Stop it and start this one**. The note stays at the bottom right after a reload. When it no longer listens, press the x beside it to put it away. An event that already has a note shows its summary and **Open notes**, which opens the note in the pop-up. See [Today](today.md) and [the initiative sheet](initiatives-screen.md).
|
|
99
99
|
|
|
100
100
|
## Dictate into your notes
|
|
101
101
|
|
package/workspace/guide/today.md
CHANGED
|
@@ -50,7 +50,7 @@ Each day of the month answers three questions:
|
|
|
50
50
|
|
|
51
51
|
The switch above the month shows one answer at a time: **Overview** (all three), **Full** (the time in meetings), **Key** (the meeting that matters), **Prep** (how ready you are) or **Free** (the free blocks of 90 minutes or more).
|
|
52
52
|
|
|
53
|
-
A meeting's detail ends with **Meeting notes**. Press **Take notes** to start a note linked to the meeting, and to its initiative when the brief names one.
|
|
53
|
+
A meeting's detail ends with **Meeting notes**. Press **Take notes** to start a note linked to the meeting, and to its initiative when the brief names one. PMOS starts to listen at once and keeps the note small at the bottom right. Press it to open the note as a pop-up. When the meeting has a note, the detail shows its summary and **Open notes**, which opens it in the pop-up, and the meeting's row shows **notes**. See [Notes](notes.md).
|
|
54
54
|
|
|
55
55
|
The memory of a meeting says what kind of meeting it is (one-to-one, onboarding or group), how many times it was held, how many are to come, and why it matters.
|
|
56
56
|
|
package/workspace/package.json
CHANGED
|
@@ -776,6 +776,8 @@ async function boot() {
|
|
|
776
776
|
if (FRAME) { reg.setFrameHome((id, p) => go(id, p)); fr.reg = reg; } // the frame: pmApp.open(id) is the place on screen
|
|
777
777
|
}
|
|
778
778
|
catch (e) { console.error('[pm] the registry failed to load:', e); }
|
|
779
|
+
// meeting-notes Run 6: the note being taken stays at the bottom right on every destination
|
|
780
|
+
import('./note-dock.js').then((m) => m.mountDock()).catch((e) => console.error('[pm] the note dock failed to load:', e));
|
|
779
781
|
render();
|
|
780
782
|
}
|
|
781
783
|
}
|
|
@@ -6212,13 +6212,7 @@
|
|
|
6212
6212
|
color: var(--muted-foreground);
|
|
6213
6213
|
}
|
|
6214
6214
|
|
|
6215
|
-
/* meeting-notes
|
|
6216
|
-
.nt-inhost__head {
|
|
6217
|
-
display: flex;
|
|
6218
|
-
align-items: center;
|
|
6219
|
-
justify-content: space-between;
|
|
6220
|
-
gap: 8px;
|
|
6221
|
-
}
|
|
6215
|
+
/* meeting-notes Runs 5 and 6: the note in one column, as the pop-up draws it */
|
|
6222
6216
|
.nt-wrap--inline .nt-body2 {
|
|
6223
6217
|
grid-template-columns: 1fr;
|
|
6224
6218
|
}
|
|
@@ -6232,7 +6226,70 @@
|
|
|
6232
6226
|
border-top: 1px solid var(--border);
|
|
6233
6227
|
}
|
|
6234
6228
|
.nt-wrap--inline .nt-lines {
|
|
6235
|
-
max-height:
|
|
6229
|
+
max-height: 240px;
|
|
6230
|
+
}
|
|
6231
|
+
|
|
6232
|
+
/* meeting-notes Run 6: the note being taken, minimized at the bottom right, and its pop-up */
|
|
6233
|
+
.nt-dock {
|
|
6234
|
+
position: fixed;
|
|
6235
|
+
right: 16px;
|
|
6236
|
+
bottom: 40px;
|
|
6237
|
+
z-index: 1;
|
|
6238
|
+
display: flex;
|
|
6239
|
+
flex-direction: column;
|
|
6240
|
+
align-items: flex-end;
|
|
6241
|
+
}
|
|
6242
|
+
.nt-dock:empty {
|
|
6243
|
+
display: none;
|
|
6244
|
+
}
|
|
6245
|
+
.nt-pillrow {
|
|
6246
|
+
display: flex;
|
|
6247
|
+
align-items: center;
|
|
6248
|
+
gap: 4px;
|
|
6249
|
+
}
|
|
6250
|
+
.nt-pill {
|
|
6251
|
+
max-width: 320px;
|
|
6252
|
+
background: var(--card);
|
|
6253
|
+
box-shadow: 0 4px 16px color-mix(in oklab, var(--term-bg) 18%, transparent);
|
|
6254
|
+
}
|
|
6255
|
+
.nt-pill__txt {
|
|
6256
|
+
display: flex;
|
|
6257
|
+
align-items: center;
|
|
6258
|
+
gap: 8px;
|
|
6259
|
+
min-width: 0;
|
|
6260
|
+
}
|
|
6261
|
+
.nt-pill__title {
|
|
6262
|
+
min-width: 0;
|
|
6263
|
+
overflow: hidden;
|
|
6264
|
+
text-overflow: ellipsis;
|
|
6265
|
+
white-space: nowrap;
|
|
6266
|
+
}
|
|
6267
|
+
.nt-pill__clock {
|
|
6268
|
+
font-size: 12px;
|
|
6269
|
+
font-variant-numeric: tabular-nums;
|
|
6270
|
+
color: var(--muted-foreground);
|
|
6271
|
+
}
|
|
6272
|
+
.nt-pop {
|
|
6273
|
+
width: min(440px, calc(100vw - 32px));
|
|
6274
|
+
max-height: calc(100vh - 120px);
|
|
6275
|
+
display: flex;
|
|
6276
|
+
flex-direction: column;
|
|
6277
|
+
border: 1px solid var(--border);
|
|
6278
|
+
background: var(--card);
|
|
6279
|
+
box-shadow: 0 12px 40px color-mix(in oklab, var(--term-bg) 24%, transparent);
|
|
6280
|
+
}
|
|
6281
|
+
.nt-pop__head {
|
|
6282
|
+
display: flex;
|
|
6283
|
+
align-items: center;
|
|
6284
|
+
justify-content: space-between;
|
|
6285
|
+
gap: 8px;
|
|
6286
|
+
padding: 8px 12px;
|
|
6287
|
+
border-bottom: 1px solid var(--border);
|
|
6288
|
+
}
|
|
6289
|
+
.nt-pop__body {
|
|
6290
|
+
min-height: 0;
|
|
6291
|
+
overflow-y: auto;
|
|
6292
|
+
padding: 12px;
|
|
6236
6293
|
}
|
|
6237
6294
|
|
|
6238
6295
|
/* -- layouts · .lg- the legacy and fixture shells (Run 3, slice 3D) -- */
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/* pm/note-dock.js — the meeting note being taken, kept small at the bottom right of the workspace (meeting-notes Run 6).
|
|
2
|
+
*
|
|
3
|
+
* Take notes on a calendar event makes the note and starts listening at once. The note stays minimized as a pill at the
|
|
4
|
+
* bottom right: a red dot while it listens, its title and its time. A press on the pill opens the note as a pop-up: the same
|
|
5
|
+
* note as the Notes tab, in one column (pm/view-notes.js renderInline). Minimize or Esc puts it back; Open in Notes shows it
|
|
6
|
+
* on the full screen. The pill stays on every destination.
|
|
7
|
+
*
|
|
8
|
+
* One note at a time. The note is kept in this browser (KEY), so it is still there after a reload; a note that no longer
|
|
9
|
+
* listens can be put away with the x. Starting another note while one is listening asks first.
|
|
10
|
+
*
|
|
11
|
+
* mountDock() → draws the dock on every render (pm/app.js boot)
|
|
12
|
+
* takeNotes(e, opts, btn) → makes a note linked to the event, starts listening, and keeps it minimized
|
|
13
|
+
* openDock(id) → shows a note in the pop-up
|
|
14
|
+
* minimize() · activeId()
|
|
15
|
+
*/
|
|
16
|
+
import { el } from './dom.js';
|
|
17
|
+
import { button, iconButton, layer, toast, ensureComponentsCss } from './primitives.js';
|
|
18
|
+
import { askConfirm } from './dialogs.js';
|
|
19
|
+
import { pmPost } from './http.js';
|
|
20
|
+
import { state, setState, subscribe, FIXTURE } from './state.js';
|
|
21
|
+
import { meetingOf, refreshNotes, noteById } from './notes-links.js';
|
|
22
|
+
|
|
23
|
+
const KEY = 'pmos.workspace.pm.note';
|
|
24
|
+
let active = read(); // { id, title }: the one note in the dock
|
|
25
|
+
let open = false; // the pop-up is open
|
|
26
|
+
let host = null;
|
|
27
|
+
let V = null; // pm/view-notes.js, read the first time the dock draws a note
|
|
28
|
+
let tick = null;
|
|
29
|
+
const viewCtx = { state, setState, FIXTURE };
|
|
30
|
+
|
|
31
|
+
function read() {
|
|
32
|
+
try { const j = JSON.parse(localStorage.getItem(KEY) || 'null'); return j && typeof j.id === 'string' ? { id: j.id, title: String(j.title || '') } : null; } catch { return null; }
|
|
33
|
+
}
|
|
34
|
+
function write() {
|
|
35
|
+
try { if (active) localStorage.setItem(KEY, JSON.stringify(active)); else localStorage.removeItem(KEY); } catch { /* private mode: the note stays for this page */ }
|
|
36
|
+
}
|
|
37
|
+
function view() {
|
|
38
|
+
if (!V) import('./view-notes.js').then((m) => { V = m; setState({}); });
|
|
39
|
+
return V;
|
|
40
|
+
}
|
|
41
|
+
const liveId = () => (V ? V.listeningId() : null);
|
|
42
|
+
const titleOf = () => { if (!active) return ''; const r = noteById(active.id); return (V && V.titleOf(active.id)) || (r && r.title) || active.title || 'Untitled note'; };
|
|
43
|
+
|
|
44
|
+
export const activeId = () => (active ? active.id : null);
|
|
45
|
+
|
|
46
|
+
export function mountDock() {
|
|
47
|
+
if (host || FIXTURE || typeof document === 'undefined') return;
|
|
48
|
+
ensureComponentsCss();
|
|
49
|
+
host = el('div', { class: 'nt-dock', 'data-nt-dock': '' });
|
|
50
|
+
// Esc in the pop-up minimizes it. It is read first (capture), so a field does not take it to undo the typing, and the shell
|
|
51
|
+
// does not also read it.
|
|
52
|
+
host.addEventListener('keydown', (e) => { if (e.key === 'Escape' && open) { e.preventDefault(); e.stopPropagation(); minimize(); } }, true);
|
|
53
|
+
const l = layer();
|
|
54
|
+
if (l) l.appendChild(host);
|
|
55
|
+
subscribe(draw);
|
|
56
|
+
if (active) view();
|
|
57
|
+
draw();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function minimize() {
|
|
61
|
+
if (!open) return;
|
|
62
|
+
open = false;
|
|
63
|
+
if (V) V.setPopped(null);
|
|
64
|
+
setState({});
|
|
65
|
+
}
|
|
66
|
+
/** Show this note in the pop-up. Another note that is listening is asked about first. */
|
|
67
|
+
export async function openDock(id) {
|
|
68
|
+
if (active && active.id !== id && liveId() === active.id && !await switchFrom()) { openDock(active.id); return; }
|
|
69
|
+
const r = noteById(id);
|
|
70
|
+
active = { id, title: (r && r.title) || '' }; write();
|
|
71
|
+
open = true;
|
|
72
|
+
const v = V || await import('./view-notes.js');
|
|
73
|
+
V = v; v.setPopped(id);
|
|
74
|
+
setState({});
|
|
75
|
+
requestAnimationFrame(() => { const t = host && host.querySelector('[data-nt-body]'); if (t) t.focus(); });
|
|
76
|
+
}
|
|
77
|
+
function close() {
|
|
78
|
+
if (liveId() === (active && active.id)) return;
|
|
79
|
+
active = null; open = false; write();
|
|
80
|
+
if (V) V.setPopped(null);
|
|
81
|
+
setState({});
|
|
82
|
+
}
|
|
83
|
+
/** Ask before a new note replaces the one that is listening. True when the PM stopped it. */
|
|
84
|
+
async function switchFrom() {
|
|
85
|
+
const sure = await askConfirm({ title: '"' + titleOf() + '" is still being taken.', body: 'Only one note is taken at a time.', confirmLabel: 'Stop it and start this one', cancelLabel: 'Keep it' });
|
|
86
|
+
if (!sure) return false;
|
|
87
|
+
if (V) await V.stopListening(viewCtx);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Take notes on a calendar event: make the note, start listening at once, and keep it minimized. */
|
|
92
|
+
export async function takeNotes(e, { ws, repo, initiative } = {}, btn) {
|
|
93
|
+
if (active && liveId() === active.id && !await switchFrom()) { openDock(active.id); return; }
|
|
94
|
+
if (btn) btn.disabled = true;
|
|
95
|
+
const ini = initiative && initiative.id ? { id: initiative.id, ws: ws || null, repo: repo || null, title: initiative.title || null } : undefined;
|
|
96
|
+
const r = await pmPost('/api/notes/new', { meeting: meetingOf(e), ...(ini ? { initiative: ini } : {}) });
|
|
97
|
+
if (btn) btn.disabled = false;
|
|
98
|
+
if (!r || !r.ok) { toast({ text: (r && r.error) || 'The note was not made.', tone: 'destructive' }); return; }
|
|
99
|
+
active = { id: r.note.id, title: r.note.title || '' }; write();
|
|
100
|
+
open = false;
|
|
101
|
+
const v = V || await import('./view-notes.js');
|
|
102
|
+
V = v; v.setPopped(null);
|
|
103
|
+
refreshNotes();
|
|
104
|
+
setState({});
|
|
105
|
+
if (await v.listenNow(viewCtx, r.note.id)) toast({ text: 'Notes are being taken. Let people know.' });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function clockText() {
|
|
109
|
+
const s = V && V.liveStarted && V.liveStarted();
|
|
110
|
+
return s ? V.fmtTime(Date.now() - s) : '';
|
|
111
|
+
}
|
|
112
|
+
function draw() {
|
|
113
|
+
if (!host) return;
|
|
114
|
+
const v = view();
|
|
115
|
+
const live = !!active && !!v && liveId() === active.id;
|
|
116
|
+
clearInterval(tick); tick = null;
|
|
117
|
+
if (!active || !v) { host.replaceChildren(); return; }
|
|
118
|
+
if (live) tick = setInterval(() => { const c = host.querySelector('[data-nt-dock-clock]'); if (c) c.textContent = clockText(); }, 1000);
|
|
119
|
+
if (!open) {
|
|
120
|
+
const pill = button({ label: el('span', { class: 'nt-pill__txt' },
|
|
121
|
+
live ? el('span', { class: 'nt-live-dot', 'aria-hidden': 'true' }) : null,
|
|
122
|
+
el('span', { class: 'nt-pill__title' }, titleOf()),
|
|
123
|
+
live ? el('span', { class: 'nt-pill__clock', 'data-nt-dock-clock': '' }, clockText()) : null),
|
|
124
|
+
variant: 'outline', attrs: { class: 'nt-pill', 'data-nt-pill': live ? 'live' : 'idle', 'aria-label': (live ? 'Notes are being taken: ' : 'Meeting note: ') + titleOf() + '. Open the note.' },
|
|
125
|
+
onClick: () => openDock(active.id) });
|
|
126
|
+
host.replaceChildren(el('div', { class: 'nt-pillrow' }, pill,
|
|
127
|
+
live ? null : iconButton({ icon: 'x', ariaLabel: 'Put the note away', onClick: close, attrs: { 'data-nt-dock-close': '' } })));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const body = el('div', { class: 'nt-pop__body' });
|
|
131
|
+
const pop = el('section', { class: 'nt-pop', role: 'dialog', 'aria-label': 'Meeting note', 'data-nt-pop': active.id },
|
|
132
|
+
el('div', { class: 'nt-pop__head' }, el('span', { class: 'nt-sec__h' }, live ? 'Taking notes' : 'Meeting note'),
|
|
133
|
+
button({ label: 'Minimize', variant: 'ghost', size: 'sm', onClick: minimize, attrs: { 'data-nt-minimize': '' } })),
|
|
134
|
+
body);
|
|
135
|
+
host.replaceChildren(pop);
|
|
136
|
+
if (!v.renderInline(body, viewCtx, active.id)) body.appendChild(el('p', { class: 'nt-hint' }, 'Another note is being taken.'));
|
|
137
|
+
}
|
|
@@ -2,27 +2,26 @@
|
|
|
2
2
|
*
|
|
3
3
|
* notesFor(e) → the note rows linked to a calendar event (same id, or same start minute and title)
|
|
4
4
|
* notesOfInitiative(id) → the note rows linked to an initiative, newest first
|
|
5
|
+
* noteById(id) → the note's row, or null
|
|
5
6
|
* notesBlock(e, { ws, repo, initiative }) → the "Meeting notes" block of an event's detail. Take notes makes a note linked
|
|
6
|
-
* to the event and
|
|
7
|
-
* an event that has a note shows its summary and Open notes,
|
|
7
|
+
* to the event, starts listening and keeps the note minimized at the bottom right
|
|
8
|
+
* (pm/note-dock.js, Run 6); an event that has a note shows its summary and Open notes,
|
|
9
|
+
* which opens it as a pop-up
|
|
8
10
|
* noteRows(rows, { onOpen }) → the rows of an initiative's Notes tab; onOpen runs before the note opens
|
|
9
11
|
* openNote(id) → opens the Notes tab on that note
|
|
10
12
|
* onNotes(fn) · refreshNotes() → redraw when the list is read
|
|
11
13
|
*
|
|
12
14
|
* One read feeds it: GET /api/notes (a row carries the meeting, the initiative and up to three summary lines, never the body;
|
|
13
|
-
* a private note carries its title only). Take notes writes POST /api/notes/new { meeting, initiative }.
|
|
15
|
+
* a private note carries its title only). Take notes (pm/note-dock.js) writes POST /api/notes/new { meeting, initiative }.
|
|
14
16
|
*/
|
|
15
17
|
import { el } from './dom.js';
|
|
16
18
|
import { button } from './primitives.js';
|
|
17
|
-
import { pmGet
|
|
18
|
-
import {
|
|
19
|
+
import { pmGet } from './http.js';
|
|
20
|
+
import { takeNotes, openDock, activeId } from './note-dock.js';
|
|
19
21
|
|
|
20
22
|
const READ_MS = 15000; // the list is read again at most this often while a calendar or an initiative is drawn
|
|
21
23
|
let rows = null; let readAt = 0; let reading = null; let failed = false;
|
|
22
24
|
const subs = new Set();
|
|
23
|
-
let shown = null; // the note open in an event's detail (Take notes or Open notes); the listening note is shown too
|
|
24
|
-
let V = null; // pm/view-notes.js, read the first time a note is shown in a detail
|
|
25
|
-
const viewCtx = { state, setState, FIXTURE };
|
|
26
25
|
|
|
27
26
|
export function onNotes(fn) { subs.add(fn); return () => subs.delete(fn); }
|
|
28
27
|
export function refreshNotes() {
|
|
@@ -51,6 +50,7 @@ export function sameEvent(meeting, e) {
|
|
|
51
50
|
export const notesFor = (e) => list().filter((n) => sameEvent(n.meeting, e));
|
|
52
51
|
export const notesOfInitiative = (id) => list().filter((n) => n.initiative && n.initiative.id === id);
|
|
53
52
|
export const notesKnown = () => rows !== null || failed;
|
|
53
|
+
export const noteById = (id) => (rows || []).find((n) => n.id === id) || null;
|
|
54
54
|
|
|
55
55
|
/** The meeting link a calendar event gives a note. */
|
|
56
56
|
export function meetingOf(e) {
|
|
@@ -63,22 +63,6 @@ export async function openNote(id) {
|
|
|
63
63
|
v.openNote(id);
|
|
64
64
|
c.openView('notes');
|
|
65
65
|
}
|
|
66
|
-
async function takeNotes(e, { ws, repo, initiative } = {}, btn) {
|
|
67
|
-
if (btn) btn.disabled = true;
|
|
68
|
-
const ini = initiative && initiative.id ? { id: initiative.id, ws: ws || null, repo: repo || null, title: initiative.title || null } : undefined;
|
|
69
|
-
const r = await pmPost('/api/notes/new', { meeting: meetingOf(e), ...(ini ? { initiative: ini } : {}) });
|
|
70
|
-
if (btn) btn.disabled = false;
|
|
71
|
-
if (!r || !r.ok) { const { toast } = await import('./primitives.js'); toast({ text: (r && r.error) || 'The note was not made.', tone: 'destructive' }); return; }
|
|
72
|
-
shown = r.note.id;
|
|
73
|
-
await refreshNotes();
|
|
74
|
-
setState({});
|
|
75
|
-
}
|
|
76
|
-
function show(id) { shown = id; setState({}); }
|
|
77
|
-
function view() {
|
|
78
|
-
if (!V) import('./view-notes.js').then((m) => { V = m; setState({}); });
|
|
79
|
-
return V;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
66
|
const dayOf = (iso) => { try { return new Date(iso).toLocaleDateString([], { weekday: 'short', day: 'numeric', month: 'short' }); } catch { return ''; } };
|
|
83
67
|
const clock = (iso) => { try { return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch { return ''; } };
|
|
84
68
|
|
|
@@ -88,30 +72,25 @@ export function notesBlock(e, opts = {}) {
|
|
|
88
72
|
const ended = !!e.ended;
|
|
89
73
|
if (mine.length) {
|
|
90
74
|
const n = mine[0];
|
|
91
|
-
const v = view();
|
|
92
|
-
const live = v && v.listeningId() === n.id;
|
|
93
|
-
if (v && (shown === n.id || live)) {
|
|
94
|
-
const host = el('div', { class: 'td-day__notes nt-inhost', 'data-notes-block': 'inline', 'data-notes-id': n.id });
|
|
95
|
-
const head = el('div', { class: 'nt-inhost__head' }, el('h4', null, 'Meeting notes'),
|
|
96
|
-
live ? null : button({ label: 'Close', variant: 'ghost', size: 'sm', attrs: { 'data-notes-close': n.id }, onClick: () => show(null) }));
|
|
97
|
-
host.appendChild(head);
|
|
98
|
-
if (v.renderInline(host, viewCtx, n.id)) return host;
|
|
99
|
-
}
|
|
100
75
|
const lines = n.summary_lines || [];
|
|
101
|
-
|
|
76
|
+
if (activeId() === n.id) {
|
|
77
|
+
return el('div', { class: 'td-day__notes', 'data-notes-block': 'docked', 'data-notes-id': n.id },
|
|
78
|
+
el('h4', null, 'Meeting notes'),
|
|
79
|
+
el('p', { class: 'td-day__note' }, 'This note is open at the bottom right.'),
|
|
80
|
+
el('div', { class: 'td-day__acts' }, button({ label: 'Open the note', size: 'sm', attrs: { 'data-notes-open': n.id }, onClick: () => openDock(n.id) })));
|
|
81
|
+
}
|
|
102
82
|
return el('div', { class: 'td-day__notes', 'data-notes-block': 'linked', 'data-notes-id': n.id },
|
|
103
83
|
el('h4', null, 'Meeting notes'),
|
|
104
84
|
n.private ? el('p', { class: 'td-day__note' }, 'This note is private.')
|
|
105
85
|
: lines.length ? el('ul', { class: 'nt-lsum' }, ...lines.map((l) => el('li', null, l)))
|
|
106
86
|
: el('p', { class: 'td-day__note' }, n.segments ? 'No summary yet. Open the note and press Summarize.' : 'No summary yet.'),
|
|
107
|
-
|
|
108
|
-
busy ? null : el('div', { class: 'td-day__acts' }, button({ label: 'Open notes', size: 'sm', attrs: { 'data-notes-open': n.id }, onClick: () => show(n.id) })));
|
|
87
|
+
el('div', { class: 'td-day__acts' }, button({ label: 'Open notes', size: 'sm', attrs: { 'data-notes-open': n.id }, onClick: () => openDock(n.id) })));
|
|
109
88
|
}
|
|
110
89
|
if (!notesKnown()) return null;
|
|
111
90
|
const take = button({ label: 'Take notes', variant: ended ? undefined : 'primary', size: 'sm', attrs: { 'data-notes-take': e.id || '' }, onClick: () => takeNotes(e, opts, take) });
|
|
112
91
|
return el('div', { class: 'td-day__notes', 'data-notes-block': 'none' },
|
|
113
92
|
el('h4', null, 'Meeting notes'),
|
|
114
|
-
el('p', { class: 'td-day__note' }, ended ? 'No notes were taken in PMOS for this meeting.' : 'Take notes
|
|
93
|
+
el('p', { class: 'td-day__note' }, ended ? 'No notes were taken in PMOS for this meeting.' : 'Take notes when the meeting starts. PMOS listens at once and keeps the note at the bottom right. It is linked to this event' + (opts.initiative ? ' and its initiative.' : '.')),
|
|
115
94
|
el('div', { class: 'td-day__acts' }, take));
|
|
116
95
|
}
|
|
117
96
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* pm/view-notes.js — the Notes view tab: one meeting on one screen (meeting-notes, Runs 1 to
|
|
1
|
+
/* pm/view-notes.js — the Notes view tab: one meeting on one screen (meeting-notes, Runs 1 to 6).
|
|
2
2
|
*
|
|
3
3
|
* The screen follows the simplified mockup the PM reviewed (prototype/meeting-notes):
|
|
4
4
|
* header the title and the date, and one action: Listen; while listening a red dot, the time, the sources and Stop;
|
|
@@ -28,8 +28,10 @@
|
|
|
28
28
|
* calendar) and which initiative it belongs to, and how that was set: from the calendar event, suggested from what was said,
|
|
29
29
|
* or chosen by the PM. Change opens a sheet for each.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* MINIMIZED AT THE BOTTOM RIGHT (Run 6). Take notes on a calendar event starts listening at once (listenNow: no chooser) and
|
|
32
|
+
* keeps the note as a pill at the bottom right (pm/note-dock.js). The pill opens the note as a pop-up, which renderInline
|
|
33
|
+
* draws in one column. While the pop-up shows a note, the Notes tab says so and does not draw it a second time.
|
|
34
|
+
* Open in Notes shows it on the full screen.
|
|
33
35
|
*
|
|
34
36
|
* Reads and writes (lib/routes-notes.js). Notes live in the per-user runtime directory, never in a repository:
|
|
35
37
|
* GET /api/notes · /api/notes/one?id=… · /api/notes/engine?ws=… · /api/notes/recording?id=… · /api/people?ws=…
|
|
@@ -67,7 +69,8 @@ let busy = '';
|
|
|
67
69
|
let player = null; // the replay: { id, bytes, el, playing }, kept across redraws so playback goes on
|
|
68
70
|
let people = null; // { ws, names } — the repository's people, offered in the line editor
|
|
69
71
|
let pendingOpen = null; // a note another page asked to open (openNote), selected at the next draw
|
|
70
|
-
let inlineSwitch = null; // the note
|
|
72
|
+
let inlineSwitch = null; // the note the pop-up is switching to (renderInline), so a redraw does not start it twice
|
|
73
|
+
let popped = null; // the note the pop-up shows (pm/note-dock.js), which the Notes tab then does not draw
|
|
71
74
|
const LABELS = [['decision', 'Decision'], ['action', 'Action'], ['question', 'Question'], ['fact', 'Fact'], ['topic', 'Topic'], ['statement', 'Statement'], ['small talk', 'Small talk']];
|
|
72
75
|
|
|
73
76
|
const nowClock = (iso) => { try { return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch { return ''; } };
|
|
@@ -1021,7 +1024,12 @@ export function renderBoard(inner, ctx) {
|
|
|
1021
1024
|
if (banner) wrap.appendChild(el('div', { class: 'nt-banner' + (banner.tone === 'bad' ? ' nt-banner--bad' : ''), 'data-nt-banner': banner.tone, role: banner.tone === 'bad' ? 'alert' : 'status' }, banner.text));
|
|
1022
1025
|
const n = currentId ? cache.get(currentId) : null;
|
|
1023
1026
|
if (currentId && !n) { loadNote(ctx, currentId); wrap.appendChild(el('p', { class: 'nt-hint' }, 'reading the note …')); }
|
|
1024
|
-
else if (
|
|
1027
|
+
else if (n && popped === n.id) {
|
|
1028
|
+
wrap.appendChild(el('div', { class: 'nt-start', 'data-nt-popped': '' },
|
|
1029
|
+
el('h2', { class: 'nt-h' }, n.title || 'Untitled note'),
|
|
1030
|
+
el('p', { class: 'nt-hint' }, 'This note is open in the pop-up at the bottom right.'),
|
|
1031
|
+
button({ label: 'Show it here', variant: 'primary', onClick: () => openFull(ctx, n.id), attrs: { 'data-nt-unpop': '' } })));
|
|
1032
|
+
} else if (!n) {
|
|
1025
1033
|
wrap.appendChild(el('div', { class: 'nt-start' },
|
|
1026
1034
|
el('h2', { class: 'nt-h' }, 'Notes'),
|
|
1027
1035
|
el('p', { class: 'nt-hint', 'data-nt-about': '' }, 'Take notes in a meeting. PMOS listens on this machine, catches the decisions, actions and questions, and checks them against your repository. No audio leaves this machine, and notes are kept outside every repository.'),
|
|
@@ -1055,17 +1063,39 @@ function keepFocus() {
|
|
|
1055
1063
|
}
|
|
1056
1064
|
async function openFull(ctx, id) {
|
|
1057
1065
|
await flushSave(ctx);
|
|
1058
|
-
|
|
1066
|
+
popped = null;
|
|
1067
|
+
const [c, d] = await Promise.all([import('./panel-cockpit.js'), import('./note-dock.js')]);
|
|
1068
|
+
d.minimize();
|
|
1059
1069
|
pendingOpen = id;
|
|
1060
1070
|
c.openView('notes');
|
|
1061
1071
|
}
|
|
1062
1072
|
|
|
1073
|
+
/** The note the pop-up shows (pm/note-dock.js), or null. */
|
|
1074
|
+
export function setPopped(id) { popped = id || null; }
|
|
1075
|
+
/** The note's title as last read or typed, or ''. */
|
|
1076
|
+
export const titleOf = (id) => (editor && editor.id === id && editor.title.value.trim()) || ((cache.get(id) || {}).title) || '';
|
|
1077
|
+
/** When the note that is listening started, or null. */
|
|
1078
|
+
export const liveStarted = () => (audio && audio.state === 'on' ? audio.started : speech && speech.mode === 'record' && speech.state === 'on' ? speech.started : null);
|
|
1079
|
+
/** Stop the note that is listening, as its Stop does. */
|
|
1080
|
+
export const stopListening = (ctx) => stopRecording(ctx);
|
|
1081
|
+
/** Start listening on this note at once, with no chooser (Run 6, Take notes): the microphone and the meeting when Whisper is set
|
|
1082
|
+
* up, else the microphone through the browser's on-device recognition. False when the note cannot listen (private, gone). */
|
|
1083
|
+
export async function listenNow(ctx, id) {
|
|
1084
|
+
if (id !== currentId) await selectNote(ctx, id);
|
|
1085
|
+
let n = cache.get(id);
|
|
1086
|
+
if (!n) { await loadNote(ctx, id); n = cache.get(id); }
|
|
1087
|
+
if (!n || n.private) return false;
|
|
1088
|
+
const eng = engine && engine.ws === wsOf(ctx) && engine.whisper ? engine : await loadEngine(ctx);
|
|
1089
|
+
if (eng.whisper && eng.whisper.ready) await startAudio(ctx, n, ['mic', 'meeting']); else await startSpeech(ctx, n, 'record');
|
|
1090
|
+
return listeningId() === id;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1063
1093
|
/** The note that is listening now, or null. */
|
|
1064
1094
|
export const listeningId = () => (audio && audio.id) || (speech && speech.mode === 'record' && (speech.state === 'on' || speech.state === 'checking') ? speech.id : null);
|
|
1065
1095
|
|
|
1066
|
-
/** One note drawn
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1096
|
+
/** One note drawn in the pop-up (meeting-notes Run 6, pm/note-dock.js): the same note, editor and listening as the Notes tab,
|
|
1097
|
+
* in one column, with Open in Notes for the full screen. It never stops a meeting that is being heard: when another note is
|
|
1098
|
+
* listening it draws nothing and returns false. */
|
|
1069
1099
|
export function renderInline(host, ctx, id) {
|
|
1070
1100
|
if (ctx.FIXTURE) return false;
|
|
1071
1101
|
const other = listeningId();
|