create-pmos 0.153.0 → 0.155.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/briefing-cards.md +1 -1
- package/workspace/guide/console-calendar.md +25 -2
- package/workspace/guide/console3-sessions.md +5 -1
- package/workspace/guide/document-sheet.md +18 -3
- package/workspace/lib/calendar-layers.js +130 -11
- package/workspace/lib/calendar-links.js +64 -0
- package/workspace/lib/engine/boards.js +4 -2
- package/workspace/lib/routes-boards.js +51 -1
- package/workspace/lib/routes-calendar-views.js +42 -3
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-calendar.js +105 -6
- package/workspace/public/pm/c3-demo-boards.js +4 -3
- package/workspace/public/pm/c3-demo-cal4.js +43 -3
- package/workspace/public/pm/c3-demo-init.js +16 -0
- package/workspace/public/pm/c3-demo.js +1 -0
- package/workspace/public/pm/c3-doc-sheet.js +328 -21
- package/workspace/public/pm/c3-dock.js +8 -15
- package/workspace/public/pm/c3-docs.js +2 -1
- package/workspace/public/pm/c3-ib-docs.js +3 -1
- package/workspace/public/pm/c3-shell.js +5 -6
- package/workspace/public/pm/c3-terminal-icons.js +0 -1
- package/workspace/public/pm/console3.css +96 -2
- package/workspace/test-lists/c3-doc-sheet-v2.json +6 -0
- package/workspace/test-lists/c3-meeting-board.json +5 -0
|
@@ -21,6 +21,7 @@ import { el } from './dom.js';
|
|
|
21
21
|
import { c3ic } from './pm-work-view.js';
|
|
22
22
|
import { button, ensureComponentsCss } from './primitives.js';
|
|
23
23
|
import { toast } from './c3-toast.js';
|
|
24
|
+
import { pmPost } from './http.js';
|
|
24
25
|
import { notesView, openNote, noteOfEvent, primeNotes } from './c3-notes.js'; // Calendar › Notes (initiative c3-notes-place)
|
|
25
26
|
import { onNotes } from './notes-links.js';
|
|
26
27
|
|
|
@@ -36,7 +37,8 @@ const KIND = {
|
|
|
36
37
|
};
|
|
37
38
|
const RANK = { milestone: 0, release: 1, report: 2, due: 3, own: 4 };
|
|
38
39
|
const TONE = { bad: 'Late or at risk of missing', warn: 'At risk', ok: 'On track' };
|
|
39
|
-
const REF = { initiative: 'Open the initiative', 'plan-item': 'Open the initiative', item: 'Open in Work', release: 'Open the release', report: 'Open in Reports'
|
|
40
|
+
const REF = { initiative: 'Open the initiative', 'plan-item': 'Open the initiative', item: 'Open in Work', release: 'Open the release', report: 'Open in Reports',
|
|
41
|
+
jira: 'Open the sprint', ask: 'Open in Triage', portfolio: 'Open the Portfolio' };
|
|
40
42
|
const CTXWORD = { decision: 'Decision', action: 'Action', moved: 'Moved', prepared: 'Prepared', todo: 'To prepare' };
|
|
41
43
|
const NO_SUGGESTION = 'Free, nothing specific to suggest';
|
|
42
44
|
/* the tidy-up suggestions (lib/calendar-tidy.js): the severity groups in order, and each rule in words */
|
|
@@ -528,6 +530,8 @@ function openRef(ui, ref, extra) {
|
|
|
528
530
|
if (ref.type === 'initiative') { if (!h.goInitiative(ref.id)) h.openInit(host, r, ref.id, extra.title); return; }
|
|
529
531
|
if (ref.type === 'plan-item' || ref.type === 'release') { const ini = extra.initiative || ref.initiative; if (!(ini && h.goInitiative(ini))) h.gotoTab('init'); return; }
|
|
530
532
|
if (ref.type === 'item') { h.gotoTab('work'); return; }
|
|
533
|
+
if (ref.type === 'jira' || ref.type === 'ask') { openBoardTab(ui, ref.board, ref.type === 'jira' ? 'sprints' : 'triage'); return; }
|
|
534
|
+
if (ref.type === PORTFOLIO) { openBoardTab(ui, null); return; }
|
|
531
535
|
if (ref.type === 'report') h.gotoTab('rep');
|
|
532
536
|
}
|
|
533
537
|
|
|
@@ -585,7 +589,8 @@ function meetingDetail(ui, hit) {
|
|
|
585
589
|
else if (past) body = [list(['decision', 'action'], 'What came out of it', 'out', 'No decision or action is recorded.'), list(['moved'], 'What moved after it', 'moved', 'Nothing moved within 48 hours that refers to it.')];
|
|
586
590
|
else body = upcomingBody(ui, rec, items);
|
|
587
591
|
const b = !past && rec && rec.brief ? rec.brief : null;
|
|
588
|
-
const
|
|
592
|
+
const lv = !past && rec ? rec.level || null : null;
|
|
593
|
+
const iniId = lv && lv.kind !== 'initiative' ? null : typeof e.initiative === 'string' && !(lv && lv.by === 'pm') ? e.initiative : b && b.initiative ? b.initiative.id : null;
|
|
589
594
|
const iniTitle = iniId ? (b && b.initiative && b.initiative.id === iniId && b.initiative.title) || (((ui.d.gantt || []).find((g) => g.id === iniId) || {}).title) || iniId : null;
|
|
590
595
|
const iniStatus = b && b.initiative && b.initiative.id === iniId ? b.initiative.status : (((ui.d.gantt || []).find((g) => g.id === iniId) || {}).status || null);
|
|
591
596
|
const sources = b && b.sources && b.sources.length ? 'Put together from ' + listWords(b.sources) + '. Each line opens its source. ' : '';
|
|
@@ -596,6 +601,7 @@ function meetingDetail(ui, hit) {
|
|
|
596
601
|
el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag', 'data-cal3': 'person' }, n)),
|
|
597
602
|
iniId ? el('span', { class: 'tag', 'data-cal3': 'meeting-initiative' }, 'Initiative: ' + iniTitle) : null,
|
|
598
603
|
iniStatus && STATUS_TONE[iniStatus] ? el('span', { class: 'tag cal3-tone-' + STATUS_TONE[iniStatus] }, iniStatus) : null),
|
|
604
|
+
past || !M.ctx ? null : levelRow(ui, e, lv),
|
|
599
605
|
noteLink(ui, e),
|
|
600
606
|
...body,
|
|
601
607
|
el('p', { class: 'dim cal3-note', 'data-cal3': past ? 'past-note' : 'sources' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : sources + 'PMOS proposes changes. It never edits your calendar.'),
|
|
@@ -611,13 +617,106 @@ const ctxBox = (key, title, ...kids) => el('div', { class: 'cal3-ctx', 'data-cal
|
|
|
611
617
|
const ul = (rows) => el('ul', { class: 'cal3-cl', role: 'list' }, ...rows);
|
|
612
618
|
const ci = (attrs, ...kids) => el('li', { class: 'cal3-ci', ...attrs }, ...kids);
|
|
613
619
|
|
|
620
|
+
/* ── what a meeting is about: the Portfolio, a board or an initiative (initiative c3-meeting-board) ──
|
|
621
|
+
* The switch says which level the card reads and why PMOS chose it (lib/calendar-layers.js levelOf). Pressing a level links every
|
|
622
|
+
* occurrence of the meeting to it (POST /api/calendar-links); Board and Initiative ask which one when there is more than one. */
|
|
623
|
+
// the meeting's level, not the old initiative list's Portfolio mode (cuts.test.js keeps that mode out)
|
|
624
|
+
const PORTFOLIO = 'portfolio';
|
|
625
|
+
const LEVELS = [[PORTFOLIO, 'Portfolio', 'Read every board'], ['board', 'Board', 'Read one board and its sprint'], ['initiative', 'Initiative', 'Read one initiative']];
|
|
626
|
+
const linkPick = new Map(); // event id → the level whose picker is open
|
|
627
|
+
|
|
628
|
+
function levelRow(ui, e, lv) {
|
|
629
|
+
const open = linkPick.get(e.id) || null;
|
|
630
|
+
const seg = el('div', { class: 'seg cal3-lvseg', role: 'group', 'aria-label': 'This meeting is about', 'data-cal3': 'level' },
|
|
631
|
+
...LEVELS.map(([k, label, title]) => { const x = button({ variant: 'ghost', attrs: { class: 'cal3-b', 'aria-pressed': String(!!lv && lv.kind === k), 'data-cal3-level': k, title },
|
|
632
|
+
onClick: () => pickLevel(ui, e, lv, k) }); x.replaceChildren(el('span', null, label)); return x; }));
|
|
633
|
+
const what = !lv ? 'Not linked yet. Pick a level and PMOS reads it before the meeting.'
|
|
634
|
+
: lv.kind === PORTFOLIO ? 'Reads every board, linked because ' + lv.why + '.'
|
|
635
|
+
: lv.kind === 'board' ? 'Reads the ' + (lv.name || lv.id) + ' board and its sprint, linked because ' + lv.why + '.'
|
|
636
|
+
: 'Reads ' + (lv.name || lv.id) + ', linked because ' + lv.why + '.';
|
|
637
|
+
const unlink = lv && lv.by === 'pm' ? el('a', { href: '#', class: 'cal3-ref', 'data-cal3': 'unlink', onClick: (ev) => { ev.preventDefault(); saveLink(ui, e, { kind: 'none' }, 'The link is removed.'); } }, 'Unlink') : null;
|
|
638
|
+
return el('div', { class: 'cal3-level', 'data-cal3-ctx': 'level', 'data-cal3-level-by': lv ? lv.by : 'none' }, seg, el('span', { class: 'dim cal3-note' }, what), unlink, open ? pickerOf(ui, e, open) : null);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function pickLevel(ui, e, lv, kind) {
|
|
642
|
+
if (lv && lv.kind === kind && lv.by === 'pm' && kind === PORTFOLIO) return;
|
|
643
|
+
if (kind === PORTFOLIO) { saveLink(ui, e, { kind }, 'This meeting now reads every board.'); return; }
|
|
644
|
+
const choices = choicesOf(ui, kind);
|
|
645
|
+
if (choices.length === 1) { saveLink(ui, e, linkBody(kind, choices[0]), 'This meeting now reads ' + choices[0].name + '.'); return; }
|
|
646
|
+
linkPick.set(e.id, linkPick.get(e.id) === kind ? null : kind);
|
|
647
|
+
ui.redraw();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const choicesOf = (ui, kind) => kind === 'board' ? (ui.d.link_boards || []).map((b) => ({ ws: b.ws, id: b.id, name: b.name }))
|
|
651
|
+
: (ui.d.gantt || []).filter((g) => g && g.id).map((g) => ({ id: g.id, name: g.title || g.id }));
|
|
652
|
+
const linkBody = (kind, c) => kind === 'board' ? { kind, ws: c.ws, board: c.id, title: c.name } : { kind, initiative: c.id, title: c.name };
|
|
653
|
+
|
|
654
|
+
function pickerOf(ui, e, kind) {
|
|
655
|
+
const choices = choicesOf(ui, kind);
|
|
656
|
+
if (!choices.length) return el('span', { class: 'dim cal3-note', 'data-cal3': 'level-none' }, kind === 'board' ? 'No board to link yet.' : 'No initiative to link in this range.');
|
|
657
|
+
const sel = el('select', { 'aria-label': kind === 'board' ? 'Pick the board' : 'Pick the initiative', 'data-cal3': 'level-pick',
|
|
658
|
+
onChange: () => { const c = choices[Number(sel.value)]; if (c) saveLink(ui, e, linkBody(kind, c), 'This meeting now reads ' + c.name + '.'); } },
|
|
659
|
+
el('option', { value: '' }, kind === 'board' ? 'Pick the board' : 'Pick the initiative'), ...choices.map((c, i) => el('option', { value: String(i) }, c.name)));
|
|
660
|
+
return sel;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function saveLink(ui, e, body, said) {
|
|
664
|
+
linkPick.delete(e.id);
|
|
665
|
+
let r = null;
|
|
666
|
+
try { r = await pmPost('/api/calendar-links', { event: { title: e.title || e.id, series: e.series || null }, ...body }); } catch (err) { r = { ok: false, error: String((err && err.message) || err) }; }
|
|
667
|
+
if (!r || !r.ok) { toast('The link was not saved: ' + ((r && r.error) || 'no answer') + '.'); ui.redraw(); return; }
|
|
668
|
+
toast(said);
|
|
669
|
+
ui.h.go(ui.host, ui.r, {});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/* the sprint's days left in words: working days after today up to its end */
|
|
673
|
+
function sprintLeft(h, end, today) {
|
|
674
|
+
if (!end) return '';
|
|
675
|
+
if (end < today) return 'ended ' + h.dayLabel(end);
|
|
676
|
+
let n = 0;
|
|
677
|
+
for (let t = new Date(today + 'T12:00:00Z').getTime() + 86400000; ; t += 86400000) {
|
|
678
|
+
const d = new Date(t).toISOString().slice(0, 10); if (d > end) break;
|
|
679
|
+
const w = new Date(t).getUTCDay(); if (w > 0 && w < 6) n++;
|
|
680
|
+
}
|
|
681
|
+
return (n === 0 ? 'last day' : plural(n, 'working day') + ' left') + ', ends ' + h.dayLabel(end);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const HEALTH = { ok: ['', 'On track'], warn: ['warn', 'Needs attention'], bad: ['bad', 'Off track'] };
|
|
685
|
+
|
|
686
|
+
function boardParts(ui, b) {
|
|
687
|
+
const { h } = ui;
|
|
688
|
+
const out = [];
|
|
689
|
+
if (b.boards && b.boards.length) out.push(ctxBox('boards', [c3ic('cols', 'sm'), 'Boards'], ul(b.boards.map((x) => {
|
|
690
|
+
const [tone, word] = HEALTH[x.health] || ['', 'Not read'];
|
|
691
|
+
return ci({ 'data-cal3-board': x.id }, el('span', { class: 'tag' + (tone ? ' cal3-tone-' + tone : '') }, word), el('span', { class: 'cal3-ct' }, x.name), el('span', { class: 'dim mono cal3-at' }, x.line),
|
|
692
|
+
el('a', { href: '#', class: 'cal3-ref', title: 'Open the board: ' + x.name, onClick: (ev) => { ev.preventDefault(); openBoardTab(ui, x.id, 'summary'); } }, c3ic('out', 'sm'), 'Open the board'));
|
|
693
|
+
}))));
|
|
694
|
+
if (b.sprint) {
|
|
695
|
+
const sp = b.sprint; const c = sp.counts || { todo: 0, doing: 0, done: 0 };
|
|
696
|
+
out.push(ctxBox('sprint', [c3ic('cols', 'sm'), sp.name, el('span', { class: 'dim mono cal3-h4r' }, sprintLeft(h, sp.end, ui.d.today || ''))],
|
|
697
|
+
el('div', { class: 'cal3-bar', role: 'img', 'aria-label': c.done + ' done, ' + c.doing + ' in progress, ' + c.todo + ' to do' },
|
|
698
|
+
setVar(el('i', { class: 'd' }), 'n', c.done), setVar(el('i', { class: 'p' }), 'n', c.doing), setVar(el('i', { class: 't' }), 'n', c.todo)),
|
|
699
|
+
el('div', { class: 'chips2 dim mono cal3-counts' }, el('span', null, c.done + ' done'), el('span', null, c.doing + ' in progress'), el('span', null, c.todo + ' to do'),
|
|
700
|
+
b.next ? el('span', { 'data-cal3': 'next-sprint' }, '· ' + b.next.name + ': ' + b.next.planned + ' planned, ' + b.next.carry + ' may carry over') : null)));
|
|
701
|
+
}
|
|
702
|
+
return out;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/* a board's home on Initiatives: the page reads ?init= when it opens (c3-initiatives.js parseLoc) */
|
|
706
|
+
function openBoardTab(ui, board, tab) {
|
|
707
|
+
try { const u = new URL(location.href); u.searchParams.set('init', board ? 'board:' + encodeURIComponent(board) + ':' + tab : ''); if (!board) u.searchParams.delete('init'); history.replaceState(history.state, '', u); } catch { /* no history */ }
|
|
708
|
+
ui.h.gotoTab('init');
|
|
709
|
+
}
|
|
710
|
+
|
|
614
711
|
function upcomingBody(ui, rec, items) {
|
|
615
712
|
const { h } = ui;
|
|
616
713
|
const b = rec && rec.brief ? rec.brief : null;
|
|
714
|
+
const lv = rec && rec.level ? rec.level : null;
|
|
617
715
|
const prep = items.filter((x) => x.kind === 'prepared' || x.kind === 'todo');
|
|
618
716
|
const out = [];
|
|
619
717
|
if (b && b.gist && b.gist.length) out.push(el('div', { class: 'cal3-ctx cal3-gist', 'data-cal3-ctx': 'gist' }, el('h4', null, c3ic('spark', 'sm'), 'In short'), el('p', { class: 'cal3-p' }, b.gist.join(' '))));
|
|
620
|
-
if (b
|
|
718
|
+
if (b) out.push(...boardParts(ui, b));
|
|
719
|
+
if (b && b.matters && b.matters.length) out.push(ctxBox('matters', b.planning ? 'To settle in planning' : 'What matters today', ul(b.matters.map((x) => ci({ 'data-cal3-matter': x.tag },
|
|
621
720
|
el('span', { class: 'tag cal3-tone-' + x.tone }, x.tag), el('span', { class: 'cal3-ct' }, x.text), x.meta ? el('span', { class: 'dim mono cal3-at' }, x.meta) : null,
|
|
622
721
|
refLink(ui, x.ref, { initiative: x.ref.initiative, title: x.text }))))));
|
|
623
722
|
if (b && b.last) {
|
|
@@ -643,11 +742,11 @@ function upcomingBody(ui, rec, items) {
|
|
|
643
742
|
...(b.links || []).map((l) => el('a', { class: 'chip2', href: l.url, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), l.label)),
|
|
644
743
|
...(b.refs || []).map((r) => r.link ? el('a', { class: 'chip2', href: r.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), r.label, r.source ? el('span', { class: 'dim' }, ' ' + r.source) : null)
|
|
645
744
|
: el('span', { class: 'chip2' }, r.label, r.source ? el('span', { class: 'dim' }, ' ' + r.source) : null))));
|
|
646
|
-
const noIni = !
|
|
745
|
+
const noIni = !lv; const noLast = !(b && b.last);
|
|
647
746
|
if (noIni || noLast) {
|
|
648
|
-
const missing = noIni && noLast ? '
|
|
747
|
+
const missing = noIni && noLast ? 'This meeting is not linked and there is no earlier note from it.' : noIni ? 'This meeting is not linked to the Portfolio, a board or an initiative.' : 'There is no earlier note from this meeting.';
|
|
649
748
|
out.push(ctxBox('knows', 'What PMOS knows', fold('info', missing + (out.length ? '' : ' Nothing is recorded to prepare.'), { 'data-cal3': 'knows' }),
|
|
650
|
-
ul([noIni ? ci({}, el('span', { class: 'tag' }, 'Link'), el('span', { class: 'cal3-ct' }, '
|
|
749
|
+
ul([noIni ? ci({}, el('span', { class: 'tag' }, 'Link'), el('span', { class: 'cal3-ct' }, 'Pick Portfolio, Board or Initiative above, and what matters there shows here.')) : null,
|
|
651
750
|
ci({}, el('span', { class: 'tag' }, 'Note'), el('span', { class: 'cal3-ct' }, 'Take a note in this meeting, and the next one opens with its summary and next steps.'))].filter(Boolean))));
|
|
652
751
|
}
|
|
653
752
|
return out;
|
|
@@ -584,14 +584,15 @@ function handleMore(S, method, path, p) {
|
|
|
584
584
|
return okb({ board: { name: 'Demo board' }, releases: S.releases.filter((x) => x.board === p.board).map((x) => ({ id: x.id, name: x.name, type: 'both', initiative_ids: x.scope.slice(), deploy_at: x.date + 'T11:00:00', release_at: x.date + 'T12:00:00', accepted_at: x.status === 'Released' ? x.date + 'T12:30:00' : null, gapFlags: [], tags: { detected: [], reason: null }, summary: [], status: x.status, board: x.board })) });
|
|
585
585
|
}
|
|
586
586
|
if (path === '/api/docs/library') {
|
|
587
|
-
const files = [];
|
|
587
|
+
const files = []; const edges = []; // the research informs the PRD; the rubric comes from the PRD (the document sheet's Relations graph)
|
|
588
588
|
for (const r of feat(p.board)) {
|
|
589
|
-
const h = hash(r.id); const slug = r.id.toLowerCase();
|
|
589
|
+
const h = hash(r.id); const slug = r.id.toLowerCase(); const at0 = files.length;
|
|
590
590
|
if (r.spec) files.push({ path: 'planning/ib/' + slug + '-prd.md', title: r.title + ' PRD', category: 'spec', date: D(-(h % 20) - 1), initiative: r.id });
|
|
591
591
|
files.push({ path: 'planning/ib/' + slug + '-research.md', title: r.title + ' research', category: 'res', date: D(-(h % 50) - 5), initiative: r.id });
|
|
592
592
|
if (catOfRow(S, r) === 'Do') files.push({ path: 'planning/ib/' + slug + '-rubric.md', title: r.title + ' rubric', category: 'eval', date: D(-(h % 9) - 1), initiative: r.id });
|
|
593
|
+
if (r.spec) { edges.push([at0 + 1, at0, 0]); if (files.length === at0 + 3) edges.push([at0, at0 + 2, 1]); }
|
|
593
594
|
}
|
|
594
|
-
return okb({ key: 'demo', files, edges: [
|
|
595
|
+
return okb({ key: 'demo', files, edges, kinds: ['informs', 'rubric from'], categories: [{ key: 'spec', name: 'Specs' }, { key: 'eval', name: 'Evals' }, { key: 'res', name: 'Research & plans' }], initiatives: {} });
|
|
595
596
|
}
|
|
596
597
|
return undefined;
|
|
597
598
|
}
|
|
@@ -90,7 +90,23 @@ const CONTEXT = {
|
|
|
90
90
|
'm3@2026-10-06': { phase: 'past', held: false, items: [] },
|
|
91
91
|
'm1@2026-10-08': { phase: 'upcoming', held: true, items: [
|
|
92
92
|
{ kind: 'prepared', text: 'Async summary of the 3 open tickets', ref: init('search') },
|
|
93
|
-
]
|
|
93
|
+
],
|
|
94
|
+
// a meeting about a board (initiative c3-meeting-board): the title names no board, so the PM linked it
|
|
95
|
+
level: { kind: 'board', ws: D.WS_ID, id: 'idpf', name: 'IDPF', why: 'you linked it to the board', by: 'pm' },
|
|
96
|
+
brief: {
|
|
97
|
+
initiative: null,
|
|
98
|
+
gist: ['A meeting of the IDPF board.', 'IDPF Sprint 42 has 11 of 20 tickets still open, and 2 are blocked.', 'On the board: 1 initiative at risk.', '1 new ask waits in triage.'],
|
|
99
|
+
board: { ws: D.WS_ID, id: 'idpf', name: 'IDPF', health: 'warn', counts: { total: 3, open: 3, risk: 1, late: 0 }, next_release: null },
|
|
100
|
+
sprint: { name: 'IDPF Sprint 42', start: '2026-10-05', end: '2026-10-16', counts: { todo: 5, doing: 6, done: 9 }, total: 20, open: 11, blocked: 2, read_at: '2026-10-08T08:10:00' },
|
|
101
|
+
matters: [
|
|
102
|
+
{ tone: 'bad', tag: 'Blocked', text: 'IDPF-1190 QA test plan for the beta: QA capacity', meta: 'In Progress · flagged in Jira', ref: { type: 'jira', id: 'IDPF-1190', board: 'idpf', ws: D.WS_ID } },
|
|
103
|
+
{ tone: 'bad', tag: 'Blocked', text: 'IDPF-1204 Partner sandbox keys', meta: 'Blocked', ref: { type: 'jira', id: 'IDPF-1204', board: 'idpf', ws: D.WS_ID } },
|
|
104
|
+
{ tone: 'warn', tag: 'At risk', text: 'Self-serve onboarding v2', meta: 'initiative on this board · due 2026-10-26', ref: init('IDPF-12') },
|
|
105
|
+
{ tone: 'warn', tag: 'New ask', text: 'SSO for enterprise trials, from Lina Kusuma (Sales)', meta: 'waiting in triage', ref: { type: 'ask', id: 'ask-demo1', board: 'idpf', ws: D.WS_ID } },
|
|
106
|
+
],
|
|
107
|
+
last: null, earlier: 0, purpose: null, prep: null, links: [], refs: [], planning: false,
|
|
108
|
+
sources: ['the calendar', 'the IDPF board', 'its IDPF Sprint 42 tickets from Jira'],
|
|
109
|
+
} },
|
|
94
110
|
'm2@2026-10-08': { phase: 'upcoming', held: true, items: [
|
|
95
111
|
{ kind: 'prepared', text: 'Agenda: scope cut, blockers, next steps', ref: init('checkout') },
|
|
96
112
|
{ kind: 'prepared', text: 'Pre-read: the Checkout v2 PRD with 2 open comments', ref: { type: 'plan-item', id: 'checkout-prd' } },
|
|
@@ -104,6 +120,7 @@ const CONTEXT = {
|
|
|
104
120
|
{ kind: 'prepared', text: 'Three scope options with their dates', ref: init('checkout') },
|
|
105
121
|
{ kind: 'todo', text: 'Cut Checkout v2 to 3 core flows to hold 24 Oct?', ref: item('item-demo010') },
|
|
106
122
|
],
|
|
123
|
+
level: { kind: 'initiative', id: 'checkout', name: 'Checkout v2', why: "today's brief names the initiative", by: 'brief' },
|
|
107
124
|
// the card before a meeting (initiative c3-meeting-card): what the server's briefOf puts together
|
|
108
125
|
brief: {
|
|
109
126
|
initiative: { id: 'checkout', title: 'Checkout v2', status: 'at risk' },
|
|
@@ -118,12 +135,35 @@ const CONTEXT = {
|
|
|
118
135
|
earlier: 3, purpose: null, prep: null, links: [], refs: [],
|
|
119
136
|
sources: ['the calendar', '3 earlier notes of this meeting', 'the checkout plan', 'your items'],
|
|
120
137
|
} },
|
|
121
|
-
|
|
138
|
+
// a meeting about the Portfolio: the brief names no initiative and the PM linked it
|
|
139
|
+
'm5@2026-10-08': { phase: 'upcoming', held: false, items: [],
|
|
140
|
+
level: { kind: 'portfolio', name: 'Portfolio', why: 'you linked it to the Portfolio', by: 'pm' },
|
|
141
|
+
brief: {
|
|
142
|
+
initiative: null,
|
|
143
|
+
gist: ['A review of 5 boards.', 'Across the boards: 1 initiative late and 2 initiatives at risk.', '1 release in the next 14 days.'],
|
|
144
|
+
boards: [
|
|
145
|
+
{ ws: D.WS_ID, id: 'idpf', name: 'IDPF', health: 'warn', line: '3 open initiatives · 1 at risk · 1 ask' },
|
|
146
|
+
{ ws: D.WS_ID, id: 'pay', name: 'Payments', health: 'ok', line: '2 open initiatives' },
|
|
147
|
+
{ ws: D.WS_ID, id: 'growth', name: 'Growth', health: 'ok', line: '2 open initiatives' },
|
|
148
|
+
{ ws: D.WS_ID, id: 'office', name: 'Office move', health: 'bad', line: '2 open initiatives · 1 late · 1 at risk' },
|
|
149
|
+
{ ws: D.WS_ID, id: 'hiring', name: 'Hiring Q4', health: 'ok', line: '1 open initiative' },
|
|
150
|
+
],
|
|
151
|
+
matters: [
|
|
152
|
+
{ tone: 'bad', tag: 'Late', text: 'Fit-out contractor selection', meta: 'Office move · due 2026-10-06', ref: init('OFF-01') },
|
|
153
|
+
{ tone: 'warn', tag: 'At risk', text: 'Self-serve onboarding v2', meta: 'IDPF · due 2026-10-26', ref: init('IDPF-12') },
|
|
154
|
+
{ tone: 'ok', tag: 'Release', text: 'Tokenisation 1.4', meta: 'Payments · 2026-10-15', ref: { type: 'release', id: 'rel-pay-14' } },
|
|
155
|
+
],
|
|
156
|
+
last: null, earlier: 0, purpose: null, prep: null, links: [], refs: [], planning: false,
|
|
157
|
+
sources: ['the calendar', 'every board on the Portfolio'],
|
|
158
|
+
} },
|
|
122
159
|
};
|
|
123
160
|
|
|
161
|
+
/* the boards a meeting can be linked to (routes-calendar-views.js link_boards), as c3-demo-boards.js names them */
|
|
162
|
+
const LINK_BOARDS = [['idpf', 'IDPF'], ['pay', 'Payments'], ['growth', 'Growth'], ['office', 'Office move'], ['hiring', 'Hiring Q4']].map(([id, name]) => ({ ws: D.WS_ID, id, name }));
|
|
163
|
+
|
|
124
164
|
/** The answer of GET /api/calendar-views with the Calendar page's keys added. */
|
|
125
165
|
export function withCalendarPage(out) {
|
|
126
166
|
if (!out || !out.range) return out;
|
|
127
167
|
const { start, end } = out.range;
|
|
128
|
-
return withCalendarTidy({ ...out, layers: layersIn(start, end), free: freeIn(start, end), context: CONTEXT });
|
|
168
|
+
return withCalendarTidy({ ...out, layers: layersIn(start, end), free: freeIn(start, end), context: CONTEXT, link_boards: LINK_BOARDS });
|
|
129
169
|
}
|
|
@@ -316,6 +316,22 @@ POST['/api/doc-sheet/reject'] = (b) => {
|
|
|
316
316
|
};
|
|
317
317
|
POST['/api/doc-sheet/why'] = (b) => { const R = revsOf(b.path); const r = R && R.find((x) => x.rev === Number(b.rev)); if (!r) return { ok: false, error: 'no such revision' }; if (!String(b.why || '').trim()) return { ok: false, error: 'a why is required' }; r.why_added = b.why.trim(); r.why_added_at = '2026-10-08T08:12:00.000Z'; return { ok: true, rev: r.rev }; };
|
|
318
318
|
POST['/api/doc-sheet/mark-current'] = (b) => { if (!String(b.why || '').trim()) return { ok: false, error: 'a why is required' }; DEMO_EVENTS.push({ path: b.path, kind: 'marked', at: '2026-10-08T08:12:00.000Z', why: b.why.trim(), by: 'pm' }); return { ok: true, at: '2026-10-08T08:12:00.000Z' }; };
|
|
319
|
+
/* the copies in other tools (GET /api/docs, lib/doc-sync.js signals; initiative c3-doc-sheet-v2): Checkout v2 PRD has a Confluence copy that is behind
|
|
320
|
+
* and a Google Doc that PMOS is ahead of; the drop-off analysis has a Confluence copy in step. Check marks them read now; sync and link are kept in memory. */
|
|
321
|
+
const pOf = (id) => DFILES.find((f) => f.id === id).path;
|
|
322
|
+
const COPIES = {
|
|
323
|
+
[pOf('p1')]: [{ kind: 'confluence', label: 'Confluence', target: 'acme.atlassian.net/wiki/spaces/PM/pages/48213/Checkout+v2+PRD', url: null, title: 'Checkout v2 PRD', signal: 'behind', basis: 'text', words: 'Confluence has a change PMOS does not have: Dana edited the Scope section on Thu 8 Oct.', checked_at: '2026-10-08T07:40:00.000Z', checked_by: 'agent', remote_modified_at: '2026-10-08T06:55:00.000Z', remote_modified_by: 'Dana Ruiz', linked_at: '2026-09-25T10:00:00.000Z', syncing: null, sync_failed: null },
|
|
324
|
+
{ kind: 'google', label: 'Google Docs', target: 'docs.google.com/document/d/1Xc2Checkout', url: null, title: 'Checkout v2 PRD (shared with Design)', signal: 'ahead', basis: 'text', words: 'PMOS has changes the Google Doc does not have: the done criteria and the audit risk.', checked_at: '2026-10-08T07:40:00.000Z', checked_by: 'agent', remote_modified_at: '2026-10-05T16:20:00.000Z', remote_modified_by: 'Wawan', linked_at: '2026-10-01T09:00:00.000Z', syncing: null, sync_failed: null }],
|
|
325
|
+
[pOf('u1')]: [{ kind: 'confluence', label: 'Confluence', target: 'acme.atlassian.net/wiki/spaces/PM/pages/48390', url: null, title: 'Address drop-off analysis', signal: 'same', basis: 'text', words: 'Both have the same text.', checked_at: '2026-10-08T07:40:00.000Z', checked_by: 'agent', remote_modified_at: '2026-10-06T11:00:00.000Z', remote_modified_by: 'Wawan', linked_at: '2026-10-06T11:05:00.000Z', syncing: null, sync_failed: null }] };
|
|
326
|
+
GET['/api/docs'] = () => ({ ok: true, docs: Object.entries(COPIES).map(([path, links]) => ({ path, title: (DFILES.find((f) => f.path === path) || {}).title || path, links: links.map((l) => ({ ...l })), attention: links.some((l) => l.signal !== 'same') })), summary: {}, open: null, session: null, connectors: {} });
|
|
327
|
+
POST['/api/docs/check'] = () => { for (const L of Object.values(COPIES)) for (const l of L) { l.checked_at = '2026-10-08T08:12:00.000Z'; l.checked_by = 'agent'; } return { ok: true, session: 'demo-check' }; };
|
|
328
|
+
POST['/api/docs/sync'] = (b) => { const l = (COPIES[b.path] || []).find((x) => x.kind === b.kind); if (!l) return { ok: false, error: b.path + ' has no such copy' }; if (!['pmos', 'copy'].includes(b.use)) return { ok: false, error: 'say which version to use: pmos or copy' }; l.syncing = { run: null, use: b.use, started_at: '2026-10-08T08:12:00.000Z' }; return { ok: true, session: 'demo-sync' }; };
|
|
329
|
+
POST['/api/docs/link'] = (b) => {
|
|
330
|
+
if (!['google', 'confluence'].includes(b.kind) || !String(b.target || '').trim()) return { ok: false, error: 'not a Google Docs or Confluence address' };
|
|
331
|
+
const L = COPIES[b.path] || (COPIES[b.path] = []); if (L.some((x) => x.kind === b.kind)) return { ok: false, error: 'that document already has a ' + b.kind + ' copy' };
|
|
332
|
+
L.push({ kind: b.kind, label: b.kind === 'google' ? 'Google Docs' : 'Confluence', target: b.target, url: null, title: '', signal: 'unchecked', basis: null, words: 'No check has read this copy yet.', checked_at: null, checked_by: null, remote_modified_at: null, remote_modified_by: null, linked_at: '2026-10-08T08:12:00.000Z', syncing: null, sync_failed: null });
|
|
333
|
+
return { ok: true };
|
|
334
|
+
};
|
|
319
335
|
POST['/api/doc-sheet/change-note'] = (b) => ({ ok: true, note: 'A change note for the owners of ' + (b.downstream || []).join(', ') });
|
|
320
336
|
|
|
321
337
|
/* health: the six checks (the mockup's docHealth, line 4059). Checkout v2 PRD is recomputed from its text; the others answer the fixed N of M. */
|
|
@@ -76,6 +76,7 @@ function postRoute(path, body) {
|
|
|
76
76
|
try { b = body ? JSON.parse(body) : {}; } catch { b = {}; }
|
|
77
77
|
if (path === '/api/pm-items/add') return { ok: true, item: { id: 'item-demo-new' + (++added), title: String(b.line || '').trim() || 'New item', due: null, priority: null, waiting_on: null, job: null, steps: [], created_at: D.NOW_ISO + 'Z' } };
|
|
78
78
|
if (path === '/api/pm-items/remove') return { ok: true, undo_ms: 8000 };
|
|
79
|
+
if (path === '/api/calendar-links') return (window.__c3links = (window.__c3links || []).concat([b])), { ok: true, link: b.kind === 'none' ? null : { kind: b.kind, ws: b.ws || null, board: b.board || null, initiative: b.initiative || null, title: b.title || null, by: 'pm:wawan' } }; // nothing is saved
|
|
79
80
|
/* the Work page's routes (initiative console3-work-page): each answers as the real route does; pm-work-view.js lays the press over the fixtures */
|
|
80
81
|
if (path === '/api/pm-items/notyet') {
|
|
81
82
|
const when = String(b.when || ''); const n = when === 'tomorrow' ? 1 : when === 'next-week' ? 7 : 0;
|