create-pmos 0.252.0 → 0.254.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.
Files changed (54) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/scripts/workflow-state.js +6 -1
  6. package/payload/state/discoveries.md +1 -1
  7. package/payload/state/log.md +1 -1
  8. package/workspace/briefing-premium-board-test.js +275 -0
  9. package/workspace/briefing-premium-initiative-test.js +304 -0
  10. package/workspace/briefing-premium-products-test.js +164 -0
  11. package/workspace/briefing-premium-test.js +438 -0
  12. package/workspace/briefing-premium-work-test.js +281 -0
  13. package/workspace/fixtures/board-premium-facts.js +38 -0
  14. package/workspace/fixtures/briefing-premium-facts.js +32 -0
  15. package/workspace/fixtures/initiative-premium-facts.js +42 -0
  16. package/workspace/fixtures/my-work-premium-facts.js +31 -0
  17. package/workspace/guide/briefing-premium.md +151 -0
  18. package/workspace/guide/index.md +1 -0
  19. package/workspace/lib/board-premium.js +133 -0
  20. package/workspace/lib/briefing-premium-agent.js +134 -0
  21. package/workspace/lib/briefing-premium-products.js +121 -0
  22. package/workspace/lib/briefing-premium-writes.js +138 -0
  23. package/workspace/lib/briefing-premium.js +581 -0
  24. package/workspace/lib/engine/workflow-state.js +6 -1
  25. package/workspace/lib/headless.js +1 -1
  26. package/workspace/lib/initiative-premium.js +147 -0
  27. package/workspace/lib/my-work-premium.js +151 -0
  28. package/workspace/lib/route-modules.js +4 -0
  29. package/workspace/lib/routes-board-premium.js +61 -0
  30. package/workspace/lib/routes-briefing-premium.js +115 -0
  31. package/workspace/lib/routes-initiative-premium.js +68 -0
  32. package/workspace/lib/routes-my-work-premium.js +31 -0
  33. package/workspace/lib/settings-store.js +3 -0
  34. package/workspace/lib/vendor/scripts/workflow-state.js +6 -1
  35. package/workspace/package-lock.json +2 -2
  36. package/workspace/package.json +1 -1
  37. package/workspace/public/pm/premium-board.js +180 -0
  38. package/workspace/public/pm/premium-initiative.js +336 -0
  39. package/workspace/public/pm/premium-shell.js +184 -0
  40. package/workspace/public/pm/premium-work.js +198 -0
  41. package/workspace/public/pm/premium.css +1212 -0
  42. package/workspace/public/pm/premium.js +272 -0
  43. package/workspace/public/premium-board.html +73 -0
  44. package/workspace/public/premium-initiative.html +46 -0
  45. package/workspace/public/premium-work.html +84 -0
  46. package/workspace/public/premium.html +69 -0
  47. package/workspace/public/vendor/geist-mono-var.woff2 +0 -0
  48. package/workspace/public/vendor/geist-var.woff2 +0 -0
  49. package/workspace/server.js +5 -3
  50. package/workspace/test-files.json +14 -0
  51. package/workspace/test-lists/briefing-premium.json +19 -0
  52. package/workspace/third-party/README.md +12 -0
  53. package/workspace/third-party/geist-OFL.LICENSE +93 -0
  54. package/workspace/third-party/geist-mono-OFL.LICENSE +93 -0
