create-pmos 0.252.0 → 0.253.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 +2 -2
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/workflow-state.js +6 -1
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/briefing-premium-board-test.js +275 -0
- package/workspace/briefing-premium-initiative-test.js +304 -0
- package/workspace/briefing-premium-test.js +438 -0
- package/workspace/briefing-premium-work-test.js +281 -0
- package/workspace/fixtures/board-premium-facts.js +38 -0
- package/workspace/fixtures/briefing-premium-facts.js +32 -0
- package/workspace/fixtures/initiative-premium-facts.js +42 -0
- package/workspace/fixtures/my-work-premium-facts.js +31 -0
- package/workspace/guide/briefing-premium.md +137 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/lib/board-premium.js +133 -0
- package/workspace/lib/briefing-premium-agent.js +134 -0
- package/workspace/lib/briefing-premium-writes.js +138 -0
- package/workspace/lib/briefing-premium.js +514 -0
- package/workspace/lib/engine/workflow-state.js +6 -1
- package/workspace/lib/headless.js +1 -1
- package/workspace/lib/initiative-premium.js +147 -0
- package/workspace/lib/my-work-premium.js +151 -0
- package/workspace/lib/route-modules.js +4 -0
- package/workspace/lib/routes-board-premium.js +61 -0
- package/workspace/lib/routes-briefing-premium.js +83 -0
- package/workspace/lib/routes-initiative-premium.js +68 -0
- package/workspace/lib/routes-my-work-premium.js +31 -0
- package/workspace/lib/vendor/scripts/workflow-state.js +6 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/premium-board.js +180 -0
- package/workspace/public/pm/premium-initiative.js +336 -0
- package/workspace/public/pm/premium-shell.js +184 -0
- package/workspace/public/pm/premium-work.js +198 -0
- package/workspace/public/pm/premium.css +1198 -0
- package/workspace/public/pm/premium.js +228 -0
- package/workspace/public/premium-board.html +73 -0
- package/workspace/public/premium-initiative.html +46 -0
- package/workspace/public/premium-work.html +84 -0
- package/workspace/public/premium.html +68 -0
- package/workspace/public/vendor/geist-mono-var.woff2 +0 -0
- package/workspace/public/vendor/geist-var.woff2 +0 -0
- package/workspace/server.js +4 -2
- package/workspace/test-files.json +12 -0
- package/workspace/test-lists/briefing-premium.json +17 -0
- package/workspace/third-party/README.md +12 -0
- package/workspace/third-party/geist-OFL.LICENSE +93 -0
- package/workspace/third-party/geist-mono-OFL.LICENSE +93 -0
|
@@ -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) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/* pm/premium-work.js — the premium My work page (initiative briefing-premium-work, slice 4). Reads GET /api/my-work-premium and draws the model
|
|
2
|
+
* lib/my-work-premium.js builds from the list the Work page reads: the hero, "On your plate", With agents, Waiting on others, the dock and the standard dialog.
|
|
3
|
+
* Every press goes to the Work page's own route under /api/pm-items; this file adds no writer. A control with nothing honest to write stays off and says why. */
|
|
4
|
+
import { pmPost } from './http.js';
|
|
5
|
+
import { $, $$, esc, openDlg, closeDlg, toast, loadThread, onScroll, wireRail, logHtml, turnA, turnU, ptx, CHECK, S } from './premium-shell.js';
|
|
6
|
+
|
|
7
|
+
let M = null; // the model
|
|
8
|
+
let openR = null; // the row whose dialog is open
|
|
9
|
+
const ICO = (p) => `<svg class="i" viewBox="0 0 24 24">${p}</svg>`;
|
|
10
|
+
const GLYPH = {
|
|
11
|
+
Jira: '<path d="M12 3l8 8-8 8-8-8z"/><path d="M9 11l3 3 3-3"/>',
|
|
12
|
+
Slack: '<path d="M9 4 7 20M17 4l-2 16M4.5 9h15M4 15h15"/>',
|
|
13
|
+
Board: '<path d="M12 3 3 8l9 5 9-5z"/><path d="m3 13 9 5 9-5"/>',
|
|
14
|
+
Yours: '<path d="M4 20l4-1 11-11-3-3L5 16z"/>',
|
|
15
|
+
};
|
|
16
|
+
const glyph = (src) => GLYPH[src] || '<circle cx="12" cy="12" r="4"/>';
|
|
17
|
+
wireRail('work');
|
|
18
|
+
|
|
19
|
+
/* ── hero and header ───────────────────────────────────────────────────────────────────────────────────── */
|
|
20
|
+
function drawHero() {
|
|
21
|
+
$('#date').textContent = M.date;
|
|
22
|
+
$('#brief').innerHTML = M.hero.map((s) => s.parts.map((p) => (p.s ? `<span class="k">${esc(p.t)}</span>` : esc(p.t))).join('')).join(' ');
|
|
23
|
+
$('#snap').textContent = M.header.text;
|
|
24
|
+
$('#snap').title = 'Every figure on this page comes from an item on your list. A date shows only when one was set.';
|
|
25
|
+
}
|
|
26
|
+
function drawDock() { $('#dN').textContent = M.dock.needs; $('#dLab').textContent = M.dock.needs === 0 ? 'to clear' : M.dock.label; $('#dNext').innerHTML = M.dock.next ? `<i></i>Next <b>${esc(M.dock.next)}</b>` : 'Nothing more on your list'; }
|
|
27
|
+
|
|
28
|
+
/* ── the plate ─────────────────────────────────────────────────────────────────────────────────────────── */
|
|
29
|
+
function rowHtml(r, o = {}) {
|
|
30
|
+
const inbox = r.list === 'inbox' || r.list === 'tray', sm = o.sm;
|
|
31
|
+
let lead;
|
|
32
|
+
if (sm === 'ag') lead = `<span class="ags${r.agent === 'ready' ? ' ready' : ''}">${r.agent === 'ready' ? ICO('<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"/>') : r.agent === 'failed' ? ICO('<path d="M12 8v5M12 16.5v.5"/><circle cx="12" cy="12" r="9"/>') : '<span class="spin"></span>'}</span>`;
|
|
33
|
+
else if (sm === 'wait') lead = `<span class="avp" aria-hidden="true">${esc(((r.sub.match(/waiting on (\S)/) || [])[1] || '·'))}</span>`;
|
|
34
|
+
else if (inbox) lead = `<span class="wgl" title="${esc(r.src)}">${ICO(glyph(r.src))}</span>`;
|
|
35
|
+
else if (r.agent === 'ready') lead = `<button class="wck dec" data-open="${esc(r.key)}" aria-label="Review the agent draft: ${esc(r.title)}" title="An agent draft is ready. Open it to review.">${ICO('<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"/>')}</button>`;
|
|
36
|
+
else lead = `<button class="wck" data-ck="${esc(r.id)}" aria-pressed="false" aria-label="Mark done: ${esc(r.title)}">${CHECK}</button>`;
|
|
37
|
+
const meta = r.chip ? `<span class="wm"><span class="due${r.overdue ? ' od' : ''}">${esc(r.chip)}</span></span>` : '';
|
|
38
|
+
const state = sm === 'ag' ? ' · ' + ({ ready: 'draft ready to review', failed: 'the run failed', queued: 'queued', working: 'working', sending: 'sending' }[r.agent] || '') : '';
|
|
39
|
+
return `<div class="wrw"><div class="wrow${sm ? ' sm' : ''}${inbox ? ' inb' : ''}${o.first ? ' first' : ''}" data-key="${esc(r.key)}">${lead}<button class="wrh" aria-haspopup="dialog"><span class="wtx"><span class="wtit">${esc(r.title)}${o.first ? '<em class="tgf">Do this first</em>' : ''}</span><span class="ws">${esc(r.sub)}${esc(state)}</span></span>${meta}</button></div></div>`;
|
|
40
|
+
}
|
|
41
|
+
function drawPlate() {
|
|
42
|
+
const P = M.plate;
|
|
43
|
+
$('#plateCap').innerHTML = `<b>${P.n}</b> to clear`;
|
|
44
|
+
$('#plateNote').textContent = P.note;
|
|
45
|
+
let h = '';
|
|
46
|
+
for (const s of P.sections) {
|
|
47
|
+
if (!s.n) {
|
|
48
|
+
h += `<div class="wsec" data-lane="${s.key}"><span class="label">${esc(s.label)}</span><em>empty</em></div>` + (s.key === 'inbox' ? `<p class="empty3">${CHECK}Nothing waits to be sorted.</p>` : '<p class="empty3 plain">Nothing here.</p>');
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
h += `<div class="wsec" data-lane="${s.key}"><span class="label">${esc(s.label)}</span><em>${s.n} open</em></div>`;
|
|
52
|
+
const first = s.key === 'today' ? s.rows.find((r) => r.overdue) || null : null;
|
|
53
|
+
h += s.rows.map((r) => rowHtml(r, { first: first && r.key === first.key })).join('');
|
|
54
|
+
if (s.more) h += `<p class="note">${s.more} more ${s.more === 1 ? 'item is' : 'items are'} not shown here.</p>`;
|
|
55
|
+
}
|
|
56
|
+
$('#plateBody').innerHTML = h;
|
|
57
|
+
}
|
|
58
|
+
function drawTray() {
|
|
59
|
+
const T = M.tray;
|
|
60
|
+
$('#trayCap').textContent = T.cap;
|
|
61
|
+
$('#trayBody').innerHTML = T.rows.length ? T.rows.map((r) => rowHtml(r)).join('') + (T.more ? `<p class="note">${T.more} more not shown here.</p>` : '') : '<p class="empty3 plain">Nothing in your tray. Add a line above and it lands here first.</p>';
|
|
62
|
+
$('#trayNote').textContent = T.note;
|
|
63
|
+
}
|
|
64
|
+
function drawAgents() {
|
|
65
|
+
const A = M.agents, c = A.counts;
|
|
66
|
+
$('#agCap').textContent = A.cap;
|
|
67
|
+
$('#pipe').innerHTML = `<div class="pst"><b>${c.running}</b><span>Running</span></div><div class="pst"><b>${c.failed}</b><span>Failed</span></div><div class="pst${c.ready ? ' hot' : ''}"><b>${c.ready}</b><span>To review</span></div>`;
|
|
68
|
+
$('#agl').innerHTML = A.rows.length ? A.rows.map((r) => rowHtml(r, { sm: 'ag' })).join('') : '<p class="empty3 plain">No agent is working on your items.</p>';
|
|
69
|
+
$('#agHold').textContent = A.hold;
|
|
70
|
+
}
|
|
71
|
+
function drawWaiting() {
|
|
72
|
+
const W = M.waiting;
|
|
73
|
+
$('#waitCap').textContent = W.cap;
|
|
74
|
+
$('#waitBody').innerHTML = W.rows.length ? W.rows.map((r) => rowHtml(r, { sm: 'wait' })).join('') + (W.more ? `<p class="note">${W.more} more not shown here.</p>` : '') : '<p class="empty3 plain">Nobody owes you anything.</p>';
|
|
75
|
+
}
|
|
76
|
+
function drawReview() {
|
|
77
|
+
const R = M.review; $('#revCard').hidden = !R.n;
|
|
78
|
+
$('#revCap').textContent = R.n + ' ' + (R.n === 1 ? 'item' : 'items');
|
|
79
|
+
$('#revTxt').innerHTML = `<b>${R.n}</b> ${R.n === 1 ? 'item has' : 'items have'} sat untouched for ${R.days} days. Open one to keep it or let it go.`;
|
|
80
|
+
}
|
|
81
|
+
function draw() { drawHero(); drawPlate(); drawTray(); drawAgents(); drawWaiting(); drawReview(); drawDock(); onScroll(); }
|
|
82
|
+
const rowOf = (key) => (M && M.rows || []).find((r) => r.key === key) || null;
|
|
83
|
+
|
|
84
|
+
/* ── the dialog: detail first, the presses of the Work page, the read-only agent beside it ─────────────────── */
|
|
85
|
+
const NOTYET = [['later-today', 'Later today'], ['tomorrow', 'Tomorrow'], ['next-week', 'Next week']];
|
|
86
|
+
function cardHtml(r) {
|
|
87
|
+
const d = r.dialog, w = d.write, a = w.actions, err = '<p class="nodef err" id="cardErr" role="alert" hidden></p>';
|
|
88
|
+
const move = [a.includes('flag') ? ['flag', 'Do today'] : null, a.includes('park') ? ['park', 'Later'] : null].filter(Boolean);
|
|
89
|
+
return `<div class="acard" id="card"><p class="ct">What you can do</p>`
|
|
90
|
+
+ `<div class="cact two"><button class="cta" data-p="done">${CHECK}<span>Done</span></button>${a.includes('keep') ? '<button class="cta quiet" data-p="keep"><span>Keep it</span></button>' : ''}</div>`
|
|
91
|
+
+ `<div class="mv2">Not yet <span class="seg2">${NOTYET.map(([k, t]) => `<button data-p="notyet" data-when="${k}">${t}</button>`).join('')}</span></div>`
|
|
92
|
+
+ (move.length ? `<div class="mv2">Move to <span class="seg2">${move.map(([k, t]) => `<button data-p="${k}">${t}</button>`).join('')}</span></div>` : '')
|
|
93
|
+
+ (a.includes('waiting') ? `<label class="whyl" for="wOn">Waiting on someone</label><div class="cact two"><input id="wOn" class="why one" maxlength="80" placeholder="A name" autocomplete="off"><button class="cta quiet" data-p="waiting" id="wBtn" disabled><span>Wait for them</span></button></div>` : '')
|
|
94
|
+
+ `<div class="cact"><button class="cta quiet" data-p="remove"><span>Drop it</span></button></div>`
|
|
95
|
+
+ `${r.agent ? `<p class="nodef" id="holdWhy">${esc(d.hold)}</p>` : ''}${err}</div>`;
|
|
96
|
+
}
|
|
97
|
+
function paneHtml(r) {
|
|
98
|
+
const p = r.dialog.pane;
|
|
99
|
+
let h = `<p class="dpath">${esc(p.file)}</p>`; // the dialog heading above already names the item and its source
|
|
100
|
+
if (p.rows && p.rows.length) h += `<div class="kv2">${p.rows.map(([a, b]) => `<span>${esc(a)}</span><span>${esc(b)}</span>`).join('')}</div>`;
|
|
101
|
+
if (p.paras && p.paras.length) h += `<h3 class="d2">${esc(p.parasTitle)}</h3>${p.paras.map((x) => `<p>${esc(x)}</p>`).join('')}`;
|
|
102
|
+
return `<article class="paper doc">${h}<p class="xn">The item as your list holds it.</p></article>`;
|
|
103
|
+
}
|
|
104
|
+
function openRow(r) {
|
|
105
|
+
if (!r) return;
|
|
106
|
+
openR = r; const d = r.dialog;
|
|
107
|
+
openDlg({ eye: d.eye + ' · real data', title: r.title, sub: r.sub, say: d.say, stream: turnA(logHtml(d)) + turnA(ptx(d.say)) + cardHtml(r), noteKey: r.key, pane: paneHtml(r), label: d.pane.label });
|
|
108
|
+
loadThread(r.key);
|
|
109
|
+
setTimeout(() => { const b = $('#dBody'); b.scrollTop = b.scrollHeight; }, 120);
|
|
110
|
+
}
|
|
111
|
+
function showErr(msg) { const e = $('#cardErr'); if (!e) { toast(msg); return; } e.textContent = msg; e.hidden = false; }
|
|
112
|
+
|
|
113
|
+
/* a press: the page waits for the route's answer; a refusal shows its reason and changes nothing */
|
|
114
|
+
const ROUTE = { done: 'done', notyet: 'notyet', flag: 'flag', park: 'park', waiting: 'waiting', remove: 'remove', keep: 'keep' };
|
|
115
|
+
async function press(kind, id, extra) {
|
|
116
|
+
const body = { id, ...(extra || {}) };
|
|
117
|
+
if (kind === 'flag') body.flagged = true;
|
|
118
|
+
return pmPost('/api/pm-items/' + ROUTE[kind], body);
|
|
119
|
+
}
|
|
120
|
+
const SAID = { done: 'Marked done.', notyet: 'Put off. It comes back by itself.', flag: 'Moved to Today.', park: 'Moved to Later.', waiting: 'Now waiting.', remove: 'Dropped.', keep: 'Kept.' };
|
|
121
|
+
function toastUndo(msg, undo) {
|
|
122
|
+
const t = $('#toast'); t.innerHTML = `${esc(msg)} <button class="undo" type="button">Undo</button>`; t.classList.add('on');
|
|
123
|
+
const b = $('.undo', t); b.onclick = async () => { t.classList.remove('on'); const r = await undo(); if (!r || r.ok !== true) toast((r && r.error) || 'It could not be undone.'); await load(); };
|
|
124
|
+
clearTimeout(toastUndo.t); toastUndo.t = setTimeout(() => t.classList.remove('on'), 8000);
|
|
125
|
+
}
|
|
126
|
+
async function doPress(kind, id, extra, undo) {
|
|
127
|
+
const btns = $$('#card button'); btns.forEach((b) => { b.disabled = true; });
|
|
128
|
+
const res = await press(kind, id, extra);
|
|
129
|
+
if (!res || res.ok !== true) { btns.forEach((b) => { b.disabled = false; }); wSync(); showErr((res && res.error) || 'The press was not recorded.'); return false; }
|
|
130
|
+
closeDlg(); openR = null;
|
|
131
|
+
if (undo) toastUndo(SAID[kind], undo); else toast(SAID[kind]);
|
|
132
|
+
await load(); return true;
|
|
133
|
+
}
|
|
134
|
+
const wSync = () => { const b = $('#wBtn'); if (b) b.disabled = !(($('#wOn') || {}).value || '').trim(); };
|
|
135
|
+
$('#dBody').addEventListener('input', (e) => { if (e.target.id === 'wOn') wSync(); });
|
|
136
|
+
$('#dBody').addEventListener('click', (e) => {
|
|
137
|
+
const lt = e.target.closest('#logT');
|
|
138
|
+
if (lt) { const c = $('#alog').classList.toggle('col'); lt.setAttribute('aria-expanded', !c); return; }
|
|
139
|
+
const b = e.target.closest('[data-p]');
|
|
140
|
+
if (!b || b.disabled || !openR) return;
|
|
141
|
+
const id = openR.id, k = b.dataset.p;
|
|
142
|
+
if (k === 'notyet') doPress('notyet', id, { when: b.dataset.when });
|
|
143
|
+
else if (k === 'waiting') doPress('waiting', id, { on: ($('#wOn').value || '').trim() });
|
|
144
|
+
else if (k === 'done') doPress('done', id, null, () => pmPost('/api/pm-items/reopen', { id }));
|
|
145
|
+
else if (k === 'remove') doPress('remove', id, null, () => pmPost('/api/pm-items/undo', { id }));
|
|
146
|
+
else doPress(k, id);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/* ── the list: a check marks done, a row opens its dialog ────────────────────────────────────────────────── */
|
|
150
|
+
$('#plateBody').addEventListener('click', async (e) => { await listClick(e); });
|
|
151
|
+
$('#agl').addEventListener('click', (e) => { listClick(e); });
|
|
152
|
+
$('#waitBody').addEventListener('click', (e) => { listClick(e); });
|
|
153
|
+
$('#trayBody').addEventListener('click', (e) => { listClick(e); });
|
|
154
|
+
async function listClick(e) {
|
|
155
|
+
const ck = e.target.closest('.wck');
|
|
156
|
+
if (ck && ck.dataset.open) { openRow(rowOf(ck.dataset.open)); return; }
|
|
157
|
+
if (ck) {
|
|
158
|
+
const id = ck.dataset.ck;
|
|
159
|
+
ck.setAttribute('aria-pressed', 'true');
|
|
160
|
+
const res = await pmPost('/api/pm-items/done', { id });
|
|
161
|
+
if (!res || res.ok !== true) { ck.setAttribute('aria-pressed', 'false'); toast((res && res.error) || 'The press was not recorded.'); return; }
|
|
162
|
+
toastUndo(SAID.done, () => pmPost('/api/pm-items/reopen', { id })); await load(); return;
|
|
163
|
+
}
|
|
164
|
+
const row = e.target.closest('.wrow'); if (row && e.target.closest('.wrh')) openRow(rowOf(row.dataset.key));
|
|
165
|
+
}
|
|
166
|
+
$('#revBtn').onclick = () => { const id = (M.review.ids || [])[0]; const r = (M.rows || []).find((x) => x.id === id); if (r) openRow(r); };
|
|
167
|
+
|
|
168
|
+
/* ── add: one line, read by the Work page's own parser ─────────────────────────────────────────────────────── */
|
|
169
|
+
$('#cap').addEventListener('submit', async (e) => {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
const inp = $('#capIn'), line = inp.value.trim();
|
|
172
|
+
if (!line) { $('#capHint').textContent = 'Write one line to add.'; return; }
|
|
173
|
+
const go = $('.capb'); go.disabled = true;
|
|
174
|
+
const res = await pmPost('/api/pm-items/add', { line });
|
|
175
|
+
go.disabled = false;
|
|
176
|
+
if (!res || res.ok !== true) { $('#capHint').textContent = (res && res.error) || 'It was not added.'; return; }
|
|
177
|
+
inp.value = '';
|
|
178
|
+
const it = res.item || {};
|
|
179
|
+
const bits = [it.due ? 'due ' + it.due : null, it.priority].filter(Boolean);
|
|
180
|
+
$('#capHint').textContent = 'Added: ' + (it.title || line) + (bits.length ? ' (' + bits.join(', ') + ')' : '') + '. It is in your tray until you place it.';
|
|
181
|
+
await load();
|
|
182
|
+
const fresh = it.id ? rowOf('work:' + it.id) : null; // a new item opens at once, so it can be placed
|
|
183
|
+
if (fresh) openRow(fresh);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/* ── load ────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
187
|
+
async function load() {
|
|
188
|
+
let r = null, status = 0;
|
|
189
|
+
try { const res = await fetch('/api/my-work-premium', { credentials: 'same-origin' }); status = res.status; r = await res.json(); } catch { r = null; }
|
|
190
|
+
if (!r || !r.ok) {
|
|
191
|
+
$('#snap').textContent = 'Your list could not be read';
|
|
192
|
+
$('#brief').textContent = 'My work could not read your list, so it shows nothing rather than guess.' + (r && r.error ? ' The reason: ' + r.error : status ? ' (the server answered ' + status + ')' : ' The server did not answer.');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
M = r.model; draw();
|
|
196
|
+
}
|
|
197
|
+
$('#brief').textContent = 'Reading your list.';
|
|
198
|
+
load();
|