create-pmos 0.142.0 → 0.144.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-initiatives-test.js +16 -3
- package/workspace/guide/console-calendar.md +20 -2
- package/workspace/guide/initiative-boards.md +2 -2
- package/workspace/guide/jira.md +15 -1
- package/workspace/guide/notes.md +2 -2
- package/workspace/lib/engine/boards.js +15 -5
- package/workspace/lib/jira-status.js +29 -9
- package/workspace/lib/jira-tickets.js +30 -2
- package/workspace/lib/routes-boards.js +61 -14
- 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-demo-boards.js +23 -4
- package/workspace/public/pm/c3-ib-items.js +3 -3
- package/workspace/public/pm/c3-ib-releases.js +2 -2
- package/workspace/public/pm/c3-ib-sprints.js +3 -3
- package/workspace/public/pm/c3-ii-tabs.js +8 -4
- package/workspace/public/pm/c3-ini-ui.js +2 -0
- package/workspace/public/pm/c3-initiatives.js +41 -12
- package/workspace/public/pm/c3-notes.js +316 -0
- 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/skills/jira-sync.skill +6 -4
- package/workspace/test-lists/c3-notes-place.json +5 -0
|
@@ -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();
|
|
@@ -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];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jira-sync
|
|
3
3
|
label: "Jira status check"
|
|
4
|
-
description: "Read, read-only, the status, sprint and subtasks of the Jira tickets each initiative pushed, and save them in PMOS for the initiative's Jira tickets tab. It writes nothing to Jira."
|
|
4
|
+
description: "Read, read-only, the status, sprint and subtasks of the Jira tickets each initiative pushed, or of its Jira epics and the issues under them, and save them in PMOS for the initiative's Jira tickets tab. It writes nothing to Jira."
|
|
5
5
|
run: agent
|
|
6
6
|
agent: claude
|
|
7
7
|
output:
|
|
@@ -15,15 +15,17 @@ output:
|
|
|
15
15
|
Rules that always hold:
|
|
16
16
|
1. READ-ONLY on Jira. Use only the Jira search tool. Never create, edit, transition, assign, comment on, link, close or delete an issue. Never write to Confluence or to any other source.
|
|
17
17
|
2. THE ONLY WRITE is `pmos jira sync`, and `pmos people record` once at the end (step 5). Never edit a repository file. Never run `pmos item`. Never write a ledger by hand. Never commit; the PM's Sync carries the files.
|
|
18
|
-
3. LIGHT ON CONTENT. Ask Jira only for these fields: summary, status, the sprint field, subtasks, the Flagged field, the story points field, the resolution date, and the display names of the assignee and the reporter. Never ask for or copy a description, a comment, an attachment, an email address, any other detail of a person, or a token.
|
|
18
|
+
3. LIGHT ON CONTENT. Ask Jira only for these fields: summary, status, the issue type, the parent, the sprint field, subtasks, the Flagged field, the story points field, the resolution date, and the display names of the assignee and the reporter. Never ask for or copy a description, a comment, an attachment, an email address, any other detail of a person, or a token.
|
|
19
19
|
4. TEMPORARY FILES OUTSIDE THE REPOSITORY. Save each search result in this session's scratch folder, never in the repository, and delete it after the sync.
|
|
20
20
|
5. NOTHING INVENTED. Give `pmos jira sync` exactly what Jira returned. If Jira cannot be reached, list the initiative under "Not reached" and write nothing for it.
|
|
21
21
|
|
|
22
22
|
Procedure:
|
|
23
|
-
1. If the task names one initiative ("This check: initiative …"), check that one only: run `pmos jira query <initiative>`. Otherwise run `pmos jira query`: each line is an initiative id, a tab and the Jira search (JQL) to run for it. If no initiative has a ticket in Jira, say so in one line and stop.
|
|
23
|
+
1. If the task names one initiative ("This check: initiative …"), check that one only: run `pmos jira query <initiative>`. Otherwise run `pmos jira query`: each line is an initiative id, a tab and the Jira search (JQL) to run for it. An initiative PMOS pushed no ticket for, but which is linked to Jira epics, gets a search for those epics and the issues under them. If no initiative has a ticket or an epic in Jira, say so in one line and stop.
|
|
24
24
|
2. For each initiative, run its search with the Jira search tool and the fields in rule 3. The sprint is usually a custom field named "Sprint" (often `customfield_10020`); ask for it by name. Story points are also a custom field, named "Story Points" or "Story point estimate" (often `customfield_10016`); ask for it by name. Fetch every page.
|
|
25
25
|
3. Save the result as JSON in the scratch folder, for example `<scratch>/jira-<initiative>.json`, as a list with one object per issue:
|
|
26
|
-
`{"key", "summary", "status", "category", "sprint", "subtasks", "points", "resolved", "flagged", "reason", "url"}`
|
|
26
|
+
`{"key", "summary", "status", "category", "type", "parent", "sprint", "subtasks", "points", "resolved", "flagged", "reason", "url"}`
|
|
27
|
+
- `type`: the issue type's name, for example `Epic`, `Story`, `Task` or `Bug`.
|
|
28
|
+
- `parent`: the parent issue's key (the epic of a story), or `null`.
|
|
27
29
|
- `category`: the status category, `new`, `indeterminate` or `done`.
|
|
28
30
|
- `sprint`: `{"id": "42", "name": "Sprint 42", "state": "active", "start": "2026-10-01", "end": "2026-10-14"}` for the active sprint, `"future"` for a planned one, or `null` when the issue is in the backlog. Copy `id`, `start`, `end` and `completed` (a closed sprint's completion date) from Jira's sprint field; leave out any that Jira does not give.
|
|
29
31
|
- `points`: the story points as a number, or `null` when none are set.
|
|
@@ -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
|
+
}
|