@@ -0,0 +1,336 @@
1
+ /* pm/premium-initiative.js — the premium initiative page (initiative briefing-premium-initiative, slice 6). Reads GET /api/initiative-premium?id= and draws the
2
+ * model lib/initiative-premium.js builds from the routes the production Initiatives sheet reads: the overview and checks, the Tasks card (PRD requirements and
3
+ * the board's tracker items) and the Core documents card. A document row with a file opens the read-only document sheet (Read, Edit log, Health) in the
4
+ * standard dialog beside the read-only agent. The one press is Draft it, through POST /api/core-docs/draft. Nothing here edits a document, issues one,
5
+ * moves an initiative or adds a task. */
6
+ import { pmPost } from './http.js';
7
+ import { $, $$, esc, openDlg, closeDlg, setMode, toast, loadThread, onScroll, wireRail, logHtml, turnA, ptx, pillOf, kv, H2, CHECK, crumbHtml, wireCrumb, AI } from './premium-shell.js';
8
+
9
+ let M = null; // the model
10
+ let taskView = 'list';
11
+ const DAY = 86400000;
12
+ let docFilter = 'all', docQuery = '';
13
+ let readTimer = null;
14
+ wireRail('init');
15
+ const ICO = (p) => `<svg class="i" viewBox="0 0 24 24">${p}</svg>`;
16
+ const DOCI = '<path d="M6 3h9l4 4v14H6z"/><path d="M14 3v5h5M9 13h6M9 17h4"/>';
17
+ const SPARK = '<path d="M12 3l1.8 4.7L18.5 9.5l-4.7 1.8L12 16l-1.8-4.7L5.5 9.5l4.7-1.8z"/>';
18
+ const STATE_ICO = { Live: '<path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/>', 'Up to date': '<path d="M5 12.5l4.5 4.5L19 7.5"/>', Draft: '<path d="M4 20l4-1 11-11-3-3L5 16z"/>', 'Not started': '<circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16.5v.5"/>', Stale: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>', Due: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>' };
19
+ const TKI = { jira: '<rect x="4" y="5" width="16" height="14" rx="3"/><path d="m8.5 12 2.5 2.5 4.5-5"/>', task: '<path d="M6 4v16M6 5h11l-2.5 4L17 13H6"/>', req: '<path d="M6 3h9l4 4v14H6z"/><path d="M9 12h6M9 16h4"/>' };
20
+ const TONE = { Done: 'ok', 'In progress': 'wip', 'In review': 'wip', 'To do': 'mut' };
21
+
22
+ const params = new URLSearchParams(location.search);
23
+ const ID = params.get('id') || '';
24
+ const query = () => ['id=' + encodeURIComponent(ID), params.get('ws') ? 'ws=' + encodeURIComponent(params.get('ws')) : '', params.get('board') ? 'board=' + encodeURIComponent(params.get('board')) : ''].filter(Boolean).join('&');
25
+ const when = (iso) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || '')); if (!m) return ''; return +m[3] + ' ' + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][+m[2] - 1]; };
26
+
27
+ /* ── overview, header, checks ────────────────────────────────────────────────────────────────────────────── */
28
+ function headHtml() {
29
+ const h = M.head;
30
+ const strip = h.strip.names.length ? `<div class="wst iws"><span class="sg2" aria-hidden="true">${h.strip.seg.map((c) => `<i class="${c}"></i>`).join('')}</span><em>${h.strip.names.map((n, j) => (j === h.strip.cur ? `<b>${esc(n)}</b>` : esc(n))).join(' › ')}</em></div>` : '';
31
+ return `<div class="ihead">${h.statusLabel ? `<span class="chp ${h.tone}"><i></i>${esc(h.statusLabel)}</span>` : ''}<span class="ihm">${esc(h.meta.join(' · '))}${h.stageNote ? ` · stage ${esc(h.stageNote)}` : ''}</span></div>${strip}`;
32
+ }
33
+ function overviewHtml() {
34
+ const o = M.overview;
35
+ let todo;
36
+ if (o.missing) todo = `<p class="empty3 plain">${esc(o.why)}</p>`;
37
+ else if (o.none) todo = `<div class="todo"><span class="tdw">Nothing is waiting</span><p>${esc(o.why)}</p></div>`;
38
+ else todo = `<div class="todo ${o.tone || ''}"><span class="tdw">${esc(o.who)}</span><b>${esc(o.h)}</b>${o.p ? `<p>${esc(o.p)}</p>` : ''}${o.more ? `<small>${esc(o.more)}</small>` : ''}${o.skip ? `<small class="tdk">If you wait: ${esc(o.skip)}</small>` : ''}</div>`;
39
+ const help = M.help.length ? `<div class="agr2"><span class="label">PMOS can help</span><div>${M.help.map((h, n) => `<button class="agc" data-help="${n}">${AI}${esc(h.label)}</button>`).join('')}</div><small>You press, PMOS proposes, you decide. Nothing changes until you accept.</small></div>` : '';
40
+ const notes = M.health.concat(M.warnings).map((t) => `<p class="note" style="margin:10px 0 0">${esc(t)}</p>`).join('');
41
+ return `<section class="card rise" style="--d:3" id="cIniOv" aria-labelledby="hOv"><div class="ch"><span class="ic">${ICO('<path d="M4 6h16M4 12h16M4 18h10"/>')}</span><h2 id="hOv">Overview</h2><small>from the repository</small></div>${M.desc ? `<p class="lede" style="margin:0 0 14px">${esc(M.desc)}</p>` : ''}${todo}${help}${notes}</section>`;
42
+ }
43
+ function checksHtml() {
44
+ if (!M.checks.length) return '';
45
+ const all = M.allSteps.length ? `<details class="src"><summary>All ${M.allSteps.length} workflow steps</summary><div class="ckl">${M.allSteps.map((c) => `<div class="ckr"><i></i><b>${esc(c.label)}</b><span>${esc(c.stateLabel)}</span><small>${esc(c.note)}</small></div>`).join('')}</div></details>` : '';
46
+ return `<section class="card rise" style="--d:4" id="cIniChk" aria-labelledby="hChk"><div class="ch"><span class="ic">${ICO('<path d="M5 12l5 5L20 7"/>')}</span><h2 id="hChk">Checks</h2><small>from the workflow engine</small></div><div class="ckl">${M.checks.map((c) => `<div class="ckr ${esc(c.state)}${c.cur ? ' cur' : ''}"><i></i><b>${esc(c.label)}</b><span>${esc(c.stateLabel)}</span>${c.owed ? `<small>${esc(c.owed)}</small>` : ''}</div>`).join('')}</div>${all}</section>`;
47
+ }
48
+ $('#iMain').addEventListener('click', (e) => {
49
+ const h = e.target.closest('[data-help]'); if (!h) return;
50
+ const a = M.help[+h.dataset.help]; if (!a) return;
51
+ if (a.kind === 'draft') { const d = M.docs.rows.find((x) => x.key === a.key); if (d && !h.disabled) { h.disabled = true; draftKey(a.key); } return; }
52
+ openAsk(a.q);
53
+ });
54
+
55
+ /* ── tasks ──────────────────────────────────────────────────────────────────────────────────────────────── */
56
+ const taskState = (t) => (t.state ? t.state : 'No result');
57
+ const taskKind = (t) => (t.src === 'PRD requirement' ? 'req' : t.src.startsWith('Jira') ? 'jira' : 'task');
58
+ const dayOf = (iso) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; };
59
+ const today = () => { const d = new Date(M.asOf || Date.now()); return new Date(d.getFullYear(), d.getMonth(), d.getDate()); };
60
+ const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
61
+ const over = (t) => t.end && t.state && t.state !== 'Done' && dayOf(t.end) < today();
62
+ const dateText = (t) => (t.state === 'Done' ? '' : t.end ? (over(t) ? 'Overdue · ' : 'Due ') + when(t.end) : t.state ? 'No date' : '');
63
+ function taskRow(t) {
64
+ return `<button class="tkq${over(t) ? ' ov' : ''}" data-task="${esc(t.key)}" aria-haspopup="dialog"><span class="tki ${taskKind(t)}">${ICO(TKI[taskKind(t)])}</span><span class="tkn"><b>${esc(t.title)}</b><small>${esc([t.ref, t.src, t.owner].filter(Boolean).join(' · '))}</small></span><span class="tkw">${esc(dateText(t))}</span><small class="tkp ${TONE[t.state] || 'mut'}">${esc(taskState(t))}</small></button>`;
65
+ }
66
+ function tasksTimeline() {
67
+ const T = M.tasks.rows, D = T.filter((t) => t.start && t.end).sort((a, b) => dayOf(a.start) - dayOf(b.start)), U = T.filter((t) => !(t.start && t.end));
68
+ const und = U.length ? `<div class="tkund"><span class="label">No dates set <em>${U.length}</em></span><div>${U.map((t) => `<button class="tkch" data-task="${esc(t.key)}"><b>${esc(t.title)}</b><small class="tkp ${TONE[t.state] || 'mut'}">${esc(taskState(t))}</small></button>`).join('')}</div></div>` : '';
69
+ if (!D.length) return `<p class="empty3" style="color:var(--ink-2);margin:6px 0 10px">${U.length ? 'No task has a start and a due date, so there is nothing to draw.' : 'No tasks yet.'} The repository records no dates for PRD requirements.</p>${und}`;
70
+ const T0 = +today(), lo = Math.min(...D.map((t) => +dayOf(t.start)), T0 - 5 * DAY), hi = Math.max(...D.map((t) => +dayOf(t.end)), T0 + 14 * DAY);
71
+ const a = new Date(lo), from = new Date(a.getFullYear(), a.getMonth(), 1), z = new Date(hi), to = new Date(z.getFullYear(), z.getMonth() + 1, 0), span = to - from, X = (d) => ((d - from) / span * 100);
72
+ const ticks = []; for (let d = new Date(from); d <= to; d = new Date(d.getFullYear(), d.getMonth() + 1, 1)) ticks.push(d);
73
+ const tx = X(T0);
74
+ const head = `<div class="tlh k" aria-hidden="true"><span></span><div class="tlax">${ticks.map((d) => `<i style="left:${X(d).toFixed(2)}%">${MON[d.getMonth()]}${d.getMonth() === 0 ? ' ' + d.getFullYear() : ''}</i>`).join('')}<b style="left:${tx.toFixed(2)}%">Today</b></div><span></span></div>`;
75
+ const grid = `<div class="tlgrid k" aria-hidden="true"><span></span><div class="tlplot">${ticks.map((d) => `<u style="left:${X(d).toFixed(2)}%"></u>`).join('')}<u class="td" style="left:${tx.toFixed(2)}%"></u></div><span></span></div>`;
76
+ return head + `<div class="tll">${grid}${D.map((t) => {
77
+ const l = X(dayOf(t.start)), w = Math.max(0.9, X(dayOf(t.end)) - X(dayOf(t.start))), el = t.state === 'Done' ? 100 : Math.max(0, Math.min(100, (T0 - dayOf(t.start)) / Math.max(1, dayOf(t.end) - dayOf(t.start)) * 100));
78
+ const tip = `${t.title} · ${when(t.start)} to ${when(t.end)} · ${taskState(t).toLowerCase()}`;
79
+ return `<button class="tkt${over(t) ? ' ov' : ''}${t.state === 'Done' ? ' dn' : ''}" data-task="${esc(t.key)}"><span class="tlt"><b>${esc(t.title)}</b><small>${esc([t.ref, t.src, t.owner].filter(Boolean).join(' · '))}</small></span><span class="tkbar" role="img" aria-label="${esc(tip)}"><span class="tkb" data-tip="${esc(tip)}" style="--l:${l.toFixed(2)}%;--w:${w.toFixed(2)}%"><i style="width:${el.toFixed(1)}%"></i></span></span><span class="tkr2"><small class="tkp ${TONE[t.state] || 'mut'}">${esc(taskState(t))}</small><small>${esc(dateText(t))}</small></span></button>`;
80
+ }).join('')}</div><div class="wleg gleg"><span><i class="lgs"></i>Time passed</span><span><i class="lgr"></i>Time to the due date</span><span><i class="lgo"></i>Overdue</span></div>` + und;
81
+ }
82
+ function tasksBody() {
83
+ const T = M.tasks;
84
+ if (!T.rows.length) return `<p class="empty3 plain">${esc(T.note)}</p>`;
85
+ if (taskView === 'stage') {
86
+ const cols = ['To do', 'In progress', 'On hold', 'Done'].map((s) => [s, T.rows.filter((t) => (t.state === 'In review' ? 'In progress' : t.state) === s)]);
87
+ const none = T.rows.filter((t) => !t.state);
88
+ const all = cols.concat(none.length ? [['No result recorded', none]] : []);
89
+ return `<div class="istg tkkb" style="grid-template-columns:repeat(${all.length},minmax(0,1fr))">${all.map(([s, L]) => `<div class="isc"><p class="label">${esc(s)} <em>${L.length}</em></p>${L.map((t) => `<button class="icd tkc${over(t) ? ' ov' : ''}" data-task="${esc(t.key)}" aria-haspopup="dialog"><b>${esc(t.title)}</b><small>${esc([t.ref, t.src].filter(Boolean).join(' · '))}</small>${t.state === 'In review' || dateText(t) ? `<span class="tkcm">${t.state === 'In review' ? '<small class="tkp wip">In review</small>' : '<span></span>'}<small>${esc(dateText(t))}</small></span>` : ''}</button>`).join('') || '<p class="mut">None</p>'}</div>`).join('')}</div>`;
90
+ }
91
+ if (taskView === 'time') return tasksTimeline();
92
+ return `<div class="tkl" role="list">${T.rows.map(taskRow).join('')}</div>`;
93
+ }
94
+ function tasksHtml() {
95
+ const T = M.tasks;
96
+ let bar = '';
97
+ if (T.bar) bar = `<div class="stb" style="margin:0 0 12px"><span class="sbar2"><i style="flex:${T.bar.done || 0.0001}"></i><i class="g" style="flex:${T.bar.wip || 0.0001}"></i><i class="t" style="flex:${T.bar.todo || 0.0001}"></i></span><small>${T.bar.done} done · ${T.bar.wip} in progress · ${T.bar.todo} to do</small></div>`;
98
+ else if (T.progress) bar = `<div class="stb" style="margin:0 0 12px"><span class="sbar2"><i style="flex:${T.progress.done || 0.0001}"></i><i class="t" style="flex:${Math.max(0.0001, T.progress.total - T.progress.done)}"></i></span><small>${esc(T.progress.label || T.progress.done + ' of ' + T.progress.total)}</small></div>`;
99
+ const seg = `<div class="seg" id="tkTabs" role="group" aria-label="Task view"><i class="pill"></i><button data-v="list" aria-pressed="${taskView === 'list'}">List</button><button data-v="stage" aria-pressed="${taskView === 'stage'}">By stage</button><button data-v="time" aria-pressed="${taskView === 'time'}">Timeline</button></div>`;
100
+ return `<section class="card rise ifull" style="--d:5" id="cIniTk" aria-labelledby="hTk"><div class="ch"><span class="ic">${ICO('<rect x="4" y="4" width="16" height="16" rx="3"/><path d="m8.5 12 2.5 2.5 4.5-5"/>')}</span><h2 id="hTk">Tasks</h2><small>${esc(T.cap)}</small><button class="fold alt tkadd" data-tkadd="1">Add a task</button></div>${bar}${seg}<div id="tkBody">${tasksBody()}</div>${T.note && T.rows.length ? `<p class="note" style="margin-top:12px">${esc(T.note)}</p>` : ''}</section>`;
101
+ }
102
+ $('#iMain').addEventListener('click', (e) => {
103
+ const v = e.target.closest('#tkTabs [data-v]'); if (v) { taskView = v.dataset.v; $('#tkBody').innerHTML = tasksBody(); $$('#tkTabs button').forEach((b) => b.setAttribute('aria-pressed', b.dataset.v === taskView)); pillOf($('#tkTabs')); return; }
104
+ if (e.target.closest('[data-tkadd]')) { openAddTask(); return; }
105
+ const t = e.target.closest('[data-task]'); if (t) { openTask(M.tasks.rows.find((x) => x.key === t.dataset.task)); return; }
106
+ const f = e.target.closest('[data-dmf]'); if (f) { docFilter = f.dataset.dmf; drawDocList(); return; }
107
+ const d = e.target.closest('[data-draft]'); if (d && !d.disabled) { draft(d); return; }
108
+ const all = e.target.closest('[data-draftall]'); if (all && !all.disabled) { draftAll(all); return; }
109
+ const o = e.target.closest('[data-doc]'); if (o) { openSheet(M.docs.rows.find((x) => x.key === o.dataset.doc)); }
110
+ });
111
+ const STATES = ['To do', 'In progress', 'In review', 'Done'];
112
+ function openTask(t) {
113
+ if (!t) return;
114
+ const rows = [['Type', t.kind || (taskKind(t) === 'req' ? 'PRD requirement' : 'Task')], ['Reference', t.ref], ['Source', t.src], ['Owner', t.owner], ['Starts', t.start ? when(t.start) : ''], ['Due', t.end ? when(t.end) : ''], ['Status', t.state || 'No result recorded']];
115
+ const say = t.title + (t.state ? ' is ' + t.state.toLowerCase() + '.' : ' has no result recorded. The repository records none for a PRD requirement.');
116
+ const act = t.pmos ? `<div class="tkact"><span class="label">Status</span><div class="seg tkst" role="group" aria-label="Status"><i class="pill"></i>${STATES.map((s) => `<button data-tks="${esc(s)}" aria-pressed="${t.state === s}">${s}</button>`).join('')}</div></div><p class="note" style="margin-top:12px">A PMOS task is kept on the board. Jira tickets are read-only here: Jira owns their status and dates.</p>` : `<p class="xn">${t.src === 'PRD requirement' ? 'Read from the initiative’s PRD. Nothing on this page changes it.' : 'Read from the board. Jira owns its status and dates.'}</p>`;
117
+ openDlg({ eye: 'Task · real data', title: t.title, sub: M.title, say, stream: turnA(ptx(say)), noteKey: null, pane: `<article class="paper doc">${kv(rows)}${act}</article>`, label: 'Task' });
118
+ $('#askF').hidden = true; if ($('#oMain .tkst')) pillOf($('#oMain .tkst'));
119
+ $$('#oMain [data-tks]').forEach((b) => { b.onclick = async () => {
120
+ if (b.getAttribute('aria-pressed') === 'true') return;
121
+ const res = await pmPost('/api/boards/tasks/state', { ws: M.board.ws, board: M.board.id, task: t.uid, state: b.dataset.tks });
122
+ if (!res || res.ok !== true) { toast((res && res.error) || 'The status could not be changed.'); return; }
123
+ toast('Status changed to ' + b.dataset.tks.toLowerCase() + '.'); closeDlg(); await load();
124
+ }; });
125
+ }
126
+ function openAddTask() {
127
+ const prefs = ['Research plan', 'Release plan', 'UAT', 'Rollback plan', 'Support briefing', 'Legal review'];
128
+ const form = `<div class="addf"><p class="note" style="margin:0">Use a task for work that is not a Jira ticket, such as a research plan, a release plan or UAT. It is kept on the board and it is not a Jira ticket.</p>
129
+ <div><span class="label">Common tasks</span><div class="chq">${prefs.map((q, n) => `<button type="button" data-pre="${n}">${esc(q)}</button>`).join('')}</div></div>
130
+ <label>Title<input id="atT" maxlength="200" placeholder="For example: UAT with five partners"></label>
131
+ <div class="two"><label>Owner<input id="atO" maxlength="80" placeholder="Leave empty to use your name"></label><span></span></div>
132
+ <div class="two"><label><span>Start <em>optional</em></span><input type="date" id="atF"></label><label><span>Due <em>optional</em></span><input type="date" id="atD"></label></div>
133
+ <p class="note" style="margin:0">With no dates the board plans the task from tomorrow, for a week.</p>
134
+ <p class="note" id="atM" role="status" style="margin:0;min-height:1.2em"></p><div class="cact"><button class="cta" id="atGo">Add task</button><button class="cta alt" id="atNo">Cancel</button></div></div>`;
135
+ openDlg({ eye: 'New task · real data', title: 'Add a task', sub: M.title, say: 'Say what the task is. It is written to the board’s record.', stream: turnA(ptx('Say what the task is and when it is due. It is written to the board’s record, and I change nothing else.')), noteKey: null, pane: `<article class="paper doc">${form}</article>`, label: 'Task' });
136
+ $('#askF').hidden = true;
137
+ $('#oMain').onclick = async (e) => {
138
+ const pre = e.target.closest('[data-pre]'); if (pre) { $('#atT').value = prefs[+pre.dataset.pre]; $('#atT').focus(); return; }
139
+ if (e.target.closest('#atNo')) { closeDlg(); return; }
140
+ if (!e.target.closest('#atGo')) return;
141
+ const title = $('#atT').value.trim(), m = $('#atM'), f = $('#atF').value, z = $('#atD').value;
142
+ if (!title) { m.textContent = 'Give the task a title.'; $('#atT').focus(); return; }
143
+ if (f && z && z < f) { m.textContent = 'The due date is before the start date.'; return; }
144
+ if (f && !z) { m.textContent = 'Add a due date too, or clear the start date.'; return; }
145
+ e.target.closest('#atGo').disabled = true;
146
+ const res = await pmPost('/api/boards/tasks/add', { ws: M.board.ws, board: M.board.id, initiative: M.id, title, owner: $('#atO').value.trim() || undefined, start: f || undefined, due: z || undefined });
147
+ if (!res || res.ok !== true) { $('#atGo').disabled = false; m.textContent = (res && res.error) || 'The task could not be added.'; return; }
148
+ toast('Task added to the board.'); closeDlg(); await load();
149
+ };
150
+ }
151
+
152
+ /* ── documents ──────────────────────────────────────────────────────────────────────────────────────────── */
153
+ function docRow(d) {
154
+ const act = d.act && d.act.kind === 'draft' ? `<button class="dmcta alt" data-draft="${esc(d.key)}"${d.running ? ' disabled' : ''}>${d.running ? '<span class="spin"></span>' : ICO(SPARK)}${esc(d.act.label)}</button>` : d.path ? `<button class="dmcta alt" data-doc="${esc(d.key)}">Read</button>` : '';
155
+ const inner = `<span class="dmi c-${esc(d.cat || 'Board')}">${ICO(DOCI)}</span><span class="dmn"><b>${esc(d.title)}</b><small>${esc(d.what)}</small><span class="dmm"><span class="s ${esc(d.st || 'live')}">${ICO(STATE_ICO[d.state] || STATE_ICO['Not started'])}${esc(d.state)}</span>${d.when ? `<span>${ICO('<rect x="3" y="5" width="18" height="16" rx="3"/><path d="M3 10h18M8 3v4M16 3v4"/>')}updated ${esc(d.when)}</span>` : ''}${d.by ? `<span>${ICO('<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>')}${esc(d.by)}</span>` : ''}</span></span>`;
156
+ return `<div class="dmr ${esc(d.st || '')}" role="listitem">${d.path ? `<button class="dmo" data-doc="${esc(d.key)}" aria-haspopup="dialog">${inner}</button>` : `<span class="dmo">${inner}</span>`}<span class="dmq">${act}</span></div>`;
157
+ }
158
+ const matches = (d) => (docFilter === 'all' || (docFilter === 'none' ? d.st === 'missing' : docFilter === 'draft' ? d.st === 'draft' : (d.st === 'ok' || d.st === 'live'))) && (!docQuery.trim() || (d.title + ' ' + d.what).toLowerCase().includes(docQuery.trim().toLowerCase()));
159
+ function drawDocList() {
160
+ const D = M.docs, L = D.rows.filter(matches), core = L.filter((d) => !d.tech), tech = L.filter((d) => d.tech);
161
+ const none = !L.length ? `<div class="dmem"><p>No document matches${docQuery.trim() ? ' “' + esc(docQuery.trim()) + '”' : ''}.</p></div>` : '';
162
+ $('#dmList').innerHTML = none || (core.length ? `<div class="dmg two" role="list">${core.map(docRow).join('')}</div>` : '')
163
+ + (tech.length ? `<details class="src dmtech"${docQuery.trim() || docFilter !== 'all' ? ' open' : ''}><summary>Technical documents (${D.techCount})<small>for the engineering side. Not counted above.</small></summary><div class="dmg two" role="list">${tech.map(docRow).join('')}</div></details>` : '');
164
+ $$('#dmF button').forEach((b) => b.setAttribute('aria-pressed', b.dataset.dmf === docFilter)); pillOf($('#dmF'));
165
+ }
166
+ function docsHtml() {
167
+ const D = M.docs, c = D.counts;
168
+ if (!D.rows.length) return `<section class="card rise ifull" style="--d:6" id="cIniDoc" aria-labelledby="hDoc"><div class="ch"><span class="ic">${ICO(DOCI)}</span><h2 id="hDoc">Core documents</h2></div><p class="empty3 plain">${D.missing ? 'The core documents could not be read, so none is shown.' : 'No core documents are on record for this initiative.'}</p></section>`;
169
+ const dot = { ok: 'ok', draft: 'warn', missing: 'mut' };
170
+ const core = D.rows.filter((d) => !d.tech);
171
+ return `<section class="card rise ifull" style="--d:6" id="cIniDoc" aria-labelledby="hDoc"><div class="ch"><span class="ic">${ICO(DOCI)}</span><h2 id="hDoc">Core documents</h2><small>${c.all}</small></div>
172
+ <p class="dsum"><span><b>${c.draft}</b> ${c.draft === 1 ? 'draft' : 'drafts'} to review</span><span><b>${c.none}</b> not started</span><span><b>${c.ok}</b> up to date</span>${draftable().length > 1 ? `<button class="dlink" data-draftall="1">${ICO(SPARK)}Draft the ${draftable().length} not started</button>` : ''}</p>
173
+ <div class="dprog" role="img" aria-label="${c.ok} of ${c.all} up to date">${core.map((d) => `<i class="${dot[d.st] || (d.st === 'live' ? 'ok' : 'mut')}"></i>`).join('')}</div>
174
+ <div class="dtool"><div class="seg" id="dmF" role="group" aria-label="Filter documents"><i class="pill"></i>${[['all', 'All', c.all], ['none', 'Not started', c.none], ['draft', 'Draft', c.draft], ['ok', 'Up to date', c.ok]].map(([k, l, n]) => `<button data-dmf="${k}" aria-pressed="${docFilter === k}">${l}<em>${n}</em></button>`).join('')}</div>
175
+ <label class="dmsr">${ICO('<circle cx="11" cy="11" r="6.5"/><path d="m20 20-4-4"/>')}<input id="dmQ" type="search" placeholder="Find a document" aria-label="Find a document" value="${esc(docQuery)}"></label></div>
176
+ <div id="dmList"></div>
177
+ <p class="note" style="margin-top:12px">The core documents of this initiative. Press a row to read it, see its edit log or its health. Draft it starts a read-only agent run that writes a draft for you to read; nothing is sent. Editing, issuing and dropping a document are not built on this page.</p></section>`;
178
+ }
179
+ $('#iMain').addEventListener('input', (e) => { if (e.target.id === 'dmQ') { docQuery = e.target.value; drawDocList(); } });
180
+ const draftable = () => M.docs.rows.filter((d) => !d.tech && d.st === 'missing' && d.act && d.act.kind === 'draft' && !d.running);
181
+ async function draftKey(key) {
182
+ const d = M.docs.rows.find((x) => x.key === key);
183
+ const res = await pmPost('/api/core-docs/draft', { ws: M.board.ws, scope: M.docs.scope, key });
184
+ if (!res || res.ok !== true) { toast((res && res.error) || 'The draft could not be started.'); await load(); return false; }
185
+ toast('The agent is drafting “' + (d ? d.title : key) + '”. It runs in the background and sends nothing.');
186
+ await load(); return true;
187
+ }
188
+ async function draftAll(b) {
189
+ b.disabled = true;
190
+ const L = draftable(); let n = 0;
191
+ for (const d of L) { const res = await pmPost('/api/core-docs/draft', { ws: M.board.ws, scope: M.docs.scope, key: d.key }); if (res && res.ok === true) n++; else { toast((res && res.error) || 'A draft could not be started.'); break; } }
192
+ if (n) toast('The agent is drafting ' + n + ' ' + (n === 1 ? 'document' : 'documents') + '. They run in the background and send nothing.');
193
+ await load();
194
+ }
195
+ async function draft(b) {
196
+ const key = b.dataset.draft, d = M.docs.rows.find((x) => x.key === key);
197
+ b.disabled = true;
198
+ const res = await pmPost('/api/core-docs/draft', { ws: M.board.ws, scope: M.docs.scope, key });
199
+ if (!res || res.ok !== true) { b.disabled = false; toast((res && res.error) || 'The draft could not be started.'); return; }
200
+ toast('The agent is drafting “' + (d ? d.title : key) + '”. It runs in the background and sends nothing.');
201
+ await load();
202
+ }
203
+
204
+ /* ── the initiative's own dialog: the read-only agent about this initiative, with an optional first question ─────────────────────── */
205
+ function openAsk(q) {
206
+ const r = M.rows[0]; if (!r) return;
207
+ openDlg({ eye: 'Initiative · real data', title: r.title, sub: r.sub, say: r.dialog.say, stream: turnA(logHtml({ logTitle: 'Read from the repository', log: ['Read the initiative from the board and its sheet', 'Read its workflow steps from the workflow engine', 'Changed nothing'] })) + turnA(ptx(r.dialog.say)), noteKey: r.key, pane: `<article class="paper doc">${kv(r.dialog.facts)}<p class="xn">The initiative as the board holds it. Ask the agent anything about it; it reads this repository and changes nothing.</p></article>`, label: 'Initiative' });
208
+ loadThread(r.key);
209
+ if (q) { const i = $('#askIn'); i.value = q; $('#askF').dispatchEvent(new Event('submit', { cancelable: true, bubbles: true })); }
210
+ setMode('chat');
211
+ }
212
+
213
+ /* ── the document sheet: Read, Edit log, Health. Read-only. ─────────────────────────────────────────────── */
214
+ let sheet = null; // { d, tab, text, log, health, err }
215
+ const TABS = [['read', 'Read'], ['log', 'Edit log'], ['health', 'Health']];
216
+ function mdHtml(text) {
217
+ const lines = String(text || '').replace(/\r/g, '').split('\n'), out = [], outline = [];
218
+ let i = 0, fm = [];
219
+ if (lines[0] === '---') { const e = lines.indexOf('---', 1); if (e > 0) { fm = lines.slice(1, e).map((l) => /^([A-Za-z_][\w -]*):\s*(.*)$/.exec(l)).filter(Boolean).slice(0, 10).map((m) => [m[1], m[2].replace(/^"(.*)"$/, '$1')]); i = e + 1; } }
220
+ let para = [], list = [];
221
+ const flushP = () => { if (para.length) { out.push(`<p>${esc(para.join(' '))}</p>`); para = []; } };
222
+ const flushL = () => { if (list.length) { out.push(`<ul>${list.map((x) => `<li>${esc(x)}</li>`).join('')}</ul>`); list = []; } };
223
+ for (; i < lines.length; i++) {
224
+ const l = lines[i];
225
+ if (/^```/.test(l)) { flushP(); flushL(); const buf = []; for (i++; i < lines.length && !/^```/.test(lines[i]); i++) buf.push(lines[i]); out.push(`<pre class="mdp">${esc(buf.join('\n'))}</pre>`); continue; }
226
+ const h = /^(#{1,4})\s+(.*)$/.exec(l);
227
+ if (h) { flushP(); flushL(); const n = outline.length; outline.push(h[2]); out.push(`<h3 class="d2" id="ds-h${n}">${esc(h[2])}</h3>`); continue; }
228
+ if (/^\s*[-*]\s+/.test(l)) { flushP(); list.push(l.replace(/^\s*[-*]\s+/, '')); continue; }
229
+ if (/^\|/.test(l)) { flushP(); flushL(); const buf = [l]; while (i + 1 < lines.length && /^\|/.test(lines[i + 1])) buf.push(lines[++i]); out.push(`<pre class="mdp">${esc(buf.join('\n'))}</pre>`); continue; }
230
+ if (!l.trim()) { flushP(); flushL(); continue; }
231
+ flushL(); para.push(l.trim());
232
+ }
233
+ flushP(); flushL();
234
+ return { html: out.join(''), outline, fm };
235
+ }
236
+ function sheetPane() {
237
+ const s = sheet, d = s.d;
238
+ const menu = `<div class="seg" id="shT" role="group" aria-label="Document views"><i class="pill"></i>${TABS.map(([k, n]) => `<button data-sh="${k}" aria-pressed="${s.tab === k}">${n}</button>`).join('')}</div>`;
239
+ const head = `<p class="dpath">${esc(d.path)}</p><div class="dsm"><span class="${d.st === 'ok' ? 'ok' : d.st === 'draft' ? 'warn' : 'mut'}">${esc(d.state)}</span>${d.when ? `<span>updated ${esc(d.when)}</span>` : ''}${d.by ? `<span>${esc(d.by)}</span>` : ''}</div>`;
240
+ let body = '';
241
+ if (s.err) body = `<p class="empty3 plain">${esc(s.err)}</p>`;
242
+ else if (s.tab === 'read') {
243
+ if (s.text == null) body = '<p class="empty3 plain">Reading the file.</p>';
244
+ else { const m = s.md || (s.md = mdHtml(s.text)); body = (m.fm.length ? kv(m.fm) : '') + `<div class="dsrd${m.outline.length > 2 ? '' : ' no'}">${m.outline.length > 2 ? `<nav class="dso" aria-label="Outline"><p class="label">Outline <em>${m.outline.length} headings</em></p>${m.outline.map((t, n) => `<button data-go="${n}">${esc(t)}</button>`).join('')}</nav>` : ''}<div class="dsbody">${m.html}</div></div>`; }
245
+ } else if (s.tab === 'log') {
246
+ if (!s.log) body = '<p class="empty3 plain">Reading the log.</p>';
247
+ else {
248
+ const R = (s.log.revisions || []).slice().reverse(), E = (s.log.events || []).slice().reverse();
249
+ body = (R.length ? `<div class="evl">${R.map((r) => `<div class="hsr"><time>${esc(when(r.at))}</time><span><b>v${esc(r.rev)}</b> ${esc(r.why || (r.by === 'baseline' ? 'The file as the sheet first saw it' : 'No reason recorded'))}${r.by ? ' · ' + esc(r.by) : ''}</span></div>`).join('')}</div>` : '<p class="mut">No revision is recorded.</p>')
250
+ + (E.length ? `<p class="label" style="margin:18px 0 8px">Other entries</p><div class="evl">${E.map((e) => `<div class="hsr"><time>${esc(when(e.at))}</time><span><b>${esc(e.kind)}</b> ${esc(e.prompt || e.why || '')}</span></div>`).join('')}</div>` : '');
251
+ }
252
+ } else if (s.tab === 'health') {
253
+ if (!s.health) body = '<p class="empty3 plain">Checking the document.</p>';
254
+ else body = `<p class="note" style="margin:0 0 10px">${s.health.passed} of ${s.health.total} checks that had the input they need passed. A check with no input is not counted.</p><div class="ckl">${s.health.checks.map((c) => `<div class="ckr ${c.pass === true ? 'done' : c.pass === false ? 'blocked' : 'skipped'}"><i></i><b>${esc(c.id)}</b><span>${c.pass === true ? 'Passed' : c.pass === false ? 'Not passed' : 'No input'}</span><small>${esc(c.reason || '')}</small></div>`).join('')}</div>`;
255
+ }
256
+ return `<article class="paper doc">${head}${menu}<div id="shBody">${body}</div><p class="xn">Read-only. Editing a document and accepting an agent’s proposal are not built on this page.</p></article>`;
257
+ }
258
+ function drawSheet() { $('#oMain').innerHTML = sheetPane(); pillOf($('#shT')); }
259
+ async function getJson(url, init) { try { const r = await fetch(url, Object.assign({ credentials: 'same-origin' }, init || {})); return await r.json(); } catch { return null; } }
260
+ async function loadTab(tab) {
261
+ const s = sheet, p = encodeURIComponent(s.d.path), ws = M.board.ws ? '&ws=' + encodeURIComponent(M.board.ws) : '';
262
+ if (tab === 'read' && s.text == null) {
263
+ const lg = s.log || (s.log = await getJson('/api/doc-sheet/log?path=' + p + ws));
264
+ if (sheet !== s) return;
265
+ const revs = lg && lg.ok ? lg.revisions || [] : [];
266
+ const at = revs.length ? await getJson('/api/doc-sheet/at?path=' + p + '&rev=' + revs[revs.length - 1].rev + ws) : null;
267
+ if (sheet !== s) return;
268
+ if (at && at.ok) s.text = at.text; else s.err = (at && at.error) || (lg && lg.error) || 'The file could not be read.';
269
+ } else if (tab === 'log' && !s.log) {
270
+ s.log = await getJson('/api/doc-sheet/log?path=' + p + ws);
271
+ if (sheet !== s) return;
272
+ if (!s.log || s.log.ok === false) { s.err = (s.log && s.log.error) || 'The edit log could not be read.'; s.log = null; }
273
+ } else if (tab === 'health' && !s.health) {
274
+ const h = await getJson('/api/doc-health/check', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: s.d.path, ws: M.board.ws }) });
275
+ if (sheet !== s) return;
276
+ if (!h || h.ok === false) s.err = (h && h.error) || 'The health check could not run.'; else s.health = h.health;
277
+ }
278
+ if (sheet === s && s.tab === tab) drawSheet();
279
+ }
280
+ function openSheet(d) {
281
+ if (!d || !d.path) return;
282
+ sheet = { d, tab: 'read', text: null, log: null, health: null, err: null };
283
+ const key = 'doc:' + M.id + ':' + d.key, say = d.title + ' is ' + d.state.toLowerCase() + (d.by ? '. ' + d.by + '.' : '.') + ' I can read the file with you and answer questions. I change nothing.';
284
+ openDlg({ eye: 'Document · real data', title: d.title, sub: M.title, say, stream: turnA(logHtml({ logTitle: 'Read from the repository', log: ['Found the file ' + d.path, 'Read its revisions from the document sheet', 'Left the file as it is'] })) + turnA(ptx(say)), noteKey: key, pane: '', label: 'Document' });
285
+ drawSheet(); loadThread(key); loadTab('read');
286
+ }
287
+ $('#oMain').addEventListener('click', (e) => {
288
+ const t = e.target.closest('[data-sh]');
289
+ if (t && sheet) { sheet.tab = t.dataset.sh; sheet.err = null; drawSheet(); loadTab(sheet.tab); return; }
290
+ const g = e.target.closest('[data-go]'); if (g) { const h = $('#ds-h' + g.dataset.go); if (h) h.scrollIntoView({ block: 'start', behavior: 'smooth' }); }
291
+ });
292
+ $('#dBody').addEventListener('click', (e) => { const lt = e.target.closest('#logT'); if (lt) { const c = $('#alog').classList.toggle('col'); lt.setAttribute('aria-expanded', !c); } });
293
+
294
+ /* ── load: the routes derive the initiatives in the background, so the first answers are "reading" ────────── */
295
+ let crumbed = false;
296
+ function draw() {
297
+ if (!crumbed) {
298
+ crumbed = true;
299
+ $('#crumbHost').innerHTML = crumbHtml({ board: M.board.name || 'Board', boards: M.boards, ini: M.crumbTitle, siblings: M.siblings, ask: 'Ask about this initiative' });
300
+ const q = (id) => '?id=' + encodeURIComponent(id) + (M.board.ws ? '&ws=' + encodeURIComponent(M.board.ws) + '&board=' + encodeURIComponent(M.board.id) : '');
301
+ wireCrumb({ boards: M.boards, board: M.board.id, siblings: M.siblings, ini: M.id,
302
+ goBoard: (ws, id) => location.assign('/premium-board.html' + (ws ? '?ws=' + encodeURIComponent(ws) + '&board=' + encodeURIComponent(id) : '')),
303
+ goIni: (id) => location.assign('/premium-initiative.html' + q(id)), ask: () => openAsk('') });
304
+ }
305
+ $('#iH1').textContent = M.title; document.title = 'PMOS · ' + M.title;
306
+ $('#snap').textContent = M.header.text;
307
+ $('#snap').title = 'Every figure on this page comes from a record. A date, owner or progress shows only when one was set.';
308
+ const keepQ = $('#dmQ') && document.activeElement === $('#dmQ');
309
+ $('#iMain').innerHTML = headHtml() + `<div class="bcols"><div>${overviewHtml()}</div><div>${checksHtml()}</div></div>` + tasksHtml() + docsHtml();
310
+ if ($('#dmList')) drawDocList();
311
+ if (keepQ) { const q = $('#dmQ'); q.focus(); q.setSelectionRange(q.value.length, q.value.length); }
312
+ pillOf($('#tkTabs'));
313
+ $('#dN').textContent = M.dock.stage || '·'; $('#dLab').textContent = M.dock.label;
314
+ $('#dNext').innerHTML = M.dock.next ? `<i></i>Next <b>${esc(M.dock.next)}</b>` : 'Nothing is waiting';
315
+ onScroll();
316
+ }
317
+ async function load() {
318
+ let r = null, status = 0;
319
+ try { const res = await fetch('/api/initiative-premium?' + query(), { credentials: 'same-origin' }); status = res.status; r = await res.json(); } catch { r = null; }
320
+ clearInterval(readTimer);
321
+ if (r && r.state === 'reading') {
322
+ const t0 = Date.now(), tick = () => { $('#snap').textContent = 'Reading the initiative · ' + Math.round((Date.now() - t0) / 1000) + ' s so far'; };
323
+ tick(); readTimer = setInterval(tick, 1000);
324
+ setTimeout(load, 3000);
325
+ return;
326
+ }
327
+ if (!r || !r.ok) {
328
+ $('#snap').textContent = 'The initiative could not be read';
329
+ $('#iMain').innerHTML = `<p class="empty3 plain">${status === 404 ? esc((r && r.error) || 'This initiative is not on a board.') : 'The initiative could not be read, so this page shows nothing rather than guess.' + (r && r.error ? ' The reason: ' + esc(r.error) : status ? ' (the server answered ' + status + ')' : ' The server did not answer.')}</p>`;
330
+ if (status !== 404 && status !== 400) setTimeout(load, 15000);
331
+ return;
332
+ }
333
+ M = r.model; M.asOf = r.as_of; draw();
334
+ if (M.docs.rows.some((d) => d.running)) setTimeout(load, 5000); // a core-document run is going: ask again until it ends
335
+ }
336
+ if (!ID) { $('#snap').textContent = 'No initiative named'; $('#iMain').innerHTML = '<p class="empty3 plain">This page needs an initiative. Open one from the Board.</p>'; } else load();
@@ -0,0 +1,184 @@
1
+ /* pm/premium-shell.js — what the premium Briefing and My work pages share: the theme, the toast, the standard dialog and the read-only agent's conversation (initiatives briefing-premium and my-work-premium). */
2
+ import { pmPost } from './http.js';
3
+ export const $ = (s, r = document) => r.querySelector(s);
4
+ export const $$ = (s, r = document) => [...r.querySelectorAll(s)];
5
+ export const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
6
+ export const RM = matchMedia('(prefers-reduced-motion: reduce)').matches;
7
+ export const store = { get(k) { try { return localStorage.getItem(k); } catch { return null; } }, set(k, v) { try { localStorage.setItem(k, v); } catch { /* no storage */ } } };
8
+ export const THEME_KEY = 'pmos.c3.theme';
9
+ export const AI = '<svg class="i" viewBox="0 0 24 24"><path d="M12 3l1.8 4.7L18.5 9.5l-4.7 1.8L12 16l-1.8-4.7L5.5 9.5l4.7-1.8z"/></svg>';
10
+ export const S = { key: null }; // the key of the row whose dialog is open (the agent's thread belongs to it)
11
+ let lastFocus = null;
12
+ /* ── theme ──────────────────────────────────────────────────────────────────────────────────────────────── */
13
+ export function theme(t) {
14
+ if (t === 'light' || t === 'dark') document.documentElement.dataset.theme = t; else delete document.documentElement.dataset.theme;
15
+ }
16
+ export function flip() {
17
+ const dark = document.documentElement.dataset.theme ? document.documentElement.dataset.theme === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
18
+ const next = dark ? 'light' : 'dark';
19
+ theme(next); store.set(THEME_KEY, next);
20
+ }
21
+ { const t = new URLSearchParams(location.search).get('theme') || store.get(THEME_KEY); theme(t); }
22
+ $('#theme').onclick = flip; $('#theme2').onclick = flip;
23
+
24
+ /* ── toast ──────────────────────────────────────────────────────────────────────────────────────────────── */
25
+ let tt = null;
26
+ export function toast(msg) { const t = $('#toast'); t.textContent = msg; t.classList.add('on'); clearTimeout(tt); tt = setTimeout(() => t.classList.remove('on'), 4200); }
27
+
28
+ /* ── the standard dialog: detail first, the conversation beside it (Detail tab first on a phone) ─────────── */
29
+ export const kv = (rows) => `<div class="kv2">${rows.filter((r) => r && r[1]).map(([k, v]) => `<span>${esc(k)}</span><span>${esc(v)}</span>`).join('')}</div>`;
30
+ export const H = (t) => `<h3 class="h2">${esc(t)}</h3>`;
31
+ export function openDlg(o) {
32
+ S.key = o.noteKey || null;
33
+ const ae = document.activeElement; if (ae && ae !== document.body) lastFocus = ae;
34
+ $$('.rail,main.stage,#dock').forEach((x) => { x.inert = true; });
35
+ $('#dlg').inert = false; document.documentElement.style.overflow = 'hidden';
36
+ $('#dEye').textContent = o.eye; $('#dTitle').textContent = o.title; $('#dSub').textContent = o.sub || ''; $('#dSub').hidden = !o.sub;
37
+ $('#oMain').innerHTML = o.pane || `<article class="paper doc">${o.html}</article>`; $('#oLab').textContent = o.label || 'Detail';
38
+ $('#dBody').innerHTML = `<div class="stream" role="log">${o.stream || turnA(ptx(o.say))}</div><form class="askbar" id="askF"><input id="askIn" placeholder="Ask the agent about this row" maxlength="2000" autocomplete="off" aria-label="A question for the agent"><button type="submit" data-act="ask" id="askGo" aria-label="Ask the agent" title="Ask the agent"><svg class="i" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></svg></button></form><p class="fine2" id="askNote">The agent reads this repository and answers in words. It cannot change anything, and its answers are not checked against the records.</p>`;
39
+ $('#dlg').classList.remove('full'); setMode(innerWidth < 1000 ? 'detail' : 'split'); setTab('out'); // on a phone the dialog opens on Detail and the header button brings the conversation
40
+ $('#veil').classList.add('on'); $('#dlg').classList.add('on');
41
+ $$('#dGrid .pane').forEach((p) => { p.scrollTop = 0; });
42
+ setTimeout(() => $('#dlg').focus(), 60);
43
+ }
44
+ export function closeDlg() {
45
+ clearTimeout(threadTimer); S.key = null;
46
+ $('#veil').classList.remove('on'); $$('.rail,main.stage,#dock').forEach((x) => { x.inert = false; });
47
+ $('#dlg').inert = true; document.documentElement.style.overflow = ''; $('#dlg').classList.remove('on');
48
+ $('#dock').inert = !document.body.classList.contains('docked');
49
+ if (lastFocus) { try { lastFocus.focus(); } catch { /* gone */ } }
50
+ }
51
+ export function pillOf(s) { const o = $('button[aria-pressed="true"]', s), p = $('.pill', s); requestAnimationFrame(() => { if (!o || !p) return; p.style.left = o.offsetLeft + 'px'; p.style.width = o.offsetWidth + 'px'; }); }
52
+ export function setTab(t) { $('#dGrid').dataset.tab = t; $$('#dTabs button').forEach((b) => b.setAttribute('aria-pressed', b.dataset.t === t)); pillOf($('#dTabs .seg')); }
53
+ export function setMode(m) {
54
+ $('#dlg').dataset.mode = m;
55
+ const b = $('#dPane'), on = m === 'split';
56
+ b.setAttribute('aria-pressed', on); b.title = on ? 'Hide the conversation' : 'Show the conversation'; b.setAttribute('aria-label', b.title);
57
+ if (innerWidth < 1000) setTab(m === 'detail' ? 'out' : 'chat');
58
+ }
59
+ $('#dPane').onclick = () => setMode($('#dlg').dataset.mode === 'split' ? 'detail' : 'split');
60
+ export function setFull(f) { $('#dlg').classList.toggle('full', f); const b = $('#dExpand'); b.setAttribute('aria-pressed', f); b.title = f ? 'Exit full screen' : 'Full screen'; $('#dExpP').setAttribute('d', f ? 'M20 10h-6V4M4 14h6v6M14 10l7-7M3 21l7-7' : 'M14 4h6v6M10 20H4v-6M20 4l-7 7M4 20l7-7'); }
61
+ $('#dClose').onclick = closeDlg; $('#veil').onclick = closeDlg;
62
+ $('#dExpand').onclick = () => setFull(!$('#dlg').classList.contains('full'));
63
+ $('#dTabs').addEventListener('click', (e) => { const b = e.target.closest('[data-t]'); if (b) setTab(b.dataset.t); });
64
+ addEventListener('keydown', (e) => {
65
+ if (e.key !== 'Escape' || !$('#dlg').classList.contains('on')) return;
66
+ if ($('#dlg').classList.contains('full')) { setFull(false); e.stopImmediatePropagation(); return; }
67
+ closeDlg();
68
+ }, true);
69
+ (() => {
70
+ const g = $('#dlg .grab'); let y0 = null, dy = 0;
71
+ g.addEventListener('pointerdown', (e) => { y0 = e.clientY; dy = 0; try { g.setPointerCapture(e.pointerId); } catch { /* none */ } });
72
+ g.addEventListener('pointermove', (e) => { if (y0 == null) return; dy = Math.max(0, e.clientY - y0); $('#dlg').style.transition = 'none'; $('#dlg').style.transform = `translateY(${dy}px)`; });
73
+ const end = () => { if (y0 == null) return; const d = dy; y0 = null; dy = 0; $('#dlg').style.transition = ''; $('#dlg').style.transform = ''; if (d > 90) closeDlg(); };
74
+ g.addEventListener('pointerup', end); g.addEventListener('pointercancel', end);
75
+ })();
76
+
77
+ export const CHECK = '<svg class="i" viewBox="0 0 24 24"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
78
+ export const CHEV = '<svg class="i" viewBox="0 0 24 24"><path d="M6 9l6 6 6-6"/></svg>';
79
+ export const ptx = (t) => `<p class="tt">${esc(t)}</p>`;
80
+ export const turnA = (h) => `<div class="turn a"><i class="av">${AI}</i><div class="tc">${h}</div></div>`;
81
+ export const H1 = (t) => `<h3 class="d1">${esc(t)}</h3>`;
82
+ export const H2 = (t) => `<h3 class="d2">${esc(t)}</h3>`;
83
+ export const P1 = (t) => `<p>${esc(t)}</p>`;
84
+ export const UL = (a) => `<ul>${a.map((x) => `<li>${esc(x)}</li>`).join('')}</ul>`;
85
+ export function logHtml(d) {
86
+ return `<button class="lbtn" id="logT" aria-expanded="false"><i class="ls">${CHECK}</i><span>${esc(d.logTitle)}</span><em>${d.log.length} steps</em>${CHEV}</button><div class="alog col" id="alog">${d.log.map((t) => `<div class="al s-done"><i>${CHECK}</i><span>${esc(t)}</span></div>`).join('')}</div>`;
87
+ }
88
+ export const turnU = (t) => `<div class="turn u"><div class="ub">${esc(t)}</div></div>`;
89
+ /* ── the agent's conversation: each answer is a fresh read-only run, kept in the workspace's state folder ── */
90
+ const turnAgent = (h) => `<div class="turn a"><i class="av">${AI}</i><div class="tc">${h}</div></div>`;
91
+ const paras = (t) => String(t).split(/\n{2,}/).map((x) => `<p class="tt">${esc(x.trim()).replace(/\n/g, '<br>')}</p>`).join('');
92
+ export function threadHtml(th) {
93
+ return (th.turns || []).map((t) => turnU(t.q) + (t.state === 'answered'
94
+ ? turnAgent(`<p class="agtag">Agent · not checked against records</p>${paras(t.a)}`)
95
+ : t.state === 'failed' ? turnAgent(`<p class="tt err" role="alert">The agent did not answer. ${esc(t.error || '')}</p>`)
96
+ : turnAgent(`<p class="tt" role="status">The agent is reading the records. Nothing is promised about how long this takes.</p>`))).join('');
97
+ }
98
+ let threadTimer = null;
99
+ export function drawThread(th, key) {
100
+ if (S.key !== key) return;
101
+ const box = $('#dBody .stream'); if (!box) return;
102
+ let t = $('#thr', box); if (!t) { box.insertAdjacentHTML('beforeend', '<div id="thr"></div>'); t = $('#thr', box); }
103
+ t.innerHTML = threadHtml(th);
104
+ const go = $('#askGo'); if (go) go.disabled = !!th.working;
105
+ if (th.working) $('#askNote').textContent = 'The agent is answering. You can ask again when it has.';
106
+ const b = $('#dBody'); b.scrollTop = b.scrollHeight;
107
+ }
108
+ export async function loadThread(key) {
109
+ clearTimeout(threadTimer);
110
+ let th = null;
111
+ try { const r = await fetch('/api/briefing-premium/thread?key=' + encodeURIComponent(key) + (new URLSearchParams(location.search).get('ws') ? '&ws=' + encodeURIComponent(new URLSearchParams(location.search).get('ws')) : ''), { credentials: 'same-origin' }); if (r.ok) th = await r.json(); } catch { th = null; }
112
+ if (!th || !th.ok) return;
113
+ drawThread(th, key);
114
+ if (th.working && S.key === key) threadTimer = setTimeout(() => loadThread(key), 2500);
115
+ }
116
+ $('#dBody').addEventListener('submit', async (e) => {
117
+ e.preventDefault();
118
+ if (!S.key) return;
119
+ const inp = $('#askIn'), text = inp.value.trim(), key = S.key;
120
+ if (!text) { $('#askNote').textContent = 'Write a question for the agent.'; return; }
121
+ const go = $('#askGo'); go.disabled = true;
122
+ const ws = new URLSearchParams(location.search).get('ws');
123
+ const res = await pmPost('/api/briefing-premium/ask' + (ws ? '?ws=' + encodeURIComponent(ws) : ''), { key, text });
124
+ if (!res || res.ok !== true) { go.disabled = false; $('#askNote').textContent = (res && res.error) || 'The question was not sent.'; return; }
125
+ inp.value = ''; $('#askNote').textContent = 'Sent. The agent is answering.';
126
+ loadThread(key);
127
+ });
128
+
129
+ /* ── the hero folds into the dock on scroll ─────────────────────────────────────────────────────────────── */
130
+ let tick = false;
131
+ export function onScroll() {
132
+ if (tick) return; tick = true;
133
+ requestAnimationFrame(() => {
134
+ tick = false;
135
+ const y = scrollY, h = $('.hero').offsetHeight, p = Math.max(0, Math.min(1, y / (h * 0.55))), big = $('.brief');
136
+ if (!RM) { big.style.transform = `translateY(${-p * 30}px) scale(${1 - p * 0.12})`; big.style.opacity = 1 - p * 0.9; }
137
+ document.body.classList.toggle('docked', p > 0.62);
138
+ if (!$('#dlg').classList.contains('on')) $('#dock').inert = p <= 0.62;
139
+ $$('.ch').forEach((c) => c.classList.toggle('stuck', c.getBoundingClientRect().top <= 61 && c.closest('.card').getBoundingClientRect().bottom > 140));
140
+ });
141
+ }
142
+ addEventListener('scroll', onScroll, { passive: true }); addEventListener('resize', onScroll);
143
+
144
+ /* ── the rail: Briefing and My work are premium pages; Board and Insights open the console's tab until their slices land ── */
145
+ const GO = { brief: '/premium.html', work: '/premium-work.html', init: '/premium-board.html', rep: '/c3.html?tab=rep' };
146
+ export function wireRail(here) {
147
+ const rail = $('.rail');
148
+ rail.style.setProperty('--pi', String(['brief', 'work', 'init', 'rep'].indexOf(here)));
149
+ $$('.rail [data-go]').forEach((b) => { b.onclick = () => { if (b.dataset.go !== here) location.assign(GO[b.dataset.go]); }; });
150
+ }
151
+
152
+ /* ── the crumb: where you are, a menu to switch board or initiative, and the sparkle that opens the read-only agent ──────────────────── */
153
+ export function crumbHtml(o) {
154
+ const chev = (id, label, show) => `<button id="${id}" class="cri" aria-haspopup="menu" aria-expanded="false" aria-label="${esc(label)}"${show ? '' : ' hidden'}>${CHEV}</button>`;
155
+ return `<nav class="crumb" aria-label="Where you are"><button id="crBoard"${o.ini ? '' : ' aria-current="page" disabled'}>${esc(o.board)}</button>${chev('crSw', 'Switch board', o.boards && o.boards.length > 1)}${o.ini ? `<span aria-hidden="true">›</span><span id="crIni" aria-current="page">${esc(o.ini)}</span>${chev('crSw2', 'Switch initiative', o.siblings && o.siblings.length > 1)}` : ''}<button id="crAsk" class="cri crask" aria-label="${esc(o.ask)}">${AI}</button><div id="crMenu" class="crmenu" role="menu" hidden></div></nav>`;
156
+ }
157
+ /** o: { boards: [{ws,id,name}], board: id, siblings: [{id,title}], ini: id, goBoard(ws, id), goIni(id), ask() }. Escape and a click outside close the menu; the arrows move in it. */
158
+ export function wireCrumb(o) {
159
+ const menu = $('#crMenu'); if (!menu) return;
160
+ const close = () => { menu.hidden = true; $$('#crSw,#crSw2').forEach((b) => b.setAttribute('aria-expanded', 'false')); };
161
+ const open = (kind, btn) => {
162
+ if (!menu.hidden && menu.dataset.k === kind) { close(); return; }
163
+ close(); menu.dataset.k = kind;
164
+ menu.innerHTML = kind === 'b'
165
+ ? (o.boards || []).map((b) => `<button role="menuitem" data-sw="${esc(b.ws + '|' + b.id)}"${b.id === o.board ? ' aria-current="true"' : ''}>${esc(b.name)}</button>`).join('')
166
+ : (o.siblings || []).map((s) => `<button role="menuitem" data-swi="${esc(s.id)}"${s.id === o.ini ? ' aria-current="true"' : ''}>${esc(s.title)}</button>`).join('');
167
+ menu.hidden = false; btn.setAttribute('aria-expanded', 'true'); const f = $('button', menu); if (f) f.focus();
168
+ };
169
+ const b1 = $('#crSw'), b2 = $('#crSw2');
170
+ if (b1) b1.onclick = (e) => { e.stopPropagation(); open('b', b1); };
171
+ if (b2) b2.onclick = (e) => { e.stopPropagation(); open('i', b2); };
172
+ $('#crBoard').onclick = () => { if (o.ini && o.goBoard) { const b = (o.boards || []).find((x) => x.id === o.board); o.goBoard(b ? b.ws : null, o.board); } };
173
+ menu.onclick = (e) => {
174
+ const k = e.target.closest('[data-sw]'); if (k && o.goBoard) { const [ws, id] = k.dataset.sw.split('|'); close(); o.goBoard(ws, id); return; }
175
+ const q = e.target.closest('[data-swi]'); if (q && o.goIni) { close(); o.goIni(q.dataset.swi); }
176
+ };
177
+ document.addEventListener('click', (e) => { if (!e.target.closest('#crSw,#crSw2,#crMenu')) close(); });
178
+ document.addEventListener('keydown', (e) => {
179
+ if (menu.hidden) return;
180
+ if (e.key === 'Escape') { const k = menu.dataset.k === 'b' ? b1 : b2; close(); if (k) k.focus(); }
181
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); const L = $$('button', menu), i = L.indexOf(document.activeElement); L[(i + (e.key === 'ArrowDown' ? 1 : L.length - 1)) % L.length].focus(); }
182
+ });
183
+ $('#crAsk').onclick = () => o.ask && o.ask();
184
+ }