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
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();
|
|
@@ -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();
|