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.
- 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-products-test.js +164 -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 +151 -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-products.js +121 -0
- package/workspace/lib/briefing-premium-writes.js +138 -0
- package/workspace/lib/briefing-premium.js +581 -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 +115 -0
- package/workspace/lib/routes-initiative-premium.js +68 -0
- package/workspace/lib/routes-my-work-premium.js +31 -0
- package/workspace/lib/settings-store.js +3 -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 +1212 -0
- package/workspace/public/pm/premium.js +272 -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 +69 -0
- package/workspace/public/vendor/geist-mono-var.woff2 +0 -0
- package/workspace/public/vendor/geist-var.woff2 +0 -0
- package/workspace/server.js +5 -3
- package/workspace/test-files.json +14 -0
- package/workspace/test-lists/briefing-premium.json +19 -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
|
@@ -33,6 +33,8 @@ const bool = (section, scope, def) => ({ section, scope, type: 'bool', def });
|
|
|
33
33
|
const DEFS = Object.freeze({
|
|
34
34
|
'ws.start': one('workspaces', 'computer', ['last', 'ask'], 'last'),
|
|
35
35
|
'ws.briefing': one('workspaces', 'computer', ['one', 'all'], 'one'),
|
|
36
|
+
// the premium Briefing: which workspaces it reads (initiative briefing-premium-products). null = every one except the home workspace.
|
|
37
|
+
'ws.briefing.include': { section: 'workspaces', scope: 'computer', type: 'ids', max: 50, def: null },
|
|
36
38
|
'board.template': one('boards', 'ws', ['product', 'project', 'simple'], 'product'),
|
|
37
39
|
'board.rules': one('boards', 'ws', ['warn', 'off'], 'warn'),
|
|
38
40
|
'board.templates': { section: 'boards', scope: 'computer', type: 'templates', def: TEMPLATES },
|
|
@@ -121,6 +123,7 @@ function validate(key, value) {
|
|
|
121
123
|
if (typeof value !== 'string' || value.length > 64) return `${key} must be a time zone such as Asia/Jakarta, or empty for this computer's`;
|
|
122
124
|
try { new Intl.DateTimeFormat('en', { timeZone: value }); return null; } catch { return `${value} is not a time zone this computer knows`; }
|
|
123
125
|
}
|
|
126
|
+
case 'ids': return value === null || Array.isArray(value) && value.length <= d.max && value.every((x) => typeof x === 'string' && WS_ID.test(x)) && new Set(value).size === value.length ? null : `${key} must list at most ${d.max} workspace ids, each once`;
|
|
124
127
|
case 'templates': {
|
|
125
128
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return `${key} must name the templates`;
|
|
126
129
|
const ids = Object.keys(value);
|
|
@@ -323,7 +323,7 @@ function buildRange(root, { spec, whole }) {
|
|
|
323
323
|
|
|
324
324
|
// Where each path was ADDED, as an index into the same range. `--diff-filter=A` in one pass.
|
|
325
325
|
const added = new Map();
|
|
326
|
-
const addLog = git(root, ['log', '--reverse', '--format=%x1e%H', '--diff-filter=A', '--name-only', range]);
|
|
326
|
+
const addLog = git(root, ['log', '--reverse', '--no-renames', '--format=%x1e%H', '--diff-filter=A', '--name-only', range]); // no rename detection: a path renamed into place is an add, as the per-path lookup below sees it
|
|
327
327
|
if (addLog != null) {
|
|
328
328
|
for (const chunk of addLog.split('\x1e')) {
|
|
329
329
|
if (!chunk.trim()) continue;
|
|
@@ -358,6 +358,11 @@ function firstAppearance(root, range, rel) {
|
|
|
358
358
|
// ever been committed — so the fallback below only runs for a PR range, where "added before the
|
|
359
359
|
// range" and "never committed" are genuinely different answers and only git can tell them apart.
|
|
360
360
|
if (range.added && range.added.has(rel)) return range.added.get(rel);
|
|
361
|
+
// A TRACKED path that this range did not add was added before it: -1, exactly what the per-path
|
|
362
|
+
// `git log` below would conclude, from the one shared `ls-files`. That per-path call was one
|
|
363
|
+
// subprocess per record on a branch ahead of main, and most of a board read's time.
|
|
364
|
+
const tracked = trackedIn(root, range);
|
|
365
|
+
if (tracked && tracked.has(rel)) return -1;
|
|
361
366
|
const log = git(root, ['log', '--diff-filter=A', '--reverse', '--format=%H', '--', rel]);
|
|
362
367
|
const sha = log ? log.split('\n').map((s) => s.trim()).filter(Boolean)[0] : null;
|
|
363
368
|
if (!sha) return onDisk ? Infinity : null;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.254.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.254.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* pm/premium-board.js — the premium Board page (initiative briefing-premium-board, slice 5). Reads GET /api/board-premium and draws the model
|
|
2
|
+
* lib/board-premium.js builds from the routes the production Initiatives page reads: the Initiatives card (List, By stage, Timeline), the Sprint and
|
|
3
|
+
* Coming up cards, Core documents, the dock and the standard dialog. The one press is Draft it on a core document, through POST /api/core-docs/draft.
|
|
4
|
+
* Nothing here moves an initiative or issues a document. */
|
|
5
|
+
import { pmPost } from './http.js';
|
|
6
|
+
import { $, $$, esc, openDlg, closeDlg, toast, loadThread, onScroll, wireRail, logHtml, turnA, ptx, pillOf, CHECK, store, crumbHtml, wireCrumb } from './premium-shell.js';
|
|
7
|
+
|
|
8
|
+
const MODE_KEY = 'pmos.premium.openAs';
|
|
9
|
+
let M = null; // the model
|
|
10
|
+
let view = 'list';
|
|
11
|
+
let shown = 25; // rows of the list on screen
|
|
12
|
+
let readTimer = null;
|
|
13
|
+
wireRail('init');
|
|
14
|
+
const ICO = (p) => `<svg class="i" viewBox="0 0 24 24">${p}</svg>`;
|
|
15
|
+
const DOCI = '<path d="M6 3h9l4 4v14H6z"/><path d="M14 3v5h5M9 13h6M9 17h4"/>';
|
|
16
|
+
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"/>' };
|
|
17
|
+
|
|
18
|
+
/* ── header ───────────────────────────────────────────────────────────────────────────────────────────── */
|
|
19
|
+
let crumbed = false;
|
|
20
|
+
function drawHead() {
|
|
21
|
+
if (!crumbed) {
|
|
22
|
+
crumbed = true;
|
|
23
|
+
const boards = M.board.switcher || [];
|
|
24
|
+
$('#crumbHost').innerHTML = crumbHtml({ board: M.board.name || 'Board', boards, ask: 'Ask about this board' });
|
|
25
|
+
wireCrumb({ boards, board: M.board.id, goBoard: (ws, id) => location.assign('/premium-board.html?ws=' + encodeURIComponent(ws) + '&board=' + encodeURIComponent(id)), ask: () => openRow(rowOf('board:this-board')) });
|
|
26
|
+
}
|
|
27
|
+
$('#snap').textContent = M.header.text;
|
|
28
|
+
$('#snap').title = 'Every figure on this page comes from a record of the board. A date, owner or progress shows only when one was set.';
|
|
29
|
+
$('#brief').textContent = '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ── initiatives ───────────────────────────────────────────────────────────────────────────────────────────── */
|
|
33
|
+
const sub = (r) => `${esc(r.sub)}${r.statusLabel ? ` · <span class="${r.late ? 'bad' : 'warn'}">${esc(r.statusLabel)}</span>` : ''}`;
|
|
34
|
+
function listRow(r, i) {
|
|
35
|
+
return `<button class="ini2 ibi${r.first ? ' first' : ''}" data-key="${esc(r.key)}"><span class="it"><b>${esc(r.title)}${r.first ? '<em class="tgf">Do this first</em>' : ''}</b><small>${sub(r)}</small></span><span class="wst"><span class="sg2" aria-hidden="true">${r.seg.map((c) => `<i class="${c}"></i>`).join('')}</span><em>${esc(r.stage || 'No stage')}${r.stageNote ? ' · ' + esc(r.stageNote) : ''}</em></span><span class="pg">${r.progress == null ? '' : `<i><b style="width:${r.progress}%"></b></i>`}</span><span class="rl${r.over ? ' over' : ''}">${esc(r.finish)}</span></button>`;
|
|
36
|
+
}
|
|
37
|
+
function drawList() {
|
|
38
|
+
const L = M.initiatives.list.rows;
|
|
39
|
+
if (!L.length) return '<p class="empty3 plain">No initiatives are on this board\'s now view.</p>';
|
|
40
|
+
return `<div class="inih"><span>Initiative</span><span>Where it is</span><span>Progress</span><span>Planned finish</span></div>` + L.slice(0, shown).map(listRow).join('')
|
|
41
|
+
+ (L.length > shown ? `<p class="note" style="margin-top:10px">${L.length - shown} more are loaded. <button class="fold alt" id="more" type="button">Show ${Math.min(25, L.length - shown)} more</button></p>` : '');
|
|
42
|
+
}
|
|
43
|
+
function drawStages() {
|
|
44
|
+
const S = M.initiatives.stages;
|
|
45
|
+
if (!S.length) return '<p class="empty3 plain">This board has no workflow stages on record.</p>';
|
|
46
|
+
return `<div class="istg">${S.map((s) => `<div class="isc"><p class="label">${esc(s.stage)} <em>${s.count}</em></p>${s.rows.map((r) => `<button class="icd ibi" data-key="${esc(r.key)}"><b>${esc(r.title)}</b><small>${sub(r)}</small>${r.progress == null ? '' : `<span class="pg"><i><b style="width:${r.progress}%"></b></i></span>`}<small>${esc(r.finish)}</small></button>`).join('') || '<p class="mut">None</p>'}${s.more ? `<p class="mut">${s.more} more</p>` : ''}</div>`).join('')}</div>`;
|
|
47
|
+
}
|
|
48
|
+
function drawTime() {
|
|
49
|
+
const T = M.initiatives.timeline;
|
|
50
|
+
if (T.empty) return `<p class="empty3 plain">${esc(T.why)}</p>`;
|
|
51
|
+
return `<div class="tll"><div class="tlh" aria-hidden="true"><span>Initiative</span><span>${esc(T.from)} to ${esc(T.to)}</span><span></span></div><div class="tlgrid" aria-hidden="true"><span></span><div class="tlplot"><u class="td" style="left:${T.todayPct}%"></u></div><span></span></div>`
|
|
52
|
+
+ T.bars.map((b, k) => `<button class="tlr${b.bad ? ' bd' : ''}${b.over ? ' ov' : ''}" data-key="${esc(b.key)}" style="--k:${k}"><span class="tlt"><b>${esc(b.title)}</b><small>${esc(b.sub)}</small></span><span class="tlbar" role="img" aria-label="${esc(b.title)}: ${esc(b.start)} to ${esc(b.end)}"><span class="tlb" style="--l:${b.l}%;--w:${b.w}%"><i class="fl" style="--e:${b.elapsed}%"></i></span></span><span class="tld"><span>${esc(b.start)}</span><span>${esc(b.end)}</span></span><span class="tln"><b>${b.n}</b><small>${esc(b.word)}</small></span></button>`).join('')
|
|
53
|
+
+ `</div>${T.none ? `<p class="tlnone">${T.none} ${T.none === 1 ? 'initiative has' : 'initiatives have'} no dates on record and ${T.none === 1 ? 'is' : 'are'} not drawn.</p>` : ''}<p class="note" style="margin-top:10px">Each bar runs from a start date to an end date someone set. The filled part is the time elapsed. The line is today.</p>`;
|
|
54
|
+
}
|
|
55
|
+
function drawInit() {
|
|
56
|
+
const I = M.initiatives;
|
|
57
|
+
$('#iniCap').innerHTML = I.cap ? `<b>${I.cap.on} of ${I.cap.open}</b> open on track · ${I.risk} at risk · ${I.late} late${I.hold ? ' · ' + I.hold + ' on hold' : ''}` : 'no open initiatives';
|
|
58
|
+
const bar = $('#iniBar'); bar.hidden = !I.cap; if (I.cap) { bar.setAttribute('aria-valuemax', I.cap.open); bar.setAttribute('aria-valuenow', I.cap.on); $('b', bar).style.width = (I.cap.open ? I.cap.on / I.cap.open * 100 : 0) + '%'; }
|
|
59
|
+
$('#ibBody').innerHTML = view === 'list' ? drawList() : view === 'stages' ? drawStages() : drawTime();
|
|
60
|
+
$('#iniNote').textContent = I.note;
|
|
61
|
+
$$('#ibTabs button').forEach((b) => b.setAttribute('aria-pressed', b.dataset.v === view)); pillOf($('#ibTabs'));
|
|
62
|
+
}
|
|
63
|
+
$('#ibTabs').addEventListener('click', (e) => { const b = e.target.closest('[data-v]'); if (!b) return; view = b.dataset.v; drawInit(); });
|
|
64
|
+
$('#ibBody').addEventListener('click', (e) => {
|
|
65
|
+
if (e.target.closest('#more')) { shown += 25; drawInit(); return; }
|
|
66
|
+
const b = e.target.closest('[data-key]'); if (b) openIni(rowOf(b.dataset.key));
|
|
67
|
+
});
|
|
68
|
+
/* A click opens the initiative page, as the mockup's Page setting does; the Popup setting opens the dialog. The choice is kept in this browser. */
|
|
69
|
+
let openMode = store.get(MODE_KEY) === 'popup' ? 'popup' : 'page';
|
|
70
|
+
function drawMode() { $$('#modeSeg button').forEach((b) => b.setAttribute('aria-pressed', b.dataset.m === openMode)); pillOf($('#modeSeg')); }
|
|
71
|
+
$('#modeSeg').addEventListener('click', (e) => { const b = e.target.closest('[data-m]'); if (!b) return; openMode = b.dataset.m; store.set(MODE_KEY, openMode); drawMode(); toast('Initiatives now open as a ' + openMode + '.'); });
|
|
72
|
+
function openIni(r) {
|
|
73
|
+
if (!r) return;
|
|
74
|
+
if (openMode === 'popup' || !r.id) { openRow(r); return; }
|
|
75
|
+
location.assign('/premium-initiative.html?id=' + encodeURIComponent(r.id) + (M.board.ws ? '&ws=' + encodeURIComponent(M.board.ws) + '&board=' + encodeURIComponent(M.board.id) : ''));
|
|
76
|
+
}
|
|
77
|
+
const rowOf = (key) => (M && M.rows || []).find((r) => r.key === key) || null;
|
|
78
|
+
|
|
79
|
+
/* ── sprint, release, documents ─────────────────────────────────────────────────────────────────────────── */
|
|
80
|
+
function drawSide() {
|
|
81
|
+
const S = M.sprint;
|
|
82
|
+
$('#sprCap').textContent = S.cap || '';
|
|
83
|
+
$('#sprBody').innerHTML = S.name
|
|
84
|
+
? `<div class="spr"><div class="sph"><b>${esc(S.name)}</b><span class="mut">${esc([S.state, S.start && S.end ? S.start + ' to ' + S.end : ''].filter(Boolean).join(' · '))}</span></div>${S.goal ? `<p class="goal2"><b>Goal</b> ${esc(S.goal)}</p>` : ''}${S.summary ? `<p class="sum3">${esc(S.summary)}</p>` : ''}<div class="cnt"><span><b>${S.counts.done}</b> done</span><span><b>${S.counts.doing}</b> doing</span><span><b>${S.counts.todo}</b> to do</span></div></div>`
|
|
85
|
+
: `<p class="mut">${esc(S.why)}</p>`;
|
|
86
|
+
const R = M.release;
|
|
87
|
+
$('#relCap').textContent = R.cap || '';
|
|
88
|
+
$('#relBody').innerHTML = R.at ? `<div class="irow st"><time>${esc(R.at)}</time><span class="itx"><b>${esc(R.type ? R.type + ' release' : 'Release')}</b><small>${esc(R.cap)}</small></span></div>` : `<p class="empty3 plain">${esc(R.why)}</p>`;
|
|
89
|
+
}
|
|
90
|
+
function docRow(d) {
|
|
91
|
+
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('<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"/>')}${esc(d.act.label)}</button>` : `<button class="dmcta alt" data-tab="${esc(d.act.tab)}">${esc(d.act.label)}</button>`;
|
|
92
|
+
return `<div class="dmr" role="listitem"><span class="dmo"><span class="dmi c-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.cadence ? `<span>${ICO('<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>')}${esc(d.cadence)}</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></span><span class="dmq">${act}</span></div>`;
|
|
93
|
+
}
|
|
94
|
+
let docFilter = 'all', docQuery = '';
|
|
95
|
+
const draftable = () => (M.docs.rows || []).filter((d) => d.st === 'missing' && d.act && d.act.kind === 'draft' && !d.running);
|
|
96
|
+
const docMatch = (d) => (docFilter === 'all' || (docFilter === 'none' ? d.st === 'missing' : docFilter === 'draft' ? d.st === 'draft' : d.st === 'ok')) && (!docQuery.trim() || (d.title + ' ' + d.what).toLowerCase().includes(docQuery.trim().toLowerCase()));
|
|
97
|
+
function drawDocList() {
|
|
98
|
+
const D = M.docs, L = D.rows.filter(docMatch);
|
|
99
|
+
$('#docBody').innerHTML = L.length ? L.map(docRow).join('') : `<div class="dmem"><p>No document matches${docQuery.trim() ? ' “' + esc(docQuery.trim()) + '”' : ''}.</p></div>`;
|
|
100
|
+
$$('#dmF button').forEach((b) => b.setAttribute('aria-pressed', b.dataset.dmf === docFilter)); if ($('#dmF')) pillOf($('#dmF'));
|
|
101
|
+
}
|
|
102
|
+
function drawDocs() {
|
|
103
|
+
const D = M.docs, c = D.counts;
|
|
104
|
+
$('#docCap').textContent = D.rows.length ? String(c.all) : '';
|
|
105
|
+
if (!D.rows.length) { $('#docTop').innerHTML = ''; $('#docBody').innerHTML = `<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 board.'}</p>`; $('#docNote').textContent = ''; return; }
|
|
106
|
+
const dot = { ok: 'ok', draft: 'warn', missing: 'mut' };
|
|
107
|
+
const hadQ = $('#dmQ') && document.activeElement === $('#dmQ');
|
|
108
|
+
$('#docTop').innerHTML = `<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('<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"/>')}Draft the ${draftable().length} not started</button>` : ''}</p>
|
|
109
|
+
<div class="dprog" role="img" aria-label="${c.ok} of ${c.all} up to date">${D.rows.map((d) => `<i class="${dot[d.st] || (d.st === 'live' ? 'ok' : 'mut')}"></i>`).join('')}</div>
|
|
110
|
+
<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>
|
|
111
|
+
<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>`;
|
|
112
|
+
drawDocList();
|
|
113
|
+
if (hadQ) { const q = $('#dmQ'); q.focus(); q.setSelectionRange(q.value.length, q.value.length); }
|
|
114
|
+
$('#docNote').textContent = 'Draft it starts a read-only agent run that writes a draft for you to read. Nothing is sent. Issuing, dropping and adding a document are not built on this page.';
|
|
115
|
+
}
|
|
116
|
+
$('#docTop').addEventListener('click', async (e) => {
|
|
117
|
+
const f = e.target.closest('[data-dmf]'); if (f) { docFilter = f.dataset.dmf; drawDocList(); return; }
|
|
118
|
+
const all = e.target.closest('[data-draftall]'); if (!all || all.disabled) return;
|
|
119
|
+
all.disabled = true; let n = 0;
|
|
120
|
+
for (const d of draftable()) { 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; } }
|
|
121
|
+
if (n) toast('The agent is drafting ' + n + ' ' + (n === 1 ? 'document' : 'documents') + '. They run in the background and send nothing.');
|
|
122
|
+
await load();
|
|
123
|
+
});
|
|
124
|
+
$('#docTop').addEventListener('input', (e) => { if (e.target.id === 'dmQ') { docQuery = e.target.value; drawDocList(); } });
|
|
125
|
+
const GOTAB = { rep: '/c3.html?tab=rep', init: '/c3.html?tab=init' };
|
|
126
|
+
$('#docBody').addEventListener('click', async (e) => {
|
|
127
|
+
const t = e.target.closest('[data-tab]'); if (t) { location.assign(GOTAB[t.dataset.tab] || '/c3.html'); return; }
|
|
128
|
+
const b = e.target.closest('[data-draft]'); if (!b || b.disabled) return;
|
|
129
|
+
const key = b.dataset.draft, d = M.docs.rows.find((x) => x.key === key);
|
|
130
|
+
b.disabled = true;
|
|
131
|
+
const res = await pmPost('/api/core-docs/draft', { ws: M.board.ws, scope: M.docs.scope, key });
|
|
132
|
+
if (!res || res.ok !== true) { b.disabled = false; toast((res && res.error) || 'The draft could not be started.'); return; }
|
|
133
|
+
toast('The agent is drafting “' + (d ? d.title : key) + '”. It runs in the background and sends nothing.');
|
|
134
|
+
await load();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/* ── dock ───────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
138
|
+
function drawDock() { $('#dN').textContent = M.dock.needs; $('#dLab').textContent = M.dock.label; $('#dNext').innerHTML = M.dock.next ? `<i></i>First <b>${esc(M.dock.next)}</b>` : 'Nothing is late or at risk'; }
|
|
139
|
+
|
|
140
|
+
/* ── the dialog: the initiative's record, no press, the read-only agent beside it ──────────────────────────── */
|
|
141
|
+
function cardHtml(r) { return `<div class="acard" id="card"><p class="ct">What this page can do</p><p class="nodef" id="holdWhy">${esc(r.dialog.hold)}</p></div>`; }
|
|
142
|
+
function paneHtml(r) {
|
|
143
|
+
const p = r.dialog.pane;
|
|
144
|
+
return `<article class="paper doc">${p.file ? `<p class="dpath">${esc(p.file)}</p>` : ''}${p.rows.length ? `<div class="kv2">${p.rows.map(([a, b]) => `<span>${esc(a)}</span><span>${esc(b)}</span>`).join('')}</div>` : ''}<p class="xn">${esc(p.note || 'The initiative as the board holds it.')}</p></article>`;
|
|
145
|
+
}
|
|
146
|
+
function openRow(r) {
|
|
147
|
+
if (!r) return;
|
|
148
|
+
const d = r.dialog;
|
|
149
|
+
openDlg({ eye: d.eye + ' · real data', title: r.title, sub: r.sub + (r.statusLabel ? ' · ' + r.statusLabel : ''), say: d.say, stream: turnA(logHtml(d)) + turnA(ptx(d.say)) + cardHtml(r), noteKey: r.key, pane: paneHtml(r), label: d.pane.label });
|
|
150
|
+
loadThread(r.key);
|
|
151
|
+
setTimeout(() => { const b = $('#dBody'); b.scrollTop = b.scrollHeight; }, 120);
|
|
152
|
+
}
|
|
153
|
+
$('#dBody').addEventListener('click', (e) => { const lt = e.target.closest('#logT'); if (lt) { const c = $('#alog').classList.toggle('col'); lt.setAttribute('aria-expanded', !c); } });
|
|
154
|
+
|
|
155
|
+
/* ── load: the routes derive the initiatives in the background, so the first answers are "reading" ───────────── */
|
|
156
|
+
function draw() { drawMode(); drawHead(); drawInit(); drawSide(); drawDocs(); drawDock(); onScroll(); }
|
|
157
|
+
async function load() {
|
|
158
|
+
const p = new URLSearchParams(location.search), q = [p.get('ws') ? 'ws=' + encodeURIComponent(p.get('ws')) : '', p.get('board') ? 'board=' + encodeURIComponent(p.get('board')) : ''].filter(Boolean).join('&');
|
|
159
|
+
let r = null, status = 0;
|
|
160
|
+
try { const res = await fetch('/api/board-premium' + (q ? '?' + q : ''), { credentials: 'same-origin' }); status = res.status; r = await res.json(); } catch { r = null; }
|
|
161
|
+
clearInterval(readTimer);
|
|
162
|
+
if (r && r.state === 'reading') {
|
|
163
|
+
const t0 = Date.now(), tick = () => { $('#snap').textContent = 'Reading the board · ' + Math.round((Date.now() - t0) / 1000) + ' s so far'; };
|
|
164
|
+
tick(); readTimer = setInterval(tick, 1000);
|
|
165
|
+
if (!M) { $('#ibBody').innerHTML = '<p class="empty3 plain">The board derives each initiative from its records. This page shows nothing until it has read, rather than guess.</p>'; $('#docBody').innerHTML = ''; }
|
|
166
|
+
setTimeout(load, 3000);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!r || !r.ok) {
|
|
170
|
+
$('#snap').textContent = 'The board could not be read';
|
|
171
|
+
$('#brief').textContent = 'The board could not be read, so this page shows nothing rather than guess.' + (r && r.error ? ' The reason: ' + r.error : status ? ' (the server answered ' + status + ')' : ' The server did not answer.');
|
|
172
|
+
setTimeout(load, 15000);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
$('#brief').textContent = '';
|
|
176
|
+
M = r.model; draw();
|
|
177
|
+
if (M.docs.rows.some((d) => d.running)) setTimeout(load, 5000); // a core-document run is going: ask again until it ends
|
|
178
|
+
}
|
|
179
|
+
$('#brief').textContent = 'Reading the board.';
|
|
180
|
+
load();
|