create-pmos 0.46.0 → 0.48.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/AGENTS-HUMAN.md +1 -1
- package/payload/AGENTS.md +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
- package/payload/mods/pmos-cockpit/README.md +13 -3
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +34 -6
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +22 -3
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +16 -3
- package/payload/mods/pmos-cockpit/hooks/lib/handover-run.mjs +33 -0
- package/payload/mods/pmos-cockpit/hooks/lib/handover.mjs +267 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +189 -18
- package/payload/mods/pmos-cockpit/types/index.d.ts +16 -1
- package/payload/okf/core/concepts/frontmatter-format.md +6 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/pmos-log.sh +18 -8
- package/payload/scripts/pmos-model.js +233 -2
- package/payload/scripts/status-facts.js +204 -0
- package/payload/scripts/workflow-state.js +52 -0
- package/payload/skills/SKILL-FORMAT.md +1 -0
- package/payload/state/discoveries.md +1 -1
- package/workspace/README.md +3 -3
- package/workspace/bin/live-notes.js +133 -0
- package/workspace/design-parity.js +328 -21
- package/workspace/doc-category-bench.js +165 -0
- package/workspace/fixtures/doc-category-labelled.json +167 -0
- package/workspace/guide/initiatives-and-next-steps.md +16 -2
- package/workspace/guide/knowledge-graph.md +89 -2
- package/workspace/guide/layout.md +30 -2
- package/workspace/guide/proactive-engine.md +26 -4
- package/workspace/guide/scheduled-tasks-and-cli.md +1 -0
- package/workspace/guide/statuses.md +1 -0
- package/workspace/guide/validate-with-typesafe.md +17 -1
- package/workspace/lib/claims.js +51 -2
- package/workspace/lib/doc-category-corrections.js +113 -0
- package/workspace/lib/doc-category-writeback.js +110 -0
- package/workspace/lib/doc-category.js +225 -0
- package/workspace/lib/engine/pmos-model.js +233 -2
- package/workspace/lib/engine/status-facts.js +204 -0
- package/workspace/lib/engine/workflow-state.js +52 -0
- package/workspace/lib/jev-categorize.js +399 -0
- package/workspace/lib/jev-claims.js +59 -15
- package/workspace/lib/jev-route.js +129 -0
- package/workspace/lib/kg-cli.js +5 -2
- package/workspace/lib/kg-dangling-baseline.json +1 -0
- package/workspace/lib/kg-dangling-rules.json +1 -0
- package/workspace/lib/kg.js +26 -9
- package/workspace/lib/learn.js +282 -0
- package/workspace/lib/live-notes.js +890 -0
- package/workspace/lib/proactive-cli.js +49 -1
- package/workspace/lib/proactive.js +8 -7
- package/workspace/lib/typesafe-settings.js +10 -2
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +5 -4
- package/workspace/public/pm/app.js +148 -79
- package/workspace/public/pm/data.js +58 -0
- package/workspace/public/pm/fixture.js +40 -2
- package/workspace/public/pm/modes-live.js +267 -21
- package/workspace/public/pm/panel-cockpit.js +636 -0
- package/workspace/public/pm/panel-detail.js +210 -8
- package/workspace/public/pm/panel-initiatives.js +9 -9
- package/workspace/public/pm/panel-newsess.js +145 -86
- package/workspace/public/pm/panel-repos.js +34 -17
- package/workspace/public/pm/panel-tabs.js +145 -27
- package/workspace/public/pm/state.js +45 -4
- package/workspace/public/pm/tab-doc.js +244 -16
- package/workspace/public/pm/tab-term.js +11 -1
- package/workspace/public/pm/tokens.css +4 -3
- package/workspace/public/pm/view-addrepo.js +207 -0
- package/workspace/public/pm/view-files.js +197 -0
- package/workspace/public/pm/view-guide.js +141 -0
- package/workspace/public/pm/view-knowledge.js +225 -0
- package/workspace/public/pm/view-memory.js +122 -0
- package/workspace/public/pm/view-portfolio.js +153 -0
- package/workspace/public/pm/view-reports.js +190 -0
- package/workspace/public/pm/view-scheduled.js +161 -0
- package/workspace/public/pm/view-skills.js +216 -0
- package/workspace/public/pm/view-today.js +168 -0
- package/workspace/public/pm/view-validate.js +171 -0
- package/workspace/server.js +394 -10
- package/workspace/skills/live-notes.skill +58 -0
- package/workspace/skills/morning-brief.skill +1 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/* pm/view-memory.js — the Memory view, drawn from planning/design/cockpit-mockup/ViewMemory.dc.html.
|
|
2
|
+
*
|
|
3
|
+
* PRD workspace-cockpit-nav, Run 3 (R4). The board is a header with the actions on the right, a row of badges, the notes as
|
|
4
|
+
* cards and a closing note. The notes and every action are the view's existing list (modes-live.js `memoryLive`): this module
|
|
5
|
+
* draws it into a host, moves its action bar into the header and restyles its rows as cards. No node is removed, so every
|
|
6
|
+
* `data-r9` hook and every live behaviour stays as it was. Only the frame is new.
|
|
7
|
+
*
|
|
8
|
+
* The badges are read off the list once it is drawn, so they come from the same GET /api/memory as the notes and never
|
|
9
|
+
* disagree with them. The board's "used by N sessions" and the note kinds (preference, standard, lesson, fact) have no backing
|
|
10
|
+
* in this app, so they are not drawn. In the fixture the prototype's list is drawn under the header, unchanged, and the badges
|
|
11
|
+
* are not read.
|
|
12
|
+
*/
|
|
13
|
+
import { el } from './dom.js';
|
|
14
|
+
|
|
15
|
+
const BTN = 'font:500 12.5px var(--sans);background:transparent;color:var(--foreground);border:1px solid var(--input);border-radius:8px;padding:0 12px;height:30px;min-height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none';
|
|
16
|
+
const BTNP = 'font:500 12.5px var(--sans);background:var(--foreground);color:var(--background);border:0;border-radius:8px;padding:0 12px;height:30px;min-height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none';
|
|
17
|
+
const SMALL = 'font:500 11.5px var(--sans);background:transparent;color:var(--muted-foreground);border:1px solid var(--border);border-radius:6px;padding:0 9px;height:24px;min-height:24px;display:inline-grid;place-items:center;cursor:pointer';
|
|
18
|
+
const BADGE = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;white-space:nowrap;display:inline-block';
|
|
19
|
+
const TONE = {
|
|
20
|
+
s: ';border-color:transparent;background:color-mix(in oklab, var(--success) 16%, transparent);color:var(--success)',
|
|
21
|
+
w: ';border-color:transparent;background:color-mix(in oklab, var(--warning) 16%, transparent);color:var(--warning)',
|
|
22
|
+
d: ';border-color:transparent;background:color-mix(in oklab, var(--destructive) 18%, transparent);color:var(--destructive)',
|
|
23
|
+
};
|
|
24
|
+
const ENTRY = 'display:flex;flex-direction:column;gap:6px;width:100%;box-sizing:border-box;text-align:left;border:1px solid var(--border);border-radius:10px;padding:12px 14px;background:var(--background);min-height:0';
|
|
25
|
+
const LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground);padding:8px 0 0';
|
|
26
|
+
const NOTE = 'color:var(--muted-foreground);font-size:13px;line-height:1.5';
|
|
27
|
+
|
|
28
|
+
const badge = (text, tone, key) => el('span', { style: BADGE + (tone ? TONE[tone] : ''), 'data-board-badge': key }, String(text));
|
|
29
|
+
|
|
30
|
+
/** The badges row. It is read off the list this module just drew, so the badges and the notes always come from the same read
|
|
31
|
+
* of GET /api/memory and agree after every action. Before that read succeeds the list states its own status, so no badge is
|
|
32
|
+
* drawn. Nothing is shown that the read does not hold. */
|
|
33
|
+
function badges(list) {
|
|
34
|
+
if (!list.querySelector('[data-r9="usage-memory"]')) return null;
|
|
35
|
+
const wire = list.querySelector('[data-r9="wire"]');
|
|
36
|
+
const appr = list.querySelector('[data-r9="approval"]');
|
|
37
|
+
const wired = !!wire && /wired \u2713/.test(wire.textContent || '');
|
|
38
|
+
const n = list.querySelectorAll('[data-r9="entry"]').length;
|
|
39
|
+
const pending = list.querySelectorAll('[data-r9="pending"]').length;
|
|
40
|
+
const approval = !!appr && /approval on/.test(appr.textContent || '');
|
|
41
|
+
return el('div', { 'data-board-part': 'badges', style: 'display:flex;gap:6px;flex-wrap:wrap' },
|
|
42
|
+
wired ? badge('wired into CLAUDE.md', 's', 'wired') : badge('not wired into CLAUDE.md', 'w', 'wired'),
|
|
43
|
+
badge(n + (n === 1 ? ' note' : ' notes'), null, 'count'),
|
|
44
|
+
badge('memory.jsonl \u00b7 append-only', null, 'ledger'),
|
|
45
|
+
pending ? badge(pending + ' waiting for you', 'w', 'pending') : null,
|
|
46
|
+
badge('agent notes ' + (approval ? 'wait for your approval' : 'apply at once'), null, 'approval'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Restyle the existing list in place: its bar goes into the header, its rows become cards. Nothing is removed. */
|
|
50
|
+
function restyle(list, actions) {
|
|
51
|
+
const bar = list.querySelector('[data-r9="memory-bar"]');
|
|
52
|
+
// the list's wrapper, when it drew one (a failed or pending read returns its parts unwrapped)
|
|
53
|
+
const first = list.firstElementChild;
|
|
54
|
+
const wrap = first && bar && first !== bar && first.contains(bar) ? first : null;
|
|
55
|
+
if (bar) {
|
|
56
|
+
bar.setAttribute('style', 'display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:flex-end');
|
|
57
|
+
for (const b of bar.querySelectorAll('button')) {
|
|
58
|
+
const k = b.getAttribute('data-r9');
|
|
59
|
+
b.setAttribute('style', k === 'add' ? BTNP : BTN);
|
|
60
|
+
}
|
|
61
|
+
// the board's order: the secondary actions first, the primary action last
|
|
62
|
+
const add = bar.querySelector('[data-r9="add"]');
|
|
63
|
+
if (add) bar.appendChild(add);
|
|
64
|
+
actions.appendChild(bar);
|
|
65
|
+
}
|
|
66
|
+
if (wrap) {
|
|
67
|
+
wrap.setAttribute('style', 'display:flex;flex-direction:column;gap:10px');
|
|
68
|
+
// the list's closing line has no hook; give it the board's note style
|
|
69
|
+
for (const k of wrap.children) if (!k.hasAttribute('data-r9')) k.setAttribute('style', NOTE);
|
|
70
|
+
}
|
|
71
|
+
for (const row of list.querySelectorAll('[data-r9="entry"],[data-r9="elsewhere"],[data-r9="pending"]')) {
|
|
72
|
+
row.setAttribute('style', ENTRY);
|
|
73
|
+
row.setAttribute('data-board-card', row.getAttribute('data-r9'));
|
|
74
|
+
const [text, meta, btns] = row.children;
|
|
75
|
+
if (text) text.setAttribute('style', 'display:block;font-size:13px;line-height:1.5;color:var(--foreground)');
|
|
76
|
+
if (meta) meta.setAttribute('style', 'display:block;font:11px var(--mono);color:var(--muted-foreground)');
|
|
77
|
+
if (btns) {
|
|
78
|
+
btns.setAttribute('style', 'display:flex;gap:6px;flex-wrap:wrap');
|
|
79
|
+
for (const b of btns.querySelectorAll('button')) b.setAttribute('style', SMALL);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const lab of list.querySelectorAll('[data-r9^="usage-"],[data-r9$="-label"]')) {
|
|
83
|
+
const tone = lab.getAttribute('data-r9') === 'blocked-label' ? ';color:var(--destructive)' : '';
|
|
84
|
+
lab.setAttribute('style', LBL + tone);
|
|
85
|
+
}
|
|
86
|
+
for (const note of list.querySelectorAll('[data-r9="empty"],[data-r9="blocked"]')) note.setAttribute('style', NOTE);
|
|
87
|
+
const memNote = list.querySelector('[data-r9="note"]');
|
|
88
|
+
if (memNote) memNote.setAttribute('style', NOTE + ';color:var(--destructive)');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function renderBoard(inner, ctx) {
|
|
92
|
+
const { FIXTURE, state } = ctx;
|
|
93
|
+
const repo = state.product || null;
|
|
94
|
+
|
|
95
|
+
const actions = el('div', { 'data-board-part': 'toolbar', style: 'display:flex;gap:8px;flex-wrap:wrap;align-items:center' });
|
|
96
|
+
const header = el('div', { 'data-board-part': 'header', style: 'display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap' },
|
|
97
|
+
el('div', null,
|
|
98
|
+
el('div', { 'data-board-title': '', style: 'font:600 22px var(--sans);line-height:1.45' }, 'Memory'),
|
|
99
|
+
el('div', { 'data-board-subtitle': '', style: 'color:var(--muted-foreground)' },
|
|
100
|
+
'Curated notes agents read before they work in ' + (repo || 'this repository'))),
|
|
101
|
+
actions);
|
|
102
|
+
|
|
103
|
+
const list = el('div', { 'data-board-section': 'notes', style: 'display:flex;flex-direction:column;gap:10px;max-width:900px' });
|
|
104
|
+
ctx.renderMode(list);
|
|
105
|
+
|
|
106
|
+
let part = null; let foot = null;
|
|
107
|
+
if (FIXTURE) part = el('div', { 'data-board-part': 'badges', style: NOTE }, 'The repository memory is not read in the fixture.');
|
|
108
|
+
else {
|
|
109
|
+
// read the badges and the approval setting before the bar moves into the header
|
|
110
|
+
part = badges(list);
|
|
111
|
+
const appr = list.querySelector('[data-r9="approval"]');
|
|
112
|
+
if (part) {
|
|
113
|
+
foot = el('div', { 'data-board-part': 'note', style: NOTE }, appr && /approval on/.test(appr.textContent || '')
|
|
114
|
+
? 'You add notes here, and agents propose them. A note an agent proposes waits here until you approve it.'
|
|
115
|
+
: 'You add notes here, and agents add them too. Turn approval on to keep an agent\u2019s note pending until you approve it.');
|
|
116
|
+
}
|
|
117
|
+
restyle(list, actions);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
inner.appendChild(el('div', { 'data-board': 'memory', style: 'display:flex;flex-direction:column;gap:14px;padding:4px 6px;font-size:13px;line-height:1.5;color:var(--foreground)' },
|
|
121
|
+
header, part, list, foot));
|
|
122
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/* pm/view-portfolio.js — the All repos view, drawn from planning/design/cockpit-mockup/ViewPortfolio.dc.html.
|
|
2
|
+
*
|
|
3
|
+
* PRD workspace-cockpit-nav, Run 3 (R4). The board is a header, a row of counts, one table row per registered repository
|
|
4
|
+
* and a note. Below it this module places the view's existing list (modes-live.js `portfolioList`: the report bar, the
|
|
5
|
+
* coverage lines and the initiatives by domain), unchanged, so every `data-r4` / `data-r15` hook and every live behaviour
|
|
6
|
+
* stays as it was. Only the frame is new.
|
|
7
|
+
*
|
|
8
|
+
* Every number comes from GET /api/portfolio, the same read the list uses: a repository's initiatives are the rows the
|
|
9
|
+
* server returns for it. The board's "Need you" count and column have no cross-repository backing in this app, so they are
|
|
10
|
+
* not drawn and the note says so. "Last activity" has no backing either; the column shows when each repository's board was
|
|
11
|
+
* read instead, under that name. Nothing on this board is invented.
|
|
12
|
+
*/
|
|
13
|
+
import { el } from './dom.js';
|
|
14
|
+
import { repoTuples, selectRepo, onAddProduct } from './panel-repos.js';
|
|
15
|
+
import { repoRow } from './fixture.js';
|
|
16
|
+
|
|
17
|
+
const TTL = 15000;
|
|
18
|
+
let read = null; // { state: 'loading' | 'ok' | 'error', data, error, at }
|
|
19
|
+
|
|
20
|
+
/** The board's own read of /api/portfolio: one fetch at a time, read again after 15 seconds, like the list's. */
|
|
21
|
+
function portfolio(setState) {
|
|
22
|
+
const fresh = read && (read.state === 'loading' || Date.now() - read.at < TTL);
|
|
23
|
+
if (fresh) return read;
|
|
24
|
+
const prev = read;
|
|
25
|
+
read = { state: 'loading', data: prev ? prev.data : null, at: prev ? prev.at : 0 };
|
|
26
|
+
fetch('/api/portfolio').then((r) => r.json()).then(
|
|
27
|
+
(data) => { read = { state: data && data.ok === false ? 'error' : 'ok', data, error: data && data.error, at: Date.now() }; setState({}); },
|
|
28
|
+
(e) => { read = { state: 'error', data: null, error: (e && e.message) || String(e), at: Date.now() }; setState({}); },
|
|
29
|
+
);
|
|
30
|
+
return read;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
|
|
34
|
+
const BTN = 'font:500 12.5px var(--sans);background:transparent;color:var(--foreground);border:1px solid var(--input);border-radius:8px;padding:0 12px;height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none';
|
|
35
|
+
const BADGE = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;white-space:nowrap;display:inline-block';
|
|
36
|
+
const TONE = {
|
|
37
|
+
w: ';border-color:transparent;background:color-mix(in oklab, var(--warning) 16%, transparent);color:var(--warning)',
|
|
38
|
+
d: ';border-color:transparent;background:color-mix(in oklab, var(--destructive) 18%, transparent);color:var(--destructive)',
|
|
39
|
+
};
|
|
40
|
+
const TH = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground);text-align:left;padding:10px 14px;border-bottom:1px solid var(--border);font-weight:500';
|
|
41
|
+
const TD = 'padding:12px 14px;border-bottom:1px solid var(--border);font-size:13px';
|
|
42
|
+
const NOTE = 'color:var(--muted-foreground);font-size:13px;line-height:1.45';
|
|
43
|
+
|
|
44
|
+
const badge = (text, tone) => el('span', { style: BADGE + (tone ? TONE[tone] : '') }, String(text));
|
|
45
|
+
|
|
46
|
+
function stat(label, value, color, key) {
|
|
47
|
+
return el('div', { 'data-board-stat': key, style: 'flex:1 1 0;min-width:0;border:1px solid var(--border);background:var(--card);border-radius:10px;padding:12px 14px' },
|
|
48
|
+
el('div', { style: 'color:var(--muted-foreground)' }, label),
|
|
49
|
+
el('div', { 'data-board-value': key, style: 'font:600 26px var(--sans);line-height:1.45' + (color ? ';color:' + color : '') }, String(value)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Per repository: its rows, counted the way the board's columns say. A row with no derived board has no stage. */
|
|
53
|
+
function countsOf(name, rows) {
|
|
54
|
+
const mine = rows.filter((x) => x.repo === name);
|
|
55
|
+
return {
|
|
56
|
+
all: mine.length,
|
|
57
|
+
progress: mine.filter((x) => !x.accepted && x.stage).length,
|
|
58
|
+
blocked: mine.filter((x) => x.blocked === true).length,
|
|
59
|
+
accepted: mine.filter((x) => x.accepted === true).length,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** What the "Board read" cell says: fresh with the time, stale, pending, or not read with the reason. */
|
|
64
|
+
function readCell(rp) {
|
|
65
|
+
const t = String(rp.as_of || '').slice(11, 16);
|
|
66
|
+
if (rp.error) return el('span', { style: 'color:var(--destructive)', title: String(rp.error) }, 'not read');
|
|
67
|
+
if (rp.pending) return el('span', { style: 'color:var(--warning)' }, 'pending');
|
|
68
|
+
if (rp.stale) return el('span', { style: 'color:var(--warning)' }, 'stale' + (t ? ' · ' + t : ''));
|
|
69
|
+
return el('span', { style: 'color:var(--muted-foreground)' }, 'fresh' + (t ? ' · ' + t : ''));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function table(pf, ctx) {
|
|
73
|
+
const rows = pf.rows || [];
|
|
74
|
+
const kinds = new Map();
|
|
75
|
+
try { for (const t of repoTuples()) kinds.set(t[0], repoRow(t).kindLabel); } catch { /* no rail yet: kind stays empty */ }
|
|
76
|
+
const open = (name) => { selectRepo(name); ctx.setState({}); };
|
|
77
|
+
const head = ['Repository', 'Kind', 'Initiatives', 'In progress', 'Blocked', 'Accepted', 'Board read', ''];
|
|
78
|
+
const body = (pf.repos || []).map((rp) => {
|
|
79
|
+
const c = countsOf(rp.name, rows);
|
|
80
|
+
const staged = !rp.error && !rp.pending;
|
|
81
|
+
return el('tr', { 'data-board-repo': rp.name, onClick: () => open(rp.name), style: 'cursor:pointer', hover: 'background:var(--accent)', title: 'open the initiatives of ' + rp.name },
|
|
82
|
+
el('td', { style: TD }, el('b', null, rp.name), rp.domain ? el('span', { style: 'color:var(--muted-foreground);margin-left:6px' }, rp.domain) : null),
|
|
83
|
+
el('td', { style: TD }, kinds.get(rp.name) ? badge(kinds.get(rp.name)) : el('span', { style: 'color:var(--muted-foreground)' }, '—')),
|
|
84
|
+
el('td', { style: TD, 'data-board-col': 'initiatives' }, rp.error ? '—' : String(c.all)),
|
|
85
|
+
// a board that is not derived yet has no stages: say nothing rather than a confident zero
|
|
86
|
+
el('td', { style: TD, 'data-board-col': 'progress' }, staged ? String(c.progress) : '—'),
|
|
87
|
+
el('td', { style: TD, 'data-board-col': 'blocked' }, staged ? (c.blocked ? badge(c.blocked, 'd') : '0') : '—'),
|
|
88
|
+
el('td', { style: TD, 'data-board-col': 'accepted' }, staged ? String(c.accepted) : '—'),
|
|
89
|
+
el('td', { style: TD, 'data-board-col': 'read' }, readCell(rp)),
|
|
90
|
+
el('td', { style: TD + ';text-align:right' }, el('button', { style: BTN, hover: 'background:var(--accent)', 'data-board-action': 'open-repo',
|
|
91
|
+
onClick: (e) => { e.stopPropagation(); open(rp.name); } }, 'Open')));
|
|
92
|
+
});
|
|
93
|
+
return el('div', { 'data-board-section': 'repos', style: 'border:1px solid var(--border);border-radius:10px;overflow:hidden;overflow-x:auto' },
|
|
94
|
+
el('table', { style: 'width:100%;border-collapse:collapse' },
|
|
95
|
+
el('thead', null, el('tr', null, head.map((h) => el('th', { style: TH }, h)))),
|
|
96
|
+
el('tbody', null, body.length ? body
|
|
97
|
+
: el('tr', null, el('td', { colspan: String(head.length), style: TD + ';color:var(--muted-foreground)' }, 'No repository is registered. Add one to see it here.')))));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function renderBoard(inner, ctx) {
|
|
101
|
+
const { FIXTURE, setState } = ctx;
|
|
102
|
+
const r = FIXTURE ? null : portfolio(setState);
|
|
103
|
+
const pf = (r && r.data) || null;
|
|
104
|
+
const rows = (pf && pf.rows) || [];
|
|
105
|
+
|
|
106
|
+
const addRepo = () => {
|
|
107
|
+
// the rail's add tile opens the same thing; when the Add repository view exists, open it there instead
|
|
108
|
+
import('./panel-cockpit.js').then((m) => { if (m.VIEWS && m.VIEWS.addrepo) ctx.openView('addrepo'); else onAddProduct(); })
|
|
109
|
+
.catch(() => onAddProduct());
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const header = el('div', { 'data-board-part': 'header', style: 'display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap' },
|
|
113
|
+
el('div', null,
|
|
114
|
+
el('div', { 'data-board-title': '', style: 'font:600 22px var(--sans);line-height:1.45' }, 'All repos'),
|
|
115
|
+
el('div', { style: 'color:var(--muted-foreground)' }, 'Every registered repository, side by side')),
|
|
116
|
+
el('button', { style: BTN, hover: 'background:var(--accent)', 'data-board-action': 'add-repo', onClick: addRepo }, '+ Add repository'));
|
|
117
|
+
|
|
118
|
+
let summary = null; let reposPart = null;
|
|
119
|
+
if (FIXTURE) {
|
|
120
|
+
reposPart = el('div', { 'data-board-section': 'repos', style: NOTE }, 'The portfolio is not read in the fixture.');
|
|
121
|
+
} else if (!pf && r.state === 'error') {
|
|
122
|
+
reposPart = el('div', { 'data-board-section': 'repos', style: NOTE + ';color:var(--destructive)' }, 'The repositories could not be read: ' + (r.error || 'error') + '.');
|
|
123
|
+
} else if (!pf) {
|
|
124
|
+
reposPart = el('div', { 'data-board-section': 'repos', style: NOTE }, 'Reading the repositories …');
|
|
125
|
+
} else {
|
|
126
|
+
const blocked = rows.filter((x) => x.blocked === true).length;
|
|
127
|
+
// in flight = known not accepted; a row whose board is not derived yet is counted apart, never as in flight
|
|
128
|
+
const unknown = rows.filter((x) => x.accepted === null || x.accepted === undefined).length;
|
|
129
|
+
const unread = (pf.repos || []).filter((x) => x.error || x.pending).length;
|
|
130
|
+
summary = el('div', { 'data-board-part': 'summary', style: 'display:flex;gap:14px;flex-wrap:wrap' },
|
|
131
|
+
stat('Repositories', (pf.repos || []).length, null, 'repos'),
|
|
132
|
+
stat('Initiatives in flight', rows.filter((x) => x.accepted === false).length, null, 'inflight'),
|
|
133
|
+
stat('Blocked by a gate', blocked, blocked ? 'var(--destructive)' : null, 'blocked'));
|
|
134
|
+
if (unknown || unread) {
|
|
135
|
+
summary = [summary, el('div', { 'data-board-part': 'unread', style: NOTE + ';margin-top:-8px' },
|
|
136
|
+
(unread ? unread + (unread === 1 ? ' repository is' : ' repositories are') + ' not fully read. ' : '')
|
|
137
|
+
+ (unknown ? unknown + (unknown === 1 ? ' initiative has' : ' initiatives have') + ' no derived stage yet and ' + (unknown === 1 ? 'is' : 'are') + ' not counted above.' : ''))];
|
|
138
|
+
}
|
|
139
|
+
reposPart = table(pf, ctx);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const note = el('div', { 'data-board-part': 'note', style: NOTE },
|
|
143
|
+
'Click a row to open that repository\'s initiative list. Counts come from each repository\'s own records. What needs you is not read across repositories here; see Today.');
|
|
144
|
+
|
|
145
|
+
// the existing list, kept whole: the report bar, coverage and the initiatives by domain
|
|
146
|
+
const list = el('div', { 'data-board-section': 'initiatives', style: 'border:1px solid var(--border);border-radius:10px;overflow:hidden;display:flex;flex-direction:column' });
|
|
147
|
+
ctx.renderMode(list);
|
|
148
|
+
|
|
149
|
+
inner.appendChild(el('div', { 'data-board': 'portfolio', style: 'display:flex;flex-direction:column;gap:16px;padding:4px 6px;font-size:13px;line-height:1.45;color:var(--foreground)' },
|
|
150
|
+
header, summary, reposPart, note,
|
|
151
|
+
el('div', { style: LBL + ';margin-top:6px' }, 'Initiatives by domain'),
|
|
152
|
+
list));
|
|
153
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/* pm/view-reports.js — the Reports view tab, drawn from planning/design/cockpit-mockup/ViewReports.dc.html.
|
|
2
|
+
*
|
|
3
|
+
* PRD workspace-cockpit-nav, Run 3 (R5, done-criterion 2). Reports is a new view with no list mode. The board is a header, a row
|
|
4
|
+
* of five report cards (Daily, Weekly, Bi-weekly, Monthly, Special), a History table and a closing note.
|
|
5
|
+
*
|
|
6
|
+
* Every value comes from a read the server already serves:
|
|
7
|
+
* - History is the report files in the repository: the `notes` trees of GET /api/library under a reports folder
|
|
8
|
+
* (`pmos/state/reports`, `planning/reports`, `reports`). The report skills (report-daily … report-special) write their drafts
|
|
9
|
+
* there, one file per period. The kind is the sub-folder, the period is the file's date. Definitions are not reports, so
|
|
10
|
+
* they are counted, not listed. A row opens the file as a document tab.
|
|
11
|
+
* - A card's schedule is the repository's report schedule in GET /api/schedules?all=1. With one, "Generate" runs it now
|
|
12
|
+
* through POST /api/schedules/run (the Scheduled view's "run now"). With none, the card says so and "Set up" opens the
|
|
13
|
+
* Scheduled view, where the report schedules and the special report are set up.
|
|
14
|
+
* The board's "Generated" and "State" columns ("draft, not sent", "marked sent by you") have no backing in this app, so they
|
|
15
|
+
* are not drawn. In the fixture nothing is read, and the board says so.
|
|
16
|
+
*/
|
|
17
|
+
import { el } from './dom.js';
|
|
18
|
+
|
|
19
|
+
const BTN = 'font:500 12.5px var(--sans);background:transparent;color:var(--foreground);border:1px solid var(--input);border-radius:8px;padding:0 12px;height:30px;min-height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none;white-space:nowrap';
|
|
20
|
+
const BTNP = 'font:500 12.5px var(--sans);background:var(--foreground);color:var(--background);border:0;border-radius:8px;padding:0 12px;height:30px;min-height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none;white-space:nowrap';
|
|
21
|
+
const BADGE = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;white-space:nowrap;display:inline-block';
|
|
22
|
+
const CARD = 'border:1px solid var(--border);background:var(--background);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:6px;flex:1 1 0;min-width:150px;box-sizing:border-box';
|
|
23
|
+
const TH = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground);text-align:left;padding:10px 14px;border-bottom:1px solid var(--border)';
|
|
24
|
+
const TD = 'padding:11px 14px;border-bottom:1px solid var(--border);font-size:13px;vertical-align:middle';
|
|
25
|
+
const NOTE = 'color:var(--muted-foreground);font-size:13px;line-height:1.5';
|
|
26
|
+
|
|
27
|
+
/** The five cards. `skill` is the report skill the card runs; `covers` is what that skill says it covers. */
|
|
28
|
+
export const REPORT_KINDS = [
|
|
29
|
+
{ key: 'daily', label: 'Daily', skill: 'report-daily', covers: 'Since the previous daily draft' },
|
|
30
|
+
{ key: 'weekly', label: 'Weekly', skill: 'report-weekly', covers: 'The last 7 days' },
|
|
31
|
+
{ key: 'biweekly', label: 'Bi-weekly', skill: 'report-biweekly', covers: 'The last 14 days' },
|
|
32
|
+
{ key: 'monthly', label: 'Monthly', skill: 'report-monthly', covers: 'The previous calendar month' },
|
|
33
|
+
{ key: 'special', label: 'Special', skill: 'report-special', covers: 'One-off, you say what it is for' },
|
|
34
|
+
];
|
|
35
|
+
const KIND_OF_DIR = { daily: 'daily', weekly: 'weekly', biweekly: 'biweekly', 'bi-weekly': 'biweekly', monthly: 'monthly', special: 'special' };
|
|
36
|
+
const REPORT_DIR = /^(?:pmos\/state\/reports|planning\/reports|reports)$/;
|
|
37
|
+
|
|
38
|
+
/* ── reads: one per key, read again after 15 seconds, never in a loop ─────────────────────────── */
|
|
39
|
+
const reads = new Map(); // key -> { state: 'loading'|'ok'|'error', data, error, at }
|
|
40
|
+
function read(ctx, key, url) {
|
|
41
|
+
const hit = reads.get(key);
|
|
42
|
+
if (hit && (hit.state === 'loading' || Date.now() - hit.at < 15000)) return hit;
|
|
43
|
+
const next = { state: hit ? hit.state : 'loading', data: hit ? hit.data : null, error: hit ? hit.error : null, at: Date.now() };
|
|
44
|
+
reads.set(key, next);
|
|
45
|
+
fetch(url).then((r) => r.json()).then(
|
|
46
|
+
(d) => { reads.set(key, { state: d && d.ok === false ? 'error' : 'ok', data: d, error: d && d.error, at: Date.now() }); },
|
|
47
|
+
(e) => { reads.set(key, { state: 'error', data: null, error: (e && e.message) || String(e), at: Date.now() }); },
|
|
48
|
+
).finally(() => ctx.setState({}));
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let dataMod = null; let dataTried = false; // the data layer, imported once for wsOf()
|
|
53
|
+
let note = null; // { tone: 'ok'|'bad', text } — what the last button did
|
|
54
|
+
|
|
55
|
+
/** Every report file under the repository's report folders, newest first. Definitions are counted apart. */
|
|
56
|
+
export function reportFiles(notes) {
|
|
57
|
+
const rows = []; let definitions = 0;
|
|
58
|
+
const walk = (node, base) => {
|
|
59
|
+
for (const d of node.docs || []) {
|
|
60
|
+
const rel = String(d.path || '');
|
|
61
|
+
const under = rel.slice(base.length + 1).split('/');
|
|
62
|
+
const sub = under.length > 1 ? under[0] : '';
|
|
63
|
+
if (sub === 'definitions') { definitions++; continue; }
|
|
64
|
+
const stem = String(d.name || rel.split('/').pop()).replace(/\.[^.]+$/, '');
|
|
65
|
+
const date = /^\d{4}-\d{2}-\d{2}/.exec(stem);
|
|
66
|
+
rows.push({ path: rel, name: d.name || stem, kind: KIND_OF_DIR[sub.toLowerCase()] || null, folder: sub || base, period: date ? date[0] : null, stem });
|
|
67
|
+
}
|
|
68
|
+
for (const s of node.dirs || []) walk(s, base);
|
|
69
|
+
};
|
|
70
|
+
for (const t of notes || []) if (REPORT_DIR.test(String(t.path || ''))) walk(t, String(t.path));
|
|
71
|
+
rows.sort((a, b) => String(b.period || '').localeCompare(String(a.period || '')) || a.path.localeCompare(b.path));
|
|
72
|
+
return { rows, definitions };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const kindLabel = (k) => { const r = REPORT_KINDS.find((x) => x.key === k); return r ? r.label + ' report' : 'Report'; };
|
|
76
|
+
const when = (s) => {
|
|
77
|
+
if (!s) return '';
|
|
78
|
+
if (s.cadence === 'once') return 'once ' + (s.date || '') + ' ' + (s.time || '');
|
|
79
|
+
const c = String(s.cadence || '');
|
|
80
|
+
const on = c === 'weekly' || c === 'bi-weekly' || c === 'biweekly' ? s.day : c === 'monthly' && s.dayOfMonth ? 'day ' + s.dayOfMonth : null;
|
|
81
|
+
return [c, on, s.time].filter(Boolean).join(' ');
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
async function generate(ctx, s, label) {
|
|
85
|
+
note = { tone: 'ok', text: 'starting the ' + label.toLowerCase() + ' report …' }; ctx.setState({});
|
|
86
|
+
let out;
|
|
87
|
+
try {
|
|
88
|
+
out = await fetch('/api/schedules/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: s.id }) }).then((r) => r.json());
|
|
89
|
+
} catch (e) { out = { ok: false, error: (e && e.message) || String(e) }; }
|
|
90
|
+
const r = out && out.result;
|
|
91
|
+
if (!out || out.ok === false) note = { tone: 'bad', text: (out && out.error) || 'the server did not answer. Nothing was started.' };
|
|
92
|
+
else if (r && r.ok === false) note = { tone: 'bad', text: 'Not started: ' + (r.reason || 'the server gave no reason') };
|
|
93
|
+
else if (r && r.sessionId) {
|
|
94
|
+
note = { tone: 'ok', text: label + ' report started as a session. It is open in the tabs. The draft is filed in this repository.' };
|
|
95
|
+
try { const ini = await import('./panel-initiatives.js'); ini.openTerm(null, { id: r.sessionId, name: s.name || label + ' report', st: 'working', profile: null }); } catch { /* the note says where it went */ }
|
|
96
|
+
} else note = { tone: 'ok', text: label + ' report ran. The result is in Needs you.' };
|
|
97
|
+
reads.delete('schedules');
|
|
98
|
+
ctx.setState({});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function openReport(ws, rel) {
|
|
102
|
+
const ini = await import('./panel-initiatives.js');
|
|
103
|
+
ini.openDoc(null, rel, 'FILE', ws ? { ws, branch: null, linked: true } : null);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function card(ctx, k, sched, ws) {
|
|
107
|
+
const parts = [el('b', { 'data-board-card-title': '' }, k.label), el('div', { style: 'color:var(--muted-foreground)' }, k.covers)];
|
|
108
|
+
let btn;
|
|
109
|
+
if (k.key === 'special') {
|
|
110
|
+
parts.push(el('div', { 'data-board-card-sched': '', style: 'font:11.5px var(--mono);color:var(--muted-foreground)' }, 'set up in Scheduled'));
|
|
111
|
+
btn = el('button', { 'data-board-action': 'describe', style: BTN + ';align-self:flex-start', hover: 'background:var(--accent)', title: 'opens the Scheduled view, where you describe a special report and when it runs', onClick: () => ctx.openView('scheduled') }, 'Describe it');
|
|
112
|
+
} else if (sched === undefined) {
|
|
113
|
+
parts.push(el('div', { 'data-board-card-sched': '', style: 'font:11.5px var(--mono);color:var(--muted-foreground)' }, ws ? 'reading the schedules …' : 'no repository selected'));
|
|
114
|
+
} else if (sched) {
|
|
115
|
+
parts.push(el('div', { 'data-board-card-sched': '', style: 'font:11.5px var(--mono);color:var(--muted-foreground)' }, 'scheduled · ' + when(sched)));
|
|
116
|
+
btn = el('button', { 'data-board-action': 'generate', style: BTNP + ';align-self:flex-start', title: 'runs this repository\'s ' + k.skill + ' schedule now', onClick: () => generate(ctx, sched, k.label) }, 'Generate');
|
|
117
|
+
} else {
|
|
118
|
+
parts.push(el('div', { 'data-board-card-sched': '', style: 'font:11.5px var(--mono);color:var(--muted-foreground)' }, 'not scheduled here'));
|
|
119
|
+
btn = el('button', { 'data-board-action': 'setup', style: BTN + ';align-self:flex-start', hover: 'background:var(--accent)', title: 'opens the Scheduled view, where the report schedules are set up', onClick: () => ctx.openView('scheduled') }, 'Set up');
|
|
120
|
+
}
|
|
121
|
+
if (btn) parts.push(btn);
|
|
122
|
+
return el('div', { 'data-board-card': k.key, style: CARD }, parts);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function renderBoard(inner, ctx) {
|
|
126
|
+
const { FIXTURE } = ctx;
|
|
127
|
+
const repo = (ctx.tab && ctx.tab.repo) || ctx.repo || null;
|
|
128
|
+
let ws = null;
|
|
129
|
+
if (!FIXTURE) {
|
|
130
|
+
// the data layer maps the repository to its workspace id; it is imported once, and the board redraws when it is there
|
|
131
|
+
if (!dataTried) { dataTried = true; import('./data.js').then((m) => { dataMod = m; ctx.setState({}); }).catch(() => {}); }
|
|
132
|
+
if (dataMod && dataMod.wsOf) ws = dataMod.wsOf(repo);
|
|
133
|
+
if (!ws && ctx.state && ctx.state.product === repo) ws = ctx.state.wsId || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// the report schedules for this repository, per skill: undefined = not read yet, null = none
|
|
137
|
+
const schedOf = {};
|
|
138
|
+
let lib = null;
|
|
139
|
+
if (!FIXTURE && ws) {
|
|
140
|
+
const s = read(ctx, 'schedules', '/api/schedules?all=1');
|
|
141
|
+
const list = s.state === 'ok' && s.data && Array.isArray(s.data.schedules) ? s.data.schedules : null;
|
|
142
|
+
for (const k of REPORT_KINDS) schedOf[k.key] = list ? (list.find((x) => x.ws === ws && (x.skill === k.skill || x.task === k.skill)) || null) : undefined;
|
|
143
|
+
lib = read(ctx, 'library:' + ws, '/api/library?ws=' + encodeURIComponent(ws));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const header = el('div', { 'data-board-part': 'header' },
|
|
147
|
+
el('div', { 'data-board-title': '', style: 'font:600 22px var(--sans);line-height:1.45' }, 'Reports'),
|
|
148
|
+
el('div', { 'data-board-subtitle': '', style: 'color:var(--muted-foreground)' },
|
|
149
|
+
'A skill reads the ledgers in a session and writes a draft. Nothing is sent from here.'));
|
|
150
|
+
|
|
151
|
+
const cards = el('div', { 'data-board-part': 'cards', style: 'display:flex;gap:12px;flex-wrap:wrap' },
|
|
152
|
+
REPORT_KINDS.map((k) => (FIXTURE
|
|
153
|
+
? el('div', { 'data-board-card': k.key, style: CARD }, el('b', null, k.label), el('div', { style: 'color:var(--muted-foreground)' }, k.covers))
|
|
154
|
+
: card(ctx, k, schedOf[k.key], ws))));
|
|
155
|
+
|
|
156
|
+
const noteEl = note ? el('div', { 'data-board-part': 'run-note', style: NOTE + (note.tone === 'bad' ? ';color:var(--destructive)' : '') }, note.text) : null;
|
|
157
|
+
|
|
158
|
+
// History
|
|
159
|
+
let body; let count = null;
|
|
160
|
+
if (FIXTURE) body = el('div', { 'data-board-empty': '', style: NOTE + ';padding:14px' }, 'No reports are read here yet. The fixture reads no repository.');
|
|
161
|
+
else if (!ws) body = el('div', { 'data-board-empty': '', style: NOTE + ';padding:14px' }, 'Select a repository to see its reports.');
|
|
162
|
+
else if (!lib || (lib.state === 'loading' && !lib.data)) body = el('div', { style: NOTE + ';padding:14px' }, 'Reading the reports …');
|
|
163
|
+
else if (lib.state === 'error') body = el('div', { style: NOTE + ';padding:14px;color:var(--destructive)' }, 'The reports could not be read: ' + (lib.error || 'error'));
|
|
164
|
+
else {
|
|
165
|
+
const { rows, definitions } = reportFiles(lib.data && lib.data.notes);
|
|
166
|
+
count = { n: rows.length, definitions };
|
|
167
|
+
if (!rows.length) {
|
|
168
|
+
body = el('div', { 'data-board-empty': '', style: NOTE + ';padding:14px' },
|
|
169
|
+
'No report has been written in this repository yet. A report skill files its draft under pmos/state/reports/ or planning/reports/.');
|
|
170
|
+
} else {
|
|
171
|
+
body = el('table', { style: 'width:100%;border-collapse:collapse' },
|
|
172
|
+
el('thead', null, el('tr', null, ['Report', 'Period', 'File', ''].map((h) => el('th', { style: TH }, h)))),
|
|
173
|
+
el('tbody', null, rows.map((r) => el('tr', { 'data-board-report': r.path },
|
|
174
|
+
el('td', { style: TD }, el('b', null, kindLabel(r.kind))),
|
|
175
|
+
el('td', { style: TD }, r.period || el('span', { style: 'color:var(--muted-foreground)' }, 'no date in the name')),
|
|
176
|
+
el('td', { style: TD + ';font:12px var(--mono);color:var(--muted-foreground);overflow-wrap:anywhere' }, r.path),
|
|
177
|
+
el('td', { style: TD + ';text-align:right' }, el('button', { 'data-board-action': 'open', style: BTN, hover: 'background:var(--accent)', onClick: () => openReport(ws, r.path) }, 'Open'))))));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const historyHead = el('div', { style: 'display:flex;align-items:center;gap:10px' },
|
|
181
|
+
el('div', { style: 'font-weight:600' }, 'History'),
|
|
182
|
+
count ? el('span', { 'data-board-badge': 'count', style: BADGE }, count.n + (count.n === 1 ? ' report' : ' reports')) : null,
|
|
183
|
+
count && count.definitions ? el('span', { 'data-board-badge': 'definitions', style: BADGE }, count.definitions + (count.definitions === 1 ? ' definition' : ' definitions')) : null);
|
|
184
|
+
const history = el('div', { 'data-board-section': 'history', style: 'border:1px solid var(--border);border-radius:10px;overflow:hidden;background:var(--background)' }, body);
|
|
185
|
+
|
|
186
|
+
inner.appendChild(el('div', { 'data-board': 'reports', style: 'display:flex;flex-direction:column;gap:16px;padding:4px 6px;font-size:13px;line-height:1.45;color:var(--foreground)' },
|
|
187
|
+
header, cards, noteEl, historyHead, history,
|
|
188
|
+
el('div', { 'data-board-part': 'note', style: NOTE }, 'A report opens as a document tab. PMOS does not send reports, and it cannot see what you sent elsewhere.')));
|
|
189
|
+
}
|
|
190
|
+
|