create-pmos 0.152.0 → 0.153.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/console-calendar.md +3 -3
- package/workspace/lib/notes.js +22 -4
- package/workspace/lib/routes-notes.js +2 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-notes.js +97 -13
- package/workspace/public/pm/console3.css +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.153.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T04:29:06Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T04:29:06Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T04:29:06Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -13,13 +13,13 @@ The Calendar tab of the console page (`/c3.html`) shows time. It shows what happ
|
|
|
13
13
|
|
|
14
14
|
The switch at the start of the bar has two parts. **Calendar** shows the grid. **Notes** shows your notes.
|
|
15
15
|
|
|
16
|
-
In **Notes**, the list is at the left
|
|
16
|
+
In **Notes**, the list is at the left, under the heading **Notes**. The notes are in groups by what they are about: **Portfolio** first, then one group for each board, in the order of the portfolio. **Not linked** is the last group. A note linked to an initiative from an earlier version, with no board, is in **Initiatives**. The number shows how many notes the list shows. Type in **Search notes** to keep only the notes whose title, board, initiative or summary has the words. A row shows when the note was made and its length. In a board group, a row shows its initiative, or **board** when the note is about the whole board. 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
17
|
|
|
18
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
19
|
|
|
20
|
-
Under the title, the note shows its meeting
|
|
20
|
+
Under the title, the note shows its meeting. Under the meeting, it shows what the note is about: the **Portfolio**, a board (for example a stand-up or a sprint planning), or an initiative on its board (for example `IDPF › Partner API rate limits`). Press **Link to a meeting** to link the note to a meeting. Press **Choose what it is about** to open one menu: **Portfolio** (every board), each board (the whole board), and the current and next initiatives of each board under it. When the note has a link, press **Change** to choose another one, or **Not linked** in the menu to remove it. The note moves into its group.
|
|
21
21
|
|
|
22
|
-
- **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, the people, and the meeting: its time, its place and who organized it. 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 **
|
|
22
|
+
- **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, the people, and the meeting: its time, its place and who organized it. 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 **Choose what it is about**.
|
|
23
23
|
- **During** shows the live notes while the note is recorded or paused. At other times it shows the transcript.
|
|
24
24
|
- **After** shows **Replay** first when the sound of the note is kept on this machine. Then it 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, the highlights and the screenshots of the shared screen (**Screen**; press one to see it large). The transcript is folded at the bottom. Click a text to correct it: the summary, a decision and its why, or a next step, its owner and its due day. Press `Enter` or leave the text to save it. Press `Esc` to cancel. Empty an owner or a due day to clear it. A decision caught while the meeting was recorded has no why. Before the final notes are written, press **Write a summary of the meeting** to write your own summary; empty it to go back to the caught one. **Make task** adds a next step to the note's initiative.
|
|
25
25
|
|
package/workspace/lib/notes.js
CHANGED
|
@@ -151,7 +151,7 @@ function stateFields(n, now, resumeMs) {
|
|
|
151
151
|
function rowOf(n, now = Date.now(), resumeMs = RESUME_MS) {
|
|
152
152
|
return { id: n.id, title: n.title || 'Untitled note', created_at: n.created_at, updated_at: n.updated_at, private: !!n.private,
|
|
153
153
|
words: wordsOf(n.body), segments: (n.transcript || []).length, summary: !!(n.summary || (n.agent && n.agent.markdown)),
|
|
154
|
-
meeting: n.meeting || null, initiative: n.initiative || null, ws: n.ws || null,
|
|
154
|
+
meeting: n.meeting || null, initiative: n.initiative || null, board: n.board || null, portfolio: !!n.portfolio, ws: n.ws || null,
|
|
155
155
|
// the meeting time so far (a paused note's clock) and how many statements differ from a repository file (the Notes menu)
|
|
156
156
|
meeting_ms: n.recorded_from ? meetingMs(n) : 0, conflicts: n.private ? 0 : conflictsOf(n).length,
|
|
157
157
|
// a private note shows its title on the calendar and the initiative, never its summary
|
|
@@ -237,6 +237,14 @@ function cleanInitiative(x, by) {
|
|
|
237
237
|
return { ok: true, initiative: { id: String(x.id), ws: typeof x.ws === 'string' && x.ws.length <= 40 ? x.ws : null,
|
|
238
238
|
repo: typeof x.repo === 'string' && x.repo.length <= 120 ? x.repo : null, title: x.title ? cleanTitle(x.title) : null, by: ['pm', 'event', 'local'].includes(by) ? by : 'pm', ...(by === 'local' && Number.isFinite(x.shared) ? { shared: x.shared } : {}) } };
|
|
239
239
|
}
|
|
240
|
+
/** A board link: { id, ws, title, by }, the board of /api/boards in its workspace (console3 Calendar › Notes). */
|
|
241
|
+
const BOARD_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
242
|
+
function cleanBoard(x, by) {
|
|
243
|
+
if (x === null) return { ok: true, board: null };
|
|
244
|
+
if (!x || typeof x !== 'object' || !BOARD_ID_RE.test(String(x.id || ''))) return err(400, 'that is not a board id');
|
|
245
|
+
return { ok: true, board: { id: String(x.id), ws: typeof x.ws === 'string' && x.ws.length <= 40 ? x.ws : null, title: x.title ? cleanTitle(x.title) : null,
|
|
246
|
+
by: ['pm', 'event', 'local'].includes(by) ? by : 'pm' } };
|
|
247
|
+
}
|
|
240
248
|
/** Whether a note's meeting is this calendar event: the same id, or the same start minute and title (the brief and the
|
|
241
249
|
* calendar memory give one meeting different ids). Pure. */
|
|
242
250
|
function sameEvent(meeting, e) {
|
|
@@ -253,9 +261,10 @@ function findByEvent(dir, e) {
|
|
|
253
261
|
hits.sort((a, b) => String(b.updated_at).localeCompare(String(a.updated_at)));
|
|
254
262
|
return hits[0] || null;
|
|
255
263
|
}
|
|
256
|
-
/** Link a note to its meeting and to
|
|
257
|
-
*
|
|
258
|
-
|
|
264
|
+
/** Link a note to its meeting and to what it is about: an initiative, a board, or the whole Portfolio (`portfolio: true`,
|
|
265
|
+
* which clears the board and the initiative). A note with an initiative also keeps the initiative's board. A field left out
|
|
266
|
+
* is kept. The PM's initiative or board is never replaced by a suggestion: `by: 'local'` is refused when the PM chose one. */
|
|
267
|
+
function link(dir, { id, meeting, initiative, board, portfolio, by = 'pm', now = Date.now() } = {}) {
|
|
259
268
|
const note = load(dir, id);
|
|
260
269
|
if (!note) return err(404, 'no such note');
|
|
261
270
|
if (meeting !== undefined) {
|
|
@@ -272,7 +281,16 @@ function link(dir, { id, meeting, initiative, by = 'pm', now = Date.now() } = {}
|
|
|
272
281
|
const c = cleanInitiative(initiative, by);
|
|
273
282
|
if (!c.ok) return c;
|
|
274
283
|
note.initiative = c.initiative;
|
|
284
|
+
if (c.initiative) delete note.portfolio;
|
|
285
|
+
}
|
|
286
|
+
if (board !== undefined && !(by !== 'pm' && note.board && note.board.by === 'pm')) {
|
|
287
|
+
const c = cleanBoard(board, by);
|
|
288
|
+
if (!c.ok) return c;
|
|
289
|
+
note.board = c.board;
|
|
290
|
+
if (c.board) delete note.portfolio;
|
|
275
291
|
}
|
|
292
|
+
if (portfolio === true) { note.portfolio = true; note.board = null; note.initiative = null; }
|
|
293
|
+
else if (portfolio === false) delete note.portfolio;
|
|
276
294
|
note.updated_at = iso(now);
|
|
277
295
|
store(dir, note);
|
|
278
296
|
return { ok: true, note };
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* POST /api/notes/bullet { id, bullet, cat?, drop?, text?, owner?, due? } → { ok, bullet, summary } the PM's correction to one bullet (Run 8)
|
|
11
11
|
* POST /api/notes/new { title?, meeting?, initiative? } → { ok, note, reused } a calendar event that already has a note
|
|
12
12
|
* gets that note back (reused: true), so one meeting keeps one note (Run 4)
|
|
13
|
-
* POST /api/notes/link { id, meeting?, initiative?, from_event? } → { ok, note, kept? } the PM's link; null unlinks (Run 4)
|
|
13
|
+
* POST /api/notes/link { id, meeting?, initiative?, board?, portfolio?, from_event? } → { ok, note, kept? } the PM's link; null unlinks (Run 4)
|
|
14
14
|
* GET /api/notes/initiatives?id=…&ws=… → { ok, open: [{ id, title }], pick } the repository's open initiatives, and the local suggestion
|
|
15
15
|
* POST /api/notes/save { id, title?, body? } → { ok, updated_at, versions, added } each new line of the notes is also
|
|
16
16
|
* a transcript line, source 'typed' (`added`, Run 8)
|
|
@@ -691,7 +691,7 @@ async function handle(req, res, url, ctx) {
|
|
|
691
691
|
return done(r);
|
|
692
692
|
}
|
|
693
693
|
// from_event: the initiative is the calendar event's own, and never replaces the PM's choice
|
|
694
|
-
if (p === '/api/notes/link') return done(notes.link(dir, { id, meeting: body.meeting, initiative: body.initiative, by: body.from_event === true ? 'event' : 'pm' }));
|
|
694
|
+
if (p === '/api/notes/link') return done(notes.link(dir, { id, meeting: body.meeting, initiative: body.initiative, board: body.board, portfolio: typeof body.portfolio === 'boolean' ? body.portfolio : undefined, by: body.from_event === true ? 'event' : 'pm' }));
|
|
695
695
|
if (p === '/api/notes/dictate') return done(notes.dictation(body.text));
|
|
696
696
|
if (p === '/api/notes/save') {
|
|
697
697
|
const r = notes.save(dir, { id, title: body.title, body: body.body });
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.153.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.153.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
* c3-calendar.js draws it when the Calendar | Notes switch is on Notes: notesView(ui) returns one persistent box, which this
|
|
4
4
|
* file redraws on its own (a read, a choice, a stage, a search), so the grid's data call is not repeated.
|
|
5
5
|
*
|
|
6
|
-
* the list "
|
|
7
|
-
*
|
|
6
|
+
* the list "Notes" with a count and a search field, grouped by what a note is about: Portfolio first, then each board in the
|
|
7
|
+
* portfolio's order, then "Not linked" (scopeGroups). A row: the time or day, the length, its initiative or a "board"
|
|
8
|
+
* tag, recording or paused, "no meeting", and a dot for a conflict.
|
|
9
|
+
* the head the note's meeting, then what it is about (the Portfolio, a board, or an initiative on its board) with one picker
|
|
10
|
+
* (aboutMenu: /api/boards and each board's Now and next initiatives), the mockup the PM approved on 2026-10-12.
|
|
8
11
|
* the note the Calendar stage's layout (calendar-views.js stageNode, the mockup the PM approved on 2026-10-12): a head with
|
|
9
12
|
* Before, During and After, then the stage's cards.
|
|
10
13
|
* Before the meeting context the note kept (meeting.context): agenda (an item heard is ticked), what it is for, to be ready,
|
|
@@ -20,13 +23,14 @@
|
|
|
20
23
|
* noteOfEvent(e) the note linked to a calendar event, from the shared notes read (notes-links.js), or null
|
|
21
24
|
*/
|
|
22
25
|
import { el } from './dom.js';
|
|
23
|
-
import { button } from './primitives.js';
|
|
26
|
+
import { button, menu } from './primitives.js';
|
|
27
|
+
import { icon } from './icons.js';
|
|
24
28
|
import { c3ic } from './pm-work-view.js';
|
|
25
29
|
import { pmGet, pmPost } from './http.js';
|
|
26
30
|
import { toast } from './c3-toast.js';
|
|
27
31
|
import { onNotes, notesFor, refreshNotes } from './notes-links.js';
|
|
28
32
|
import { agendaHeard, peopleOf } from './notes-context.js';
|
|
29
|
-
import {
|
|
33
|
+
import { partsOf, conflictsOf, whenShort } from './dest-notes.js';
|
|
30
34
|
import * as dock from './note-dock.js';
|
|
31
35
|
import * as V from './view-notes.js';
|
|
32
36
|
import { makeTask } from './notes-task.js';
|
|
@@ -35,6 +39,10 @@ import { screenStrip } from './notes-screen-view.js';
|
|
|
35
39
|
const PHASES = [['before', 'clip', 'Before'], ['during', 'mic', 'During'], ['after', 'notes', 'After']];
|
|
36
40
|
const startOf = (n) => (n.meeting && n.meeting.start) || n.recorded_from || n.created_at || n.updated_at;
|
|
37
41
|
const iniName = (i) => (i ? i.title || i.id : null);
|
|
42
|
+
const boardName = (b) => (b ? b.title || b.id : null);
|
|
43
|
+
/* what a note is about, as the head's path: "Portfolio", "IDPF", or "IDPF › Partner API rate limits" */
|
|
44
|
+
const pathOf = (n) => (n.initiative ? [boardName(n.board), iniName(n.initiative)].filter(Boolean) : n.board ? [boardName(n.board)] : n.portfolio ? ['Portfolio'] : []);
|
|
45
|
+
const boardKey = (b) => (b.ws || '') + '/' + b.id;
|
|
38
46
|
const mins = (ms) => (ms >= 60000 ? Math.round(ms / 60000) + ' min' : '');
|
|
39
47
|
const lenOf = (n) => n.meeting_ms || Math.max(0, ...(n.transcript || []).map((x) => x.t || 0));
|
|
40
48
|
const dayOf = (iso) => { const t = new Date(iso); return Number.isFinite(t.getTime()) ? t.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' }) : ''; };
|
|
@@ -81,6 +89,7 @@ export function notesView(ui) {
|
|
|
81
89
|
const s = stateOf(ui.r);
|
|
82
90
|
s.ui = ui;
|
|
83
91
|
if (!s.unsub) s.unsub = onNotes(() => { if (s.box.isConnected) read(s); });
|
|
92
|
+
if (!s.boards) loadBoards(s);
|
|
84
93
|
if (!s.rows || (s.open && (!s.note || s.note.id !== s.open))) read(s);
|
|
85
94
|
draw(s);
|
|
86
95
|
return s.box;
|
|
@@ -96,6 +105,13 @@ async function read(s) {
|
|
|
96
105
|
draw(s);
|
|
97
106
|
}
|
|
98
107
|
|
|
108
|
+
/* the portfolio's boards, for the list's group order and the board names; a board the read misses keeps the note's own title */
|
|
109
|
+
async function loadBoards(s) {
|
|
110
|
+
s.boards = [];
|
|
111
|
+
const r = await pmGet('/api/boards');
|
|
112
|
+
if (r && r.ok && Array.isArray(r.boards)) { s.boards = r.boards.filter((b) => b && b.id && !b.error); if (s.box.isConnected) draw(s); }
|
|
113
|
+
}
|
|
114
|
+
|
|
99
115
|
function choose(s, id) {
|
|
100
116
|
if (s.open === id) return;
|
|
101
117
|
s.open = id; s.note = null; s.phase = null; s.shots = []; s.rec = null;
|
|
@@ -118,7 +134,7 @@ function draw(s) {
|
|
|
118
134
|
|
|
119
135
|
function listCard(s) {
|
|
120
136
|
const qtext = s.q.trim().toLowerCase();
|
|
121
|
-
const shown = (s.rows || []).filter((r) => !qtext || String(r.title || '').toLowerCase().includes(qtext) ||
|
|
137
|
+
const shown = (s.rows || []).filter((r) => !qtext || String(r.title || '').toLowerCase().includes(qtext) || pathOf(r).some((x) => String(x).toLowerCase().includes(qtext))
|
|
122
138
|
|| (r.summary_lines || []).some((x) => String(x).toLowerCase().includes(qtext)));
|
|
123
139
|
const search = el('input', { type: 'text', class: 'cal3-nsearch', placeholder: 'Search notes', 'aria-label': 'Search notes', value: s.q, 'data-cal3-notes': 'search' });
|
|
124
140
|
search.addEventListener('input', () => { s.q = search.value; draw(s); });
|
|
@@ -126,13 +142,28 @@ function listCard(s) {
|
|
|
126
142
|
if (!s.rows) body = fold('clock', 'Reading the notes', { role: 'status' });
|
|
127
143
|
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' });
|
|
128
144
|
else if (!shown.length) body = fold('search', 'No note matches “' + s.q.trim() + '”.', { 'data-cal3-notes': 'no-match' });
|
|
129
|
-
else body =
|
|
145
|
+
else body = scopeGroups(s, shown).map((g) => el('div', { class: 'cal3-ctx', 'data-cal3-notes-group': g.label }, el('h4', null, g.label),
|
|
130
146
|
el('ul', { class: 'list', role: 'list' }, g.rows.map((r) => noteRow(s, r)))));
|
|
131
147
|
return el('section', { class: 'card sect', 'data-cal3-notes': 'list', 'aria-label': 'Notes' },
|
|
132
|
-
el('div', { class: 'sh2' }, el('h2', null, c3ic('notes', 'sm'), '
|
|
148
|
+
el('div', { class: 'sh2' }, el('h2', null, c3ic('notes', 'sm'), 'Notes ', el('span', { class: 'bdg g', 'data-cal3-notes': 'count' }, String(shown.length)))),
|
|
133
149
|
el('div', { class: 'cal3-nsearchw' }, search), body);
|
|
134
150
|
}
|
|
135
151
|
|
|
152
|
+
/* the list's groups: Portfolio, each board in the portfolio's order (a board it does not list goes after them by name), an
|
|
153
|
+
* initiative linked before boards were (no board on the note) under "Initiatives", then "Not linked" */
|
|
154
|
+
function scopeGroups(s, rows) {
|
|
155
|
+
const order = new Map((s.boards || []).map((b, i) => [boardKey(b), i]));
|
|
156
|
+
const known = new Map((s.boards || []).map((b) => [boardKey(b), b.name || b.id]));
|
|
157
|
+
const g = new Map();
|
|
158
|
+
for (const r of rows) {
|
|
159
|
+
const k = r.portfolio ? '@portfolio' : r.board ? boardKey(r.board) : r.initiative ? '@initiative' : '';
|
|
160
|
+
if (!g.has(k)) g.set(k, { key: k, label: k === '@portfolio' ? 'Portfolio' : k === '@initiative' ? 'Initiatives' : !k ? 'Not linked' : known.get(k) || boardName(r.board), rows: [] });
|
|
161
|
+
g.get(k).rows.push(r);
|
|
162
|
+
}
|
|
163
|
+
const rank = (x) => (x.key === '@portfolio' ? [0, 0] : x.key === '@initiative' ? [3, 0] : !x.key ? [4, 0] : order.has(x.key) ? [1, order.get(x.key)] : [2, 0]);
|
|
164
|
+
return [...g.values()].sort((a, b) => { const p = rank(a); const q = rank(b); return p[0] - q[0] || p[1] - q[1] || a.label.localeCompare(b.label); });
|
|
165
|
+
}
|
|
166
|
+
|
|
136
167
|
function noteRow(s, r) {
|
|
137
168
|
const mode = held(r.id) || (r.state === 'recording' || r.state === 'paused' ? r.state : null);
|
|
138
169
|
const on = s.open === r.id;
|
|
@@ -142,7 +173,9 @@ function noteRow(s, r) {
|
|
|
142
173
|
btn('cal3-nopen', { title: 'Open ' + (r.title || 'Untitled note'), 'aria-current': on ? 'true' : 'false' }, [
|
|
143
174
|
el('span', { class: 'tt' }, r.title || 'Untitled note'),
|
|
144
175
|
el('span', { class: 'chips' }, el('span', { class: 'dim mono cal3-xs' }, [mode ? 'now' : whenShort(startOf(r)), len].filter(Boolean).join(' · ')),
|
|
145
|
-
mode ? el('span', { class: 'tag', 'data-cal3-notes': 'live' }, mode === 'paused' ? 'paused' : 'recording')
|
|
176
|
+
mode ? el('span', { class: 'tag', 'data-cal3-notes': 'live' }, mode === 'paused' ? 'paused' : 'recording')
|
|
177
|
+
: r.initiative ? el('span', { class: 'chip2', title: 'Initiative', 'data-cal3-notes': 'row-initiative' }, c3ic('init', 'sm'), iniName(r.initiative))
|
|
178
|
+
: r.board ? el('span', { class: 'tag', title: 'Linked to the board, not to one initiative', 'data-cal3-notes': 'row-board' }, 'board') : null,
|
|
146
179
|
!linked(r) && !mode ? el('span', { class: 'tag', title: 'Not linked to a meeting', 'data-cal3-notes': 'no-meeting' }, 'no meeting') : null,
|
|
147
180
|
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)],
|
|
148
181
|
() => choose(s, r.id)),
|
|
@@ -172,7 +205,7 @@ function noteCol(s) {
|
|
|
172
205
|
return el('div', { class: 'cal3-ncol', 'data-cal3-notes': 'note', 'data-cal3-note': n.id, 'data-cal3-stage': ph }, head, ...[].concat(body));
|
|
173
206
|
}
|
|
174
207
|
|
|
175
|
-
/* the note's meeting and
|
|
208
|
+
/* the note's meeting (view-notes.js picker) and what it is about (aboutMenu), each with what it is linked to, then Link or Change */
|
|
176
209
|
function links(s, n, ev) {
|
|
177
210
|
const adhoc = n.meeting && n.meeting.kind === 'adhoc';
|
|
178
211
|
const when = ev ? [dayOf(ev.start), hm(ev.start) + (ev.end ? ' to ' + hm(ev.end) : '')].filter(Boolean).join(' ') : '';
|
|
@@ -183,9 +216,60 @@ function links(s, n, ev) {
|
|
|
183
216
|
!ev && !adhoc ? el('span', { class: 'tag' }, 'no meeting') : null,
|
|
184
217
|
btn('chip2 lk', { 'data-cal3-notes': 'meeting-link', title: ev || adhoc ? 'Link the note to another meeting' : 'Link the note to a meeting on the calendar' }, ev || adhoc ? 'Change' : 'Link to a meeting', () => V.linkMeeting(vctx(s), n.id)),
|
|
185
218
|
ev ? btn('chip2 lk', { 'data-cal3-notes': 'show-meeting', title: 'Show the meeting in the calendar' }, [c3ic('next', 'sm'), 'Show in the calendar'], () => showMeeting(s, ev)) : null),
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
219
|
+
aboutLine(s, n));
|
|
220
|
+
}
|
|
221
|
+
function aboutLine(s, n) {
|
|
222
|
+
const path = pathOf(n);
|
|
223
|
+
const b = btn('chip2 lk', { 'data-cal3-notes': 'about-link', title: 'Choose what the note is about: the Portfolio, a board or an initiative' }, path.length ? 'Change' : 'Choose what it is about', () => aboutMenu(s, n, b));
|
|
224
|
+
return el('span', { class: 'chips2' }, c3ic('init', 'sm'),
|
|
225
|
+
path.length ? el('span', { 'data-cal3-notes': 'about' }, el('b', null, path.join(' › '))) : el('span', { class: 'dim', 'data-cal3-notes': 'about' }, 'Not linked to the Portfolio, a board or an initiative'),
|
|
226
|
+
b);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* each board with its Now and next initiatives (GET /api/boards/backlog), kept for a minute once the repository is read */
|
|
230
|
+
async function boardsWithInitiatives(s) {
|
|
231
|
+
if (s.about && Date.now() - s.about.at < 60000) return s.about.boards;
|
|
232
|
+
const r = await pmGet('/api/boards');
|
|
233
|
+
if (!r || !r.ok || !Array.isArray(r.boards)) throw new Error((r && r.error) || 'the boards could not be read');
|
|
234
|
+
const boards = r.boards.filter((b) => b && b.id && !b.error);
|
|
235
|
+
let pending = !!r.pending;
|
|
236
|
+
await Promise.all(boards.map(async (b) => {
|
|
237
|
+
const x = await pmGet('/api/boards/backlog?ws=' + encodeURIComponent(b.ws || '') + '&board=' + encodeURIComponent(b.id) + '&limit=100');
|
|
238
|
+
if (!x || !x.ok || x.pending || b.pending) pending = true;
|
|
239
|
+
b.inis = x && x.ok && Array.isArray(x.rows) ? x.rows : [];
|
|
240
|
+
}));
|
|
241
|
+
// while the repository is still being read, the initiatives are not all known: the next open reads them again
|
|
242
|
+
s.about = pending ? null : { at: Date.now(), boards };
|
|
243
|
+
return boards;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* What is this note about? One menu: Portfolio (every board), each board (the whole board) with its initiatives under it, and
|
|
247
|
+
* Not linked when the note has a link. Choosing one saves it (POST /api/notes/link) and the note moves into its group. */
|
|
248
|
+
async function aboutMenu(s, n, anchor) {
|
|
249
|
+
let boards;
|
|
250
|
+
try { boards = await boardsWithInitiatives(s); } catch (e) { toast('The boards could not be read: ' + e.message); return; }
|
|
251
|
+
if (!anchor.isConnected) return;
|
|
252
|
+
const cur = n.initiative ? 'i:' + (n.initiative.ws || '') + '/' + n.initiative.id : n.board ? 'b:' + boardKey(n.board) : n.portfolio ? 'p' : '';
|
|
253
|
+
const save = async (body, said) => {
|
|
254
|
+
const r = await pmPost('/api/notes/link', { id: n.id, ...body });
|
|
255
|
+
if (!r || !r.ok) { toast('Not saved' + ((r && r.error) ? ': ' + r.error : '')); return; }
|
|
256
|
+
toast(said);
|
|
257
|
+
refreshNotes();
|
|
258
|
+
read(s);
|
|
259
|
+
};
|
|
260
|
+
const item = (k, label, detail, ic, on, sub) => ({ label, detail, role: 'menuitemradio', iconNode: sub ? el('span', { class: 'cal3-mind' }, icon(ic)) : icon(ic),
|
|
261
|
+
attrs: { 'data-cal3-about': k, 'aria-checked': String(cur === k) }, onSelect: on });
|
|
262
|
+
const items = [item('p', 'Portfolio', 'every board', 'chart-column', () => save({ portfolio: true }, 'About the Portfolio. The note moved into its group.'))];
|
|
263
|
+
for (const b of boards) {
|
|
264
|
+
const board = { id: b.id, ws: b.ws || null, title: b.name || b.id };
|
|
265
|
+
items.push(item('b:' + boardKey(b), board.title, 'the whole board', 'square-kanban', () => save({ board, initiative: null }, 'About ' + board.title + '. The note moved into its group.')));
|
|
266
|
+
for (const i of b.inis || []) {
|
|
267
|
+
items.push(item('i:' + (b.ws || '') + '/' + i.id, i.title || i.id, null, 'target',
|
|
268
|
+
() => save({ board, initiative: { id: i.id, ws: b.ws || null, title: i.title || i.id } }, 'About ' + (i.title || i.id) + ' on ' + board.title + '. The note moved into its group.'), true));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (cur) items.push({ sep: true }, item('none', 'Not linked', 'remove the link', 'x', () => save({ portfolio: false, board: null, initiative: null }, 'Not linked to the Portfolio, a board or an initiative.')));
|
|
272
|
+
menu({ anchor, items, ariaLabel: 'What is this note about?' });
|
|
189
273
|
}
|
|
190
274
|
|
|
191
275
|
/* the replay of the note's kept audio: one <audio> per note, kept across redraws so playback goes on (as dest-notes.js does) */
|
|
@@ -207,7 +291,7 @@ function seek(s, ms) {
|
|
|
207
291
|
const vctx = (s) => ({ state: {}, setState: () => { if (s.box.isConnected) { read(s); } } });
|
|
208
292
|
const linkButtons = (s, n) => el('div', { class: 'chips2' },
|
|
209
293
|
btn('chip2 lk', { 'data-cal3-notes': 'link-meeting' }, [c3ic('cal', 'sm'), 'Link to a meeting'], () => V.linkMeeting(vctx(s), n.id)),
|
|
210
|
-
btn('chip2 lk', { 'data-cal3-notes': 'link-
|
|
294
|
+
btn('chip2 lk', { 'data-cal3-notes': 'link-about' }, [c3ic('init', 'sm'), 'Choose what it is about'], (e) => aboutMenu(s, n, e.currentTarget)));
|
|
211
295
|
|
|
212
296
|
/* a row of the stage's prepared list: the icon, the label, what it holds */
|
|
213
297
|
const prepRow = (icon, label, detail, done) => el('div', { class: 'chips2 cal3-prep', 'data-cal3-prep': label },
|
|
@@ -509,7 +509,7 @@
|
|
|
509
509
|
@container (min-width:1100px){.c3 .cal3-wrap{grid-template-columns:minmax(0,1fr) 340px}}
|
|
510
510
|
/* Calendar › Notes (c3-notes.js): the list beside the open note; one column on a narrow page */
|
|
511
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)}
|
|
512
|
+
.c3 .cal3-nwrap [data-cal3-notes="note"] .card{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--sp-2)}
|
|
513
513
|
.c3 .cal3-nwrap details>summary{cursor:pointer;padding:var(--sp-1) 0}
|
|
514
514
|
.c3 .cal3-ncol{display:grid;gap:var(--sp-3);align-content:start;min-width:0}
|
|
515
515
|
.c3 .cal3-nhead{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}
|
|
@@ -528,7 +528,9 @@
|
|
|
528
528
|
.c3 .cal3-term{max-height:60vh;overflow:auto}
|
|
529
529
|
.c3 .cal3-ov{margin:0}
|
|
530
530
|
.c3 .cal3-tdr{text-align:right}
|
|
531
|
-
.c3 .cal3-ncard{display:grid;gap:var(--sp-2)}
|
|
531
|
+
.c3 .cal3-ncard{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--sp-2)}
|
|
532
|
+
/* the About menu (c3-notes.js aboutMenu): an initiative sits indented under its board; the menu lives in the overlay layer */
|
|
533
|
+
.cal3-mind{display:inline-flex;padding-left:16px}
|
|
532
534
|
.c3 [data-cal3-notes-edit]{cursor:text;border-radius:6px;outline-offset:2px}
|
|
533
535
|
@media (hover:hover){.c3 [data-cal3-notes-edit]:hover{background:var(--muted)}}
|
|
534
536
|
.c3 .cal3-ed{display:inline-block;min-width:6ch}
|