create-pmos 0.52.0 → 0.53.1
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/mods/pmos-cockpit/README.md +13 -0
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
- package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/plan-items-stage.js +271 -0
- package/payload/scripts/pmos-log.sh +18 -7
- package/payload/scripts/workflow-state.js +28 -2
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/README.md +2 -0
- package/workspace/REMOTE.md +24 -63
- package/workspace/bin/pmos.js +19 -3
- package/workspace/cut-stub.js +40 -0
- package/workspace/first-paint-audit.js +145 -0
- package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
- package/workspace/fixtures/initiatives-api/list.json +176 -0
- package/workspace/fixtures/initiatives-api/post-403.json +5 -0
- package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
- package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
- package/workspace/fixtures/initiatives-api/sheet.json +112 -0
- package/workspace/fixtures/report-api/draft.json +177 -0
- package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
- package/workspace/fixtures/today-api/provider-rows.json +204 -0
- package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
- package/workspace/guide/board.md +81 -0
- package/workspace/guide/breakdown.md +154 -0
- package/workspace/guide/command-palette.md +75 -0
- package/workspace/guide/index.md +28 -17
- package/workspace/guide/initiatives-roadmap.md +130 -0
- package/workspace/guide/initiatives-screen.md +89 -0
- package/workspace/guide/initiatives-timeline.md +57 -0
- package/workspace/guide/integrated-work.md +94 -0
- package/workspace/guide/knowledge-graph.md +3 -68
- package/workspace/guide/launcher.md +51 -0
- package/workspace/guide/layout.md +102 -32
- package/workspace/guide/live-notes.md +3 -3
- package/workspace/guide/pm-only-acts.md +89 -0
- package/workspace/guide/pm-only-lines.md +61 -0
- package/workspace/guide/proactive-engine.md +21 -5
- package/workspace/guide/reports-page.md +80 -0
- package/workspace/guide/reports.md +73 -0
- package/workspace/guide/timeline.md +70 -0
- package/workspace/guide/today.md +85 -0
- package/workspace/guide/validate-with-typesafe.md +1 -1
- package/workspace/lib/actor.js +91 -0
- package/workspace/lib/board-cache.js +32 -2
- package/workspace/lib/board-cli.js +108 -0
- package/workspace/lib/board-fold.js +232 -0
- package/workspace/lib/breakdown-export.js +75 -0
- package/workspace/lib/brief.js +20 -2
- package/workspace/lib/capture.js +98 -0
- package/workspace/lib/engine/plan-items-stage.js +271 -0
- package/workspace/lib/engine/workflow-state.js +28 -2
- package/workspace/lib/factory-join.js +158 -0
- package/workspace/lib/goals.js +165 -0
- package/workspace/lib/grant-mint.js +113 -0
- package/workspace/lib/health-rules-factory.js +92 -0
- package/workspace/lib/health-rules-plan.js +164 -0
- package/workspace/lib/health.js +79 -0
- package/workspace/lib/initiative-cli.js +222 -0
- package/workspace/lib/initiative-display.js +183 -0
- package/workspace/lib/initiative-needs.js +127 -0
- package/workspace/lib/initiative-rows.js +210 -0
- package/workspace/lib/initiative-writer.js +141 -0
- package/workspace/lib/issues.js +206 -0
- package/workspace/lib/item-cli.js +195 -0
- package/workspace/lib/item-spans.js +137 -0
- package/workspace/lib/launcher-cli.js +207 -0
- package/workspace/lib/measure-cli.js +100 -0
- package/workspace/lib/measurements.js +189 -0
- package/workspace/lib/mobile-nav.js +57 -53
- package/workspace/lib/needs-providers.js +120 -0
- package/workspace/lib/placements.js +440 -0
- package/workspace/lib/plan-items.js +922 -0
- package/workspace/lib/plan-ledgers.js +85 -0
- package/workspace/lib/plan-rules.js +72 -0
- package/workspace/lib/plan-tree.js +380 -0
- package/workspace/lib/pm-guard.js +174 -0
- package/workspace/lib/pm-lines-needs.js +331 -0
- package/workspace/lib/pmos-verbs.js +69 -0
- package/workspace/lib/proactive-cli.js +58 -2
- package/workspace/lib/proactive.js +9 -6
- package/workspace/lib/qr.js +293 -0
- package/workspace/lib/remote-access.js +93 -1
- package/workspace/lib/report-cli.js +152 -0
- package/workspace/lib/report-draft.js +578 -0
- package/workspace/lib/report-needs.js +213 -0
- package/workspace/lib/route-modules.js +128 -0
- package/workspace/lib/routes-board.js +91 -0
- package/workspace/lib/routes-capture.js +52 -0
- package/workspace/lib/routes-initiatives.js +371 -0
- package/workspace/lib/routes-reports.js +163 -0
- package/workspace/lib/routes-timeline.js +197 -0
- package/workspace/lib/routes-today.js +32 -0
- package/workspace/lib/today-cli.js +99 -0
- package/workspace/lib/today.js +333 -0
- package/workspace/lib/triage.js +163 -0
- package/workspace/lib/verify-work-cli.js +81 -0
- package/workspace/lib/work-check-needs.js +99 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +4 -5
- package/workspace/public/icons/apple-touch-icon.png +0 -0
- package/workspace/public/icons/icon-192.png +0 -0
- package/workspace/public/icons/icon-512.png +0 -0
- package/workspace/public/index.html +40 -19
- package/workspace/public/pm/app.js +330 -30
- package/workspace/public/pm/bd-board.js +113 -0
- package/workspace/public/pm/bd-drag.js +59 -0
- package/workspace/public/pm/bd-model.js +152 -0
- package/workspace/public/pm/bd-timeline.js +233 -0
- package/workspace/public/pm/bd-tree.js +244 -0
- package/workspace/public/pm/board-copy.js +57 -0
- package/workspace/public/pm/breakdown-api.js +58 -0
- package/workspace/public/pm/breakdown-copy.js +138 -0
- package/workspace/public/pm/components.css +3135 -0
- package/workspace/public/pm/cycle-review.js +125 -0
- package/workspace/public/pm/data.js +40 -187
- package/workspace/public/pm/dest-board.js +438 -0
- package/workspace/public/pm/dest-initiatives.js +134 -0
- package/workspace/public/pm/dest-reports.js +270 -0
- package/workspace/public/pm/dest-settings.js +217 -0
- package/workspace/public/pm/dest-today.js +182 -0
- package/workspace/public/pm/dialogs.js +88 -0
- package/workspace/public/pm/display-word.js +56 -0
- package/workspace/public/pm/dock.js +175 -0
- package/workspace/public/pm/fixture.js +8 -227
- package/workspace/public/pm/frame-copy.js +99 -0
- package/workspace/public/pm/handoff-box.js +82 -0
- package/workspace/public/pm/http.js +88 -0
- package/workspace/public/pm/icons.js +53 -0
- package/workspace/public/pm/initiative-list.js +218 -0
- package/workspace/public/pm/initiative-sheet.js +261 -0
- package/workspace/public/pm/initiatives-api.js +59 -0
- package/workspace/public/pm/initiatives-copy.js +215 -0
- package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
- package/workspace/public/pm/initiatives-timeline.js +410 -0
- package/workspace/public/pm/initiatives-ui.js +123 -0
- package/workspace/public/pm/item-sheet.js +251 -0
- package/workspace/public/pm/keep-card.js +204 -0
- package/workspace/public/pm/lifecycle.js +7 -1
- package/workspace/public/pm/mobile.css +49 -20
- package/workspace/public/pm/mobile.js +157 -29
- package/workspace/public/pm/modes-live.js +7 -883
- package/workspace/public/pm/modes.js +32 -1024
- package/workspace/public/pm/new-initiative-sheet.js +74 -0
- package/workspace/public/pm/next-step-ui.js +3 -2
- package/workspace/public/pm/palette-copy.js +51 -0
- package/workspace/public/pm/palette.js +312 -0
- package/workspace/public/pm/panel-breakdown.js +702 -0
- package/workspace/public/pm/panel-cockpit.js +63 -295
- package/workspace/public/pm/panel-detail.js +25 -172
- package/workspace/public/pm/panel-initiatives.js +1 -19
- package/workspace/public/pm/panel-repos.js +31 -81
- package/workspace/public/pm/panel-tabs.js +200 -111
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/portfolio-report.js +61 -0
- package/workspace/public/pm/primitives.js +656 -0
- package/workspace/public/pm/print.css +33 -0
- package/workspace/public/pm/provider-cards.js +136 -0
- package/workspace/public/pm/registry.js +194 -0
- package/workspace/public/pm/release-card.js +110 -0
- package/workspace/public/pm/report-view.js +309 -0
- package/workspace/public/pm/reports-copy.js +114 -0
- package/workspace/public/pm/roadmap.js +271 -0
- package/workspace/public/pm/sig-watch.js +41 -0
- package/workspace/public/pm/state.js +8 -4
- package/workspace/public/pm/status-banner.js +13 -5
- package/workspace/public/pm/tab-doc.js +2 -4
- package/workspace/public/pm/tab-term.js +12 -2
- package/workspace/public/pm/tab-web.js +3 -2
- package/workspace/public/pm/timeline-copy.js +48 -0
- package/workspace/public/pm/today-cards.js +244 -0
- package/workspace/public/pm/today-copy.js +126 -0
- package/workspace/public/pm/today-goals.js +172 -0
- package/workspace/public/pm/tokens.css +16 -18
- package/workspace/public/pm/topbar.js +102 -0
- package/workspace/public/pm/triage-card.js +60 -0
- package/workspace/public/pm/undo-hold.js +103 -0
- package/workspace/public/pm/view-addrepo.js +2 -1
- package/workspace/public/pm/view-livenotes.js +3 -1
- package/workspace/public/pm/view-today.js +7 -162
- package/workspace/public/vendor/Sortable.min.js +2 -0
- package/workspace/public/vendor/lucide-sprite.js +79 -0
- package/workspace/replaced-tests.json +2877 -0
- package/workspace/run-tests.js +56 -5
- package/workspace/server.js +446 -40
- package/workspace/sheet-answer.js +61 -0
- package/workspace/skills/measure.skill +41 -0
- package/workspace/skills/morning-brief.skill +15 -9
- package/workspace/skills/report-biweekly.skill +23 -25
- package/workspace/skills/tracker-refresh.skill +50 -0
- package/workspace/test-files.json +21 -3
- package/workspace/test-lists/base.json +6 -0
- package/workspace/test-lists/run2-a.json +6 -0
- package/workspace/test-lists/run2-b.json +13 -0
- package/workspace/test-lists/run2-c.json +19 -0
- package/workspace/test-lists/run2-d.json +22 -0
- package/workspace/test-lists/run3-0.json +5 -0
- package/workspace/test-lists/run3-a.json +5 -0
- package/workspace/test-lists/run3-b.json +6 -0
- package/workspace/test-lists/run3-c.json +15 -0
- package/workspace/test-lists/run3-d.json +6 -0
- package/workspace/test-lists/run4-a.json +8 -0
- package/workspace/test-lists/run4-b.json +6 -0
- package/workspace/test-lists/run4-c.json +13 -0
- package/workspace/test-lists/run4-d.json +9 -0
- package/workspace/test-lists/run5-a.json +6 -0
- package/workspace/test-lists/run5-b.json +6 -0
- package/workspace/test-lists/run5-c.json +15 -0
- package/workspace/test-lists/workspace-launcher.json +5 -0
- package/workspace/test-lists/workspace-phone-app.json +5 -0
- package/workspace/test-run.js +27 -1
- package/workspace/third-party/README.md +44 -0
- package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
- package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
- package/workspace/design-parity.js +0 -673
- package/workspace/public/pm/tab-task.js +0 -86
- package/workspace/public/pm/view-guide.js +0 -141
- package/workspace/public/pm/view-knowledge.js +0 -225
- package/workspace/public/pm/view-memory.js +0 -122
- package/workspace/public/pm/view-portfolio.js +0 -153
- package/workspace/public/pm/view-reports.js +0 -190
- package/workspace/public/pm/view-scheduled.js +0 -161
- package/workspace/public/pm/view-skills.js +0 -216
|
@@ -1,153 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/* pm/view-scheduled.js — the Scheduled view tab, drawn from planning/design/cockpit-mockup/ViewScheduled.dc.html.
|
|
2
|
-
*
|
|
3
|
-
* PRD workspace-cockpit-nav, Run 3 (R4). The board is a header, a table of schedules (Task, Skill, Cadence, Next run, Last run,
|
|
4
|
-
* On, and the actions) and a closing note.
|
|
5
|
-
*
|
|
6
|
-
* The rows are the view's existing list (modes-live.js `scheduledLive`): each `data-r4="schedule"` row is laid out as a table
|
|
7
|
-
* row in place, and its own "run now" and "delete" buttons sit in the last column, so every `data-r4`, `data-r7` and `data-r11`
|
|
8
|
-
* hook and every live behaviour stays as it was. The list's set-up buttons (the recommended schedules and the special report)
|
|
9
|
-
* move into the board's toolbar, and its run note moves under it. No node is removed. The row's "next … · last …" line is
|
|
10
|
-
* split into the Next run and Last run columns; the line itself stays in the row, hidden, as the source of both.
|
|
11
|
-
*
|
|
12
|
-
* The Skill column and the tone of the Last run badge come from the board's own read of GET /api/schedules?all=1, the read
|
|
13
|
-
* the list uses. A row is matched by its name and its last run, so a stale read shows no skill and a plain badge rather than
|
|
14
|
-
* a wrong one. The board's on/off switch is drawn as a plain on/off badge: the list has no control that turns a schedule on
|
|
15
|
-
* or off, so the board does not offer one. The board's "+ New schedule" has no form in this app, so it is not drawn. In the
|
|
16
|
-
* fixture the prototype's list is drawn inside the table frame, unchanged.
|
|
17
|
-
*/
|
|
18
|
-
import { el } from './dom.js';
|
|
19
|
-
|
|
20
|
-
let sched = { key: null, data: null, busy: false }; // this board's read of /api/schedules?all=1
|
|
21
|
-
|
|
22
|
-
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';
|
|
23
|
-
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';
|
|
24
|
-
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;flex:none';
|
|
25
|
-
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';
|
|
26
|
-
const TONE = {
|
|
27
|
-
s: ';border-color:transparent;background:color-mix(in oklab, var(--success) 16%, transparent);color:var(--success)',
|
|
28
|
-
w: ';border-color:transparent;background:color-mix(in oklab, var(--warning) 16%, transparent);color:var(--warning)',
|
|
29
|
-
d: ';border-color:transparent;background:color-mix(in oklab, var(--destructive) 18%, transparent);color:var(--destructive)',
|
|
30
|
-
};
|
|
31
|
-
const LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
|
|
32
|
-
const NOTE = 'color:var(--muted-foreground);font-size:13px;line-height:1.5;padding:0';
|
|
33
|
-
const COLS = ['Task', 'Skill', 'Cadence', 'Next run', 'Last run', 'On', ''];
|
|
34
|
-
const GRID = 'display:grid;grid-template-columns:minmax(110px,1.5fr) minmax(90px,1fr) minmax(90px,1fr) minmax(90px,1fr) minmax(100px,1fr) 52px 168px;align-items:center;width:100%;box-sizing:border-box;text-align:left';
|
|
35
|
-
const CELL = 'padding:12px 12px;font-size:13px;grid-row:1;min-width:0;overflow-wrap:anywhere';
|
|
36
|
-
const TH = LBL + ';padding:10px 12px;grid-row:1';
|
|
37
|
-
|
|
38
|
-
const badge = (text, tone, attrs) => el('span', { ...(attrs || {}), style: BADGE + (tone ? TONE[tone] : '') }, String(text));
|
|
39
|
-
const stamp = (v) => (v ? String(v).slice(0, 16).replace('T', ' ') : null);
|
|
40
|
-
const nameOf = (s) => String(s.label || s.name || s.task || s.skill || s.id || '');
|
|
41
|
-
|
|
42
|
-
/** The board's own read of the schedules. It is read again only when the rows change (a new, removed or re-run schedule), so a
|
|
43
|
-
* redraw never loops on it. */
|
|
44
|
-
function schedules(ctx, key) {
|
|
45
|
-
if (ctx.FIXTURE) return null;
|
|
46
|
-
if (sched.key !== key && !sched.busy) {
|
|
47
|
-
sched.key = key; sched.busy = true;
|
|
48
|
-
fetch('/api/schedules?all=1').then((r) => r.json()).then(
|
|
49
|
-
(d) => { sched.data = (d && d.schedules) || null; },
|
|
50
|
-
() => { sched.data = null; },
|
|
51
|
-
).finally(() => { sched.busy = false; ctx.setState({}); });
|
|
52
|
-
}
|
|
53
|
-
return sched.data;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Lay out one existing row as a table row. `s` is the matching schedule from the board's read, or null. */
|
|
57
|
-
function restyleRow(row, s) {
|
|
58
|
-
row.setAttribute('style', GRID + ';border-bottom:1px solid var(--border)');
|
|
59
|
-
const [top, line, btns] = row.children;
|
|
60
|
-
let name = null; let cadence = null;
|
|
61
|
-
if (top) {
|
|
62
|
-
top.setAttribute('style', 'display:contents');
|
|
63
|
-
[name, cadence] = top.children;
|
|
64
|
-
if (name) { name.setAttribute('style', CELL + ';grid-column:1;font-weight:600;color:var(--foreground)'); name.setAttribute('data-board-cell', 'task'); }
|
|
65
|
-
if (cadence) { cadence.setAttribute('style', CELL + ';grid-column:3;color:var(--foreground)'); cadence.setAttribute('data-board-cell', 'cadence'); }
|
|
66
|
-
}
|
|
67
|
-
const text = line ? (line.textContent || '') : '';
|
|
68
|
-
const m = /^next (.*) · last (.*)$/.exec(text.trim());
|
|
69
|
-
const next = m ? m[1] : ''; const last = m ? m[2] : '';
|
|
70
|
-
if (line) { line.style.display = 'none'; line.setAttribute('data-board-source', 'next-last'); }
|
|
71
|
-
const off = !!cadence && (cadence.textContent || '').trim() === 'disabled';
|
|
72
|
-
|
|
73
|
-
// the Last run badge: its tone comes from the read only when the read agrees with the row about the last run
|
|
74
|
-
const agrees = s && (stamp(s.lastRun) || 'never') === last;
|
|
75
|
-
let lastEl;
|
|
76
|
-
if (!m) lastEl = el('span', null, '');
|
|
77
|
-
else if (last === 'never') lastEl = el('span', { style: 'color:var(--muted-foreground)' }, 'never');
|
|
78
|
-
else if (agrees && s.lastStatus === 'missed') lastEl = badge('missed · last ' + last, 'w', { 'data-board-badge': 'last' });
|
|
79
|
-
else if (agrees && s.lastStatus === 'fail') lastEl = badge('failed ' + last, 'd', { 'data-board-badge': 'last' });
|
|
80
|
-
else if (agrees && (s.lastStatus === 'ok' || s.lastStatus === 'started')) lastEl = badge('ran ' + last, 's', { 'data-board-badge': 'last' });
|
|
81
|
-
else lastEl = badge(last, null, { 'data-board-badge': 'last' });
|
|
82
|
-
|
|
83
|
-
row.appendChild(el('span', { 'data-board-cell': 'skill', style: CELL + ';grid-column:2;font:12px var(--mono);color:var(--foreground)' }, s ? String(s.skill || s.task || '') : ''));
|
|
84
|
-
row.appendChild(el('span', { 'data-board-cell': 'next', style: CELL + ';grid-column:4;color:var(--foreground)' }, next));
|
|
85
|
-
row.appendChild(el('span', { 'data-board-cell': 'last', style: CELL + ';grid-column:5' }, lastEl));
|
|
86
|
-
row.appendChild(el('span', { 'data-board-cell': 'on', style: CELL + ';grid-column:6' },
|
|
87
|
-
badge(off ? 'off' : 'on', off ? null : 's', { 'data-board-badge': 'on', title: off ? 'this schedule is turned off' : 'this schedule is on' })));
|
|
88
|
-
if (btns) {
|
|
89
|
-
btns.setAttribute('style', CELL + ';grid-column:7;display:flex;gap:6px;justify-content:flex-end;align-items:center');
|
|
90
|
-
btns.setAttribute('data-board-cell', 'actions');
|
|
91
|
-
for (const b of btns.querySelectorAll('button')) b.setAttribute('style', b.getAttribute('data-r4') === 'run' ? BTN : SMALL);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** The list's set-up rows (the recommended schedules and the special report) as the board's toolbar buttons. */
|
|
96
|
-
function restyleSetup(n) {
|
|
97
|
-
n.setAttribute('style', 'display:flex;flex-direction:column;gap:6px;align-items:flex-start;max-width:100%');
|
|
98
|
-
for (const b of n.querySelectorAll('button')) b.setAttribute('style', BTN);
|
|
99
|
-
for (const c of n.children) if (c.tagName === 'SPAN') c.setAttribute('style', 'font-size:12px;line-height:1.45;color:var(--muted-foreground);max-width:420px');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function renderBoard(inner, ctx) {
|
|
103
|
-
const { FIXTURE } = ctx;
|
|
104
|
-
const drawn = el('div', null);
|
|
105
|
-
ctx.renderMode(drawn);
|
|
106
|
-
|
|
107
|
-
const actions = el('div', { 'data-board-part': 'actions', style: 'display:flex;gap:8px;flex-wrap:wrap;align-items:center' });
|
|
108
|
-
const setup = el('div', { 'data-board-part': 'toolbar', style: 'display:flex;gap:10px;flex-wrap:wrap;align-items:flex-start' });
|
|
109
|
-
const notes = el('div', { style: 'display:flex;flex-direction:column;gap:6px' });
|
|
110
|
-
const frame = el('div', { 'data-board-section': 'schedules', style: 'border:1px solid var(--border);border-radius:10px;overflow-x:auto;overflow-y:hidden;background:var(--background)' });
|
|
111
|
-
|
|
112
|
-
let count = null;
|
|
113
|
-
if (FIXTURE) {
|
|
114
|
-
// the prototype's rows, unchanged; its "+ schedule a task" is the board's primary action
|
|
115
|
-
const add = [...drawn.querySelectorAll('button')].find((b) => /schedule a task/.test(b.textContent || ''));
|
|
116
|
-
if (add) { add.setAttribute('style', BTNP); actions.appendChild(add); }
|
|
117
|
-
frame.appendChild(el('div', { style: 'padding:4px 0' }, [...drawn.childNodes]));
|
|
118
|
-
} else {
|
|
119
|
-
const wrap = drawn.children.length === 1 && !drawn.firstElementChild.hasAttribute('data-r4') ? drawn.firstElementChild : null;
|
|
120
|
-
const rows = wrap ? [...wrap.querySelectorAll(':scope > [data-r4="schedule"]')] : [];
|
|
121
|
-
if (wrap) {
|
|
122
|
-
for (const k of [...wrap.children]) {
|
|
123
|
-
if (k.hasAttribute('data-r7') || k.getAttribute('data-r11') === 'special') { restyleSetup(k); setup.appendChild(k); }
|
|
124
|
-
else if (k.getAttribute('data-r4') === 'run-note') { k.setAttribute('style', NOTE + (/destructive/.test(k.getAttribute('style') || '') ? ';color:var(--destructive)' : '')); notes.appendChild(k); }
|
|
125
|
-
else if (!k.hasAttribute('data-r4')) k.setAttribute('style', NOTE + ';padding:14px');
|
|
126
|
-
}
|
|
127
|
-
wrap.setAttribute('style', 'display:flex;flex-direction:column');
|
|
128
|
-
const key = rows.map((r) => (r.textContent || '')).join('\n');
|
|
129
|
-
const data = schedules(ctx, key) || [];
|
|
130
|
-
const used = new Set();
|
|
131
|
-
for (const row of rows) {
|
|
132
|
-
const nm = ((row.children[0] && row.children[0].children[0]) || {}).textContent || '';
|
|
133
|
-
const s = data.find((x, i) => !used.has(i) && nameOf(x) === nm.trim() && (used.add(i), true)) || null;
|
|
134
|
-
restyleRow(row, s);
|
|
135
|
-
}
|
|
136
|
-
count = rows.length;
|
|
137
|
-
if (rows.length) frame.appendChild(el('div', { 'data-board-part': 'columns', style: GRID + ';border-bottom:1px solid var(--border)' },
|
|
138
|
-
COLS.map((c, i) => el('span', { style: TH + ';grid-column:' + (i + 1) }, c))));
|
|
139
|
-
frame.appendChild(wrap);
|
|
140
|
-
} else {
|
|
141
|
-
// the read is in progress or failed: the list's own status line, in the frame
|
|
142
|
-
for (const k of [...drawn.children]) k.setAttribute('style', NOTE + ';padding:14px' + (/destructive/.test(k.getAttribute('style') || '') ? ';color:var(--destructive)' : ''));
|
|
143
|
-
frame.appendChild(el('div', null, [...drawn.childNodes]));
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (count !== null) actions.appendChild(badge(count + (count === 1 ? ' schedule' : ' schedules') + ' · all repositories', null, { 'data-board-badge': 'count' }));
|
|
147
|
-
|
|
148
|
-
const header = el('div', { 'data-board-part': 'header', style: 'display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap' },
|
|
149
|
-
el('div', null,
|
|
150
|
-
el('div', { 'data-board-title': '', style: 'font:600 22px var(--sans);line-height:1.45' }, 'Scheduled tasks'),
|
|
151
|
-
el('div', { 'data-board-subtitle': '', style: 'color:var(--muted-foreground)' },
|
|
152
|
-
'Skills that run on a cadence on this machine, while the workspace runs. A missed slot is skipped, never caught up.')),
|
|
153
|
-
actions);
|
|
154
|
-
|
|
155
|
-
const setupPart = setup.children.length ? el('div', { style: 'display:flex;flex-direction:column;gap:8px' },
|
|
156
|
-
el('div', { style: LBL }, 'Set up'), setup) : null;
|
|
157
|
-
|
|
158
|
-
inner.appendChild(el('div', { 'data-board': 'scheduled', style: 'display:flex;flex-direction:column;gap:14px;padding:4px 6px;font-size:13px;line-height:1.45;color:var(--foreground)' },
|
|
159
|
-
header, setupPart, notes.children.length ? notes : null, frame,
|
|
160
|
-
el('div', { 'data-board-part': 'note', style: NOTE }, 'Times are this machine’s. A task’s result waits for you in Needs you.')));
|
|
161
|
-
}
|