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
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/* pm/report-view.js — a report as the PM reads it, and the acts on it (pm-os-absorb Run 5, slice 5C; PRD R9, R22.5, R25).
|
|
2
|
+
*
|
|
3
|
+
* One module for what Reports (dest-reports.js) and the Today cards share, so Issue works the same from either place:
|
|
4
|
+
* parseReport(md) the draft's markdown as blocks, split at the note markers (classes only, no inline style)
|
|
5
|
+
* renderReport(md) those blocks as DOM nodes; a note shows as its own text and nothing is built from a string
|
|
6
|
+
* noteSlots(md) [{ id, title, text }] the notes the PM can edit, in the order they appear
|
|
7
|
+
* markdownToCopy(md) the markdown without its frontmatter and note markers, for Copy as markdown
|
|
8
|
+
* copyMarkdown(md) put that on the clipboard, then toast
|
|
9
|
+
* printReport(md) draw the report into `body > .rp-print` and call window.print(); print.css shows only it
|
|
10
|
+
* openIssueSheet(args) the two-tap Issue: the sheet reads the draft, the PM presses Issue, the file is frozen
|
|
11
|
+
* openNotesSheet(args) one sheet for every note of the draft
|
|
12
|
+
* draftNow(args) POST /api/report/draft, then toast
|
|
13
|
+
*
|
|
14
|
+
* Today cards (registerTodayCard): `report-issue` (kind 1) opens the Issue sheet; `schedule-no-output` (kind 8) is Draft now.
|
|
15
|
+
* The `cycle-kill-check` row (kind 7) is drawn by the initiatives run's card for kind 7, not here.
|
|
16
|
+
*
|
|
17
|
+
* Nothing on this page sends a report anywhere: the only things a report leaves through are the PM's own Print / PDF and
|
|
18
|
+
* Copy as markdown. No `by` is sent; the server takes the actor from the call path (D86 point 2).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { el } from './dom.js';
|
|
22
|
+
import { button, field, sheet, toast, card, badge, ensureComponentsCss, SEL } from './primitives.js';
|
|
23
|
+
import { ensureSprite } from './icons.js';
|
|
24
|
+
import { pmGet, pmPost } from './http.js';
|
|
25
|
+
import { registerTodayCard } from './registry.js';
|
|
26
|
+
import { COPY } from './reports-copy.js';
|
|
27
|
+
|
|
28
|
+
/* ── the markdown subset the drafter writes ───────────────────────────────────────────────────────────────────────── */
|
|
29
|
+
|
|
30
|
+
const NOTE_RE = /<!-- pmos:note ([a-z0-9][a-z0-9._-]*) -->\n?([\s\S]*?)\n?<!-- \/pmos:note -->/g;
|
|
31
|
+
const MARK = '\u0001NOTE:';
|
|
32
|
+
const FRONT = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/;
|
|
33
|
+
|
|
34
|
+
const stripFront = (md) => String(md || '').replace(FRONT, '').replace(/\r\n/g, '\n');
|
|
35
|
+
const cells = (ln) => ln.trim().replace(/^\|/, '').replace(/\|$/, '').split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, '|'));
|
|
36
|
+
const isRule = (ln) => /^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(ln.trim());
|
|
37
|
+
const bare = (h) => String(h || '').replace(/\s*\([a-z0-9._-]+\)\s*$/, '');
|
|
38
|
+
|
|
39
|
+
/** The title a note carries in the notes sheet: its headline or asks name, else the initiative heading above it. */
|
|
40
|
+
function noteTitle(id, heading) {
|
|
41
|
+
if (id === 'report.headline') return COPY.noteHeadline;
|
|
42
|
+
if (id === 'report.asks') return COPY.noteAsks;
|
|
43
|
+
return COPY.noteOf(heading || id);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The draft as blocks: h1 h2 h3 p ul table note. A note keeps its text exactly; everything else is read line by line. */
|
|
47
|
+
export function parseReport(md) {
|
|
48
|
+
const notes = new Map();
|
|
49
|
+
const body = stripFront(md).replace(NOTE_RE, (_all, id, text) => { notes.set(id, text); return '\n' + MARK + id + '\n'; });
|
|
50
|
+
const lines = body.split('\n');
|
|
51
|
+
const blocks = [];
|
|
52
|
+
let heading = '';
|
|
53
|
+
for (let i = 0; i < lines.length;) {
|
|
54
|
+
const ln = lines[i];
|
|
55
|
+
if (!ln.trim()) { i++; continue; }
|
|
56
|
+
if (ln.startsWith(MARK)) { const id = ln.slice(MARK.length); blocks.push({ t: 'note', id, text: notes.get(id) || '', title: noteTitle(id, heading) }); i++; continue; }
|
|
57
|
+
const h = /^(#{1,3})\s+(.*)$/.exec(ln);
|
|
58
|
+
if (h) { if (h[1].length === 3) heading = bare(h[2]); if (h[1].length === 2) heading = ''; blocks.push({ t: 'h' + h[1].length, text: h[2] }); i++; continue; }
|
|
59
|
+
if (ln.trim().startsWith('|')) {
|
|
60
|
+
const rows = [];
|
|
61
|
+
while (i < lines.length && lines[i].trim().startsWith('|')) { if (!isRule(lines[i])) rows.push(cells(lines[i])); i++; }
|
|
62
|
+
blocks.push({ t: 'table', head: rows[0] || [], rows: rows.slice(1) });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (/^- /.test(ln)) {
|
|
66
|
+
const items = [];
|
|
67
|
+
while (i < lines.length && /^- /.test(lines[i])) { items.push(lines[i].slice(2)); i++; }
|
|
68
|
+
blocks.push({ t: 'ul', items });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const para = [];
|
|
72
|
+
while (i < lines.length && lines[i].trim() && !lines[i].startsWith(MARK) && !/^(#{1,3}\s|\|\s*|- )/.test(lines[i])) { para.push(lines[i].trim()); i++; }
|
|
73
|
+
blocks.push({ t: 'p', text: para.join(' ') });
|
|
74
|
+
}
|
|
75
|
+
return blocks;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The notes of the draft, in the order they appear. */
|
|
79
|
+
export function noteSlots(md) {
|
|
80
|
+
return parseReport(md).filter((b) => b.t === 'note').map((b) => ({ id: b.id, title: b.title, text: b.text }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** "a **bold** run" as text and <strong> nodes. No HTML is built from a string. */
|
|
84
|
+
function inline(text) {
|
|
85
|
+
const out = [];
|
|
86
|
+
String(text).split(/(\*\*[^*]+\*\*)/).forEach((part) => {
|
|
87
|
+
if (!part) return;
|
|
88
|
+
if (/^\*\*[^*]+\*\*$/.test(part)) out.push(el('strong', { class: 'rp-doc__strong' }, part.slice(2, -2)));
|
|
89
|
+
else out.push(part);
|
|
90
|
+
});
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The draft as one `div.rp-doc`. A note with no text builds nothing: an empty note is a slot, not a line of the report. */
|
|
95
|
+
export function renderReport(md) {
|
|
96
|
+
const root = el('div', { class: 'rp-doc' });
|
|
97
|
+
for (const b of parseReport(md)) {
|
|
98
|
+
if (b.t === 'h1') root.appendChild(el('h1', { class: 'rp-doc__h1' }, inline(b.text)));
|
|
99
|
+
else if (b.t === 'h2') root.appendChild(el('h2', { class: 'rp-doc__h2' }, inline(b.text)));
|
|
100
|
+
else if (b.t === 'h3') root.appendChild(el('h3', { class: 'rp-doc__h3' }, inline(b.text)));
|
|
101
|
+
else if (b.t === 'p') root.appendChild(el('p', { class: 'rp-doc__p' }, inline(b.text)));
|
|
102
|
+
else if (b.t === 'ul') root.appendChild(el('ul', { class: 'rp-doc__list' }, b.items.map((x) => el('li', { class: 'rp-doc__li' }, inline(x)))));
|
|
103
|
+
else if (b.t === 'table') {
|
|
104
|
+
root.appendChild(el('div', { class: 'rp-doc__scroll' }, el('table', { class: 'rp-doc__table' },
|
|
105
|
+
el('thead', null, el('tr', null, b.head.map((c) => el('th', { class: 'rp-doc__th', scope: 'col' }, c)))),
|
|
106
|
+
el('tbody', null, b.rows.map((r) => el('tr', null, r.map((c) => el('td', { class: 'rp-doc__td' }, c))))))));
|
|
107
|
+
} else if (b.t === 'note' && b.text.trim()) {
|
|
108
|
+
root.appendChild(el('div', { class: 'rp-doc__note', 'data-note': b.id }, b.text.split(/\n{2,}/).map((p) => el('p', { class: 'rp-doc__p rp-doc__p--note' }, p))));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return root;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The markdown a person copies: no frontmatter, no note markers, the notes' own text kept in place. */
|
|
115
|
+
export function markdownToCopy(md) {
|
|
116
|
+
return stripFront(md).replace(/<!-- \/?pmos:note[^>]*-->\n?/g, '').replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ── the two things a report leaves through: Copy as markdown, Print / PDF ───────────────────────────────────────── */
|
|
120
|
+
|
|
121
|
+
export async function copyMarkdown(md) {
|
|
122
|
+
const text = markdownToCopy(md);
|
|
123
|
+
let done = false;
|
|
124
|
+
try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); done = true; } } catch { done = false; }
|
|
125
|
+
if (!done) {
|
|
126
|
+
// clipboard API refused (no permission, or a plain http page): the old route, from a text area that is removed at once
|
|
127
|
+
const area = document.createElement('textarea');
|
|
128
|
+
area.value = text; area.className = 'rp-copy-area'; area.setAttribute('aria-hidden', 'true'); area.setAttribute('tabindex', '-1');
|
|
129
|
+
document.body.appendChild(area); area.select();
|
|
130
|
+
try { done = document.execCommand('copy'); } catch { done = false; }
|
|
131
|
+
area.remove();
|
|
132
|
+
}
|
|
133
|
+
ensureComponentsCss();
|
|
134
|
+
toast({ text: done ? COPY.copied : COPY.copyFailed, tone: done ? undefined : 'error' });
|
|
135
|
+
return done;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Link /pm/print.css once, for print only. */
|
|
139
|
+
export function ensurePrintCss() {
|
|
140
|
+
if (typeof document === 'undefined' || document.querySelector('link[data-rp-print]')) return;
|
|
141
|
+
const l = document.createElement('link');
|
|
142
|
+
l.rel = 'stylesheet'; l.href = '/pm/print.css'; l.media = 'print';
|
|
143
|
+
l.setAttribute('data-rp-print', '');
|
|
144
|
+
document.head.appendChild(l);
|
|
145
|
+
}
|
|
146
|
+
/** Remove the printed copy of a report, if one is in the page. */
|
|
147
|
+
export function clearPrint() { for (const n of document.querySelectorAll('body > .rp-print')) n.remove(); }
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The report on paper. `body > .rp-print` holds the rendered report in the light theme; print.css hides every other child
|
|
151
|
+
* of the body and shows only it. It is removed when the print dialog closes, and again before the next print.
|
|
152
|
+
*/
|
|
153
|
+
export function printReport(md) {
|
|
154
|
+
ensureComponentsCss();
|
|
155
|
+
ensurePrintCss();
|
|
156
|
+
clearPrint();
|
|
157
|
+
const host = el('div', { class: 'rp-print', 'data-pm-theme': 'light', 'aria-hidden': 'true' }, renderReport(md));
|
|
158
|
+
document.body.appendChild(host);
|
|
159
|
+
const drop = () => { host.remove(); window.removeEventListener('afterprint', drop); };
|
|
160
|
+
window.addEventListener('afterprint', drop);
|
|
161
|
+
try { window.print(); } catch { drop(); }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ── writes ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
165
|
+
|
|
166
|
+
/** Tell the page's other readers (Today's provider cards) that this repository's ledgers changed. */
|
|
167
|
+
function changed(ws) {
|
|
168
|
+
try { window.dispatchEvent(new CustomEvent('pmos:ledgers', { detail: { changed: [ws] } })); } catch { /* no listener */ }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Draft now: write the next draft of `cadence`. `redraft` keeps every note. Resolves to the server's answer. */
|
|
172
|
+
export async function draftNow({ ws, cadence, slot, redraft }) {
|
|
173
|
+
ensureComponentsCss();
|
|
174
|
+
const body = { ws, cadence };
|
|
175
|
+
if (slot) body.slot = slot;
|
|
176
|
+
if (redraft) body.redraft = true;
|
|
177
|
+
const r = await pmPost('/api/report/draft', body);
|
|
178
|
+
if (r && r.ok) { changed(ws); toast({ text: redraft ? COPY.redrafted : COPY.drafted(cadence, r.slot), timeout: 4000 }); }
|
|
179
|
+
else toast({ text: COPY.draftFailed + (r && r.error ? ' ' + r.error : ''), tone: 'error', timeout: 8000 });
|
|
180
|
+
return r;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ── the Issue sheet: two taps from anywhere ─────────────────────────────────────────────────────────────────────── */
|
|
184
|
+
|
|
185
|
+
function headlineOf(md) {
|
|
186
|
+
const bl = parseReport(md);
|
|
187
|
+
const i = bl.findIndex((b) => b.t === 'h2' && /^headline$/i.test(b.text.trim()));
|
|
188
|
+
return i >= 0 && bl[i + 1] && bl[i + 1].t === 'p' ? bl[i + 1].text : '';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* openIssueSheet({ ws, cadence, slot, onDone? })
|
|
193
|
+
* Tap 1 opened this sheet. It reads the draft and its sha. Tap 2 (Issue) posts { ws, cadence, slot, sha } to the PM-only
|
|
194
|
+
* route; a draft that changed since the read answers 409 and nothing is issued. After Issue the sheet offers Print / PDF
|
|
195
|
+
* and Copy as markdown, the only two ways a report leaves the page.
|
|
196
|
+
*/
|
|
197
|
+
export function openIssueSheet({ ws, cadence, slot, onDone } = {}) {
|
|
198
|
+
ensureComponentsCss();
|
|
199
|
+
ensureSprite();
|
|
200
|
+
if (!ws) { toast({ text: COPY.issueNoWs, tone: 'error' }); return null; }
|
|
201
|
+
const msg = el('p', { class: 'rp-sheet__msg', role: 'status' }, COPY.issueReading);
|
|
202
|
+
const head = el('p', { class: 'rp-sheet__headline' });
|
|
203
|
+
const acts = el('div', { class: 'rp-sheet__row' });
|
|
204
|
+
const cancel = button({ label: COPY.issueCancel, variant: 'ghost', onClick: () => sh.close() });
|
|
205
|
+
const sh = sheet({ title: COPY.issueTitle, sub: COPY.issueSub(cadence, slot), body: [msg, head, acts], actions: [cancel], attrs: { class: 'rp-sheet' } });
|
|
206
|
+
const actions = sh.dialog.querySelector(SEL.sheetActions);
|
|
207
|
+
let doc = null;
|
|
208
|
+
const close = button({ label: COPY.issueClose, variant: 'ghost', onClick: () => sh.close() });
|
|
209
|
+
|
|
210
|
+
const showError = (text) => { msg.textContent = text; msg.classList.add('rp-sheet__msg--bad'); actions.replaceChildren(close); };
|
|
211
|
+
|
|
212
|
+
const confirm = button({ label: COPY.issueConfirm, variant: 'primary', attrs: { 'data-rp-act': 'issue-confirm' }, onClick: async () => {
|
|
213
|
+
if (!doc) return;
|
|
214
|
+
confirm.disabled = true;
|
|
215
|
+
const r = await pmPost('/api/report/issue', { ws, cadence, slot, sha: doc.sha });
|
|
216
|
+
if (r && r.ok) {
|
|
217
|
+
changed(ws);
|
|
218
|
+
msg.classList.remove('rp-sheet__msg--bad');
|
|
219
|
+
msg.textContent = COPY.issueDone;
|
|
220
|
+
acts.replaceChildren(
|
|
221
|
+
button({ label: COPY.print, variant: 'outline', attrs: { 'data-rp-act': 'print' }, onClick: () => printReport(doc.markdown) }),
|
|
222
|
+
button({ label: COPY.copyMd, variant: 'outline', attrs: { 'data-rp-act': 'copy' }, onClick: () => copyMarkdown(doc.markdown) }));
|
|
223
|
+
actions.replaceChildren(button({ label: COPY.issueClose, variant: 'primary', onClick: () => sh.close() }));
|
|
224
|
+
if (onDone) { try { onDone(r); } catch (e) { console.error('[reports] onDone failed:', e); } }
|
|
225
|
+
} else {
|
|
226
|
+
confirm.disabled = false;
|
|
227
|
+
showError(r && r.status === 409 ? COPY.issueStale : COPY.issueFailed + (r && r.error ? ' ' + r.error : ''));
|
|
228
|
+
}
|
|
229
|
+
} });
|
|
230
|
+
|
|
231
|
+
pmGet('/api/report/draft?ws=' + encodeURIComponent(ws) + '&cadence=' + encodeURIComponent(cadence) + '&slot=' + encodeURIComponent(slot)).then((r) => {
|
|
232
|
+
if (!sh.open) return;
|
|
233
|
+
if (!r || r.ok === false || !r.exists) { showError(COPY.issueFailed + (r && r.error ? ' ' + r.error : '')); return; }
|
|
234
|
+
doc = r;
|
|
235
|
+
if (r.issued) { msg.textContent = COPY.frozen; actions.replaceChildren(close); return; }
|
|
236
|
+
msg.textContent = COPY.issueBody;
|
|
237
|
+
head.textContent = headlineOf(r.markdown);
|
|
238
|
+
actions.replaceChildren(confirm, cancel);
|
|
239
|
+
confirm.focus();
|
|
240
|
+
});
|
|
241
|
+
return sh;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* ── the Notes sheet: every note in one place ─────────────────────────────────────────────────────────────────────── */
|
|
245
|
+
|
|
246
|
+
/** openNotesSheet({ ws, cadence, slot, markdown, onSaved? }): a text area for each note marker of the draft. */
|
|
247
|
+
export function openNotesSheet({ ws, cadence, slot, markdown, onSaved } = {}) {
|
|
248
|
+
ensureComponentsCss();
|
|
249
|
+
ensureSprite();
|
|
250
|
+
const slots = noteSlots(markdown);
|
|
251
|
+
const fields = slots.map((s) => ({ slot: s, f: field({ kind: 'textarea', label: s.title, value: s.text, attrs: { 'data-note-field': s.id } }) }));
|
|
252
|
+
const msg = el('p', { class: 'rp-sheet__msg', role: 'status' }, slots.length ? '' : COPY.noteNone);
|
|
253
|
+
let sh = null;
|
|
254
|
+
const cancel = button({ label: COPY.issueCancel, variant: 'ghost', onClick: () => sh.close() });
|
|
255
|
+
const save = button({ label: COPY.noteSave, variant: 'primary', attrs: { 'data-rp-act': 'notes-save' }, onClick: async () => {
|
|
256
|
+
save.disabled = true;
|
|
257
|
+
let failed = null;
|
|
258
|
+
for (const { slot: s, f } of fields) {
|
|
259
|
+
if (f.control.value === s.text) continue; // only a note that changed is written
|
|
260
|
+
const r = await pmPost('/api/report/note', { ws, cadence, slot, id: s.id, text: f.control.value });
|
|
261
|
+
if (!r || !r.ok) { failed = r || {}; break; }
|
|
262
|
+
s.text = f.control.value;
|
|
263
|
+
}
|
|
264
|
+
save.disabled = false;
|
|
265
|
+
if (failed) { msg.textContent = COPY.noteFailed + (failed.error ? ' ' + failed.error : ''); msg.classList.add('rp-sheet__msg--bad'); return; }
|
|
266
|
+
changed(ws);
|
|
267
|
+
sh.close();
|
|
268
|
+
toast({ text: COPY.noteSaved, timeout: 3000 });
|
|
269
|
+
if (onSaved) onSaved();
|
|
270
|
+
} });
|
|
271
|
+
sh = sheet({ title: COPY.notesTitle, sub: COPY.notesSub, body: [msg, ...fields.map((x) => x.f)], actions: slots.length ? [save, cancel] : [cancel], attrs: { class: 'rp-sheet' } });
|
|
272
|
+
return sh;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ── the Today cards ──────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
276
|
+
|
|
277
|
+
/** run_id is "report-needs:<issue|no-output>:<cadence>:<slot or day>" (lib/report-needs.js). */
|
|
278
|
+
function idsOf(row) {
|
|
279
|
+
const p = String((row && row.run_id) || '').split(':');
|
|
280
|
+
return { cadence: p[2] || '', slot: p[3] || '' };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
registerTodayCard('report-issue', (row, ctx) => {
|
|
284
|
+
const { cadence, slot } = idsOf(row);
|
|
285
|
+
const t = row.today || {};
|
|
286
|
+
ensureComponentsCss();
|
|
287
|
+
return card({
|
|
288
|
+
title: row.question,
|
|
289
|
+
meta: [t.cause || COPY.todayIssueCause, t.evidence],
|
|
290
|
+
trail: [badge({ text: COPY.todayIssueTag, tone: 'outline' })],
|
|
291
|
+
openLabel: COPY.todayIssueOpen(cadence, slot),
|
|
292
|
+
onOpen: () => openIssueSheet({ ws: ctx.ws, cadence, slot }),
|
|
293
|
+
attrs: { 'data-today-row': 'report-issue', 'data-provider': 'report-needs', 'data-report': cadence + ':' + slot },
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
registerTodayCard('schedule-no-output', (row, ctx) => {
|
|
298
|
+
const { cadence } = idsOf(row);
|
|
299
|
+
const t = row.today || {};
|
|
300
|
+
ensureComponentsCss();
|
|
301
|
+
return card({
|
|
302
|
+
title: row.question,
|
|
303
|
+
meta: [t.cause || COPY.todayNoOutputCause, t.evidence],
|
|
304
|
+
trail: [badge({ text: COPY.todayDraftTag, tone: 'outline' })],
|
|
305
|
+
openLabel: COPY.todayDraftOpen(cadence),
|
|
306
|
+
onOpen: () => draftNow({ ws: ctx.ws, cadence }),
|
|
307
|
+
attrs: { 'data-today-row': 'schedule-no-output', 'data-provider': 'report-needs', 'data-report': cadence },
|
|
308
|
+
});
|
|
309
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* pm/reports-copy.js — every word the Reports destination shows (pm-os-absorb Run 5, slice 5C).
|
|
2
|
+
*
|
|
3
|
+
* Pure data and pure functions. No DOM and no import, so glossary.test.js loads this file in Node and checks each string
|
|
4
|
+
* against the glossary rules. A string a person reads on Reports, in its sheets, on a report Today card or in a toast is
|
|
5
|
+
* written here and nowhere else. Instructional register (PMOS-STE): short sentences, one verb, the action first.
|
|
6
|
+
*
|
|
7
|
+
* A report is ISSUED, never sent (D71): the page has no control that moves a report out of the workspace, and no
|
|
8
|
+
* string here says it does. GLOSSARY.extra holds the rule that keeps the words out.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
12
|
+
|
|
13
|
+
/** "2026-10-16" -> "16 Oct". A date-only value is a calendar day, never shifted by the time zone. */
|
|
14
|
+
export function shortDate(ymd) {
|
|
15
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(ymd || ''));
|
|
16
|
+
return m ? Number(m[3]) + ' ' + MON[Number(m[2]) - 1] : '';
|
|
17
|
+
}
|
|
18
|
+
/** An ISO timestamp -> "5 Oct", in the viewer's own calendar. */
|
|
19
|
+
export function dayOf(iso) {
|
|
20
|
+
const d = new Date(iso);
|
|
21
|
+
return Number.isNaN(d.getTime()) ? '' : d.getDate() + ' ' + MON[d.getMonth()];
|
|
22
|
+
}
|
|
23
|
+
/** "biweekly" -> "Biweekly"; the cadence ids are the report ids. */
|
|
24
|
+
export const cadenceName = (c) => { const s = String(c || ''); return s ? s[0].toUpperCase() + s.slice(1) : ''; };
|
|
25
|
+
export const plural = (n, one, many) => (n === 1 ? '1 ' + one : n + ' ' + (many || one + 's'));
|
|
26
|
+
|
|
27
|
+
export const COPY = {
|
|
28
|
+
title: 'Reports',
|
|
29
|
+
list: 'Reports of this repository',
|
|
30
|
+
none: 'No report yet. Press Draft now to write the first one.',
|
|
31
|
+
noRepo: 'Choose a repository to see its reports.',
|
|
32
|
+
loading: 'Reading the reports.',
|
|
33
|
+
readError: 'The reports could not be read.',
|
|
34
|
+
fixture: 'Reports are read from a repository. This preview shows none.',
|
|
35
|
+
showOlder: (n) => 'Show older (' + n + ')',
|
|
36
|
+
showFewer: 'Show fewer',
|
|
37
|
+
|
|
38
|
+
draftNow: 'Draft now',
|
|
39
|
+
redraft: 'Redraft',
|
|
40
|
+
issue: 'Issue',
|
|
41
|
+
editNotes: 'Edit notes',
|
|
42
|
+
more: 'More report actions',
|
|
43
|
+
print: 'Print / PDF',
|
|
44
|
+
copyMd: 'Copy as markdown',
|
|
45
|
+
copied: 'Copied as markdown.',
|
|
46
|
+
copyFailed: 'The browser refused the copy. Select the text and copy it by hand.',
|
|
47
|
+
|
|
48
|
+
reportName: (cadence, slot) => cadenceName(cadence) + ' report, ' + shortDate(slot),
|
|
49
|
+
rowTitle: (cadence, slot) => cadenceName(cadence) + ' · ' + shortDate(slot),
|
|
50
|
+
draft: 'Draft',
|
|
51
|
+
issued: (at, by) => 'Issued ' + (dayOf(at) || '') + (by ? ' by ' + by : ''),
|
|
52
|
+
edited: 'Edited after issue',
|
|
53
|
+
editedWhy: 'This file changed after it was issued. The issued bytes are in the issues ledger.',
|
|
54
|
+
frozen: 'Issued. The file is frozen.',
|
|
55
|
+
|
|
56
|
+
drafted: (cadence, slot) => 'Drafted the ' + cadence + ' report for ' + shortDate(slot) + '.',
|
|
57
|
+
redrafted: 'Redrafted. Your notes are kept.',
|
|
58
|
+
draftFailed: 'The draft was not written.',
|
|
59
|
+
|
|
60
|
+
notesTitle: 'Notes',
|
|
61
|
+
notesSub: 'A note is your text between the markers. Saving a note draws the report again from the files as they are now.',
|
|
62
|
+
noteHeadline: 'Headline note',
|
|
63
|
+
noteAsks: 'Note for needs your call',
|
|
64
|
+
noteOf: (title) => 'Note on ' + title,
|
|
65
|
+
noteSave: 'Save notes',
|
|
66
|
+
noteSaved: 'Notes saved.',
|
|
67
|
+
noteFailed: 'The notes were not saved.',
|
|
68
|
+
noteNone: 'This draft has no note markers. There is no note to edit.',
|
|
69
|
+
|
|
70
|
+
issueTitle: 'Issue this report',
|
|
71
|
+
issueBody: 'Issue freezes the report as you read it. The file does not change after this.',
|
|
72
|
+
issueSub: (cadence, slot) => cadenceName(cadence) + ' report, ' + shortDate(slot),
|
|
73
|
+
issueConfirm: 'Issue',
|
|
74
|
+
issueCancel: 'Cancel',
|
|
75
|
+
issueReading: 'Reading the draft.',
|
|
76
|
+
issueDone: 'Issued. The file is frozen.',
|
|
77
|
+
issueStale: 'The draft changed while you read it. Close this sheet, read it again, then press Issue.',
|
|
78
|
+
issueFailed: 'Nothing was issued.',
|
|
79
|
+
issueNoWs: 'No repository is open, so nothing was issued.',
|
|
80
|
+
issueClose: 'Close',
|
|
81
|
+
|
|
82
|
+
todayIssueCause: 'A draft is ready to issue',
|
|
83
|
+
todayNoOutputCause: 'A schedule ran and left no draft',
|
|
84
|
+
todayDraftOpen: (cadence) => 'Draft the ' + cadence + ' report',
|
|
85
|
+
todayIssueOpen: (cadence, slot) => 'Issue the ' + cadence + ' report of ' + shortDate(slot),
|
|
86
|
+
todayIssueTag: 'Issue',
|
|
87
|
+
todayDraftTag: 'Draft now',
|
|
88
|
+
|
|
89
|
+
schedulesTitle: 'Report schedules',
|
|
90
|
+
schedulesNone: 'No report schedule is set. A schedule drafts a report at its time; it never issues one.',
|
|
91
|
+
schedulesSeed: 'Set up report schedules',
|
|
92
|
+
schedulesSeeded: (n) => (n ? 'Set up ' + plural(n, 'report schedule') + '.' : 'The report schedules were already set up.'),
|
|
93
|
+
schedulesFailed: 'The schedules were not changed.',
|
|
94
|
+
pause: 'Pause',
|
|
95
|
+
resume: 'Resume',
|
|
96
|
+
paused: 'Paused',
|
|
97
|
+
pausedMsg: (name) => name + ' is paused.',
|
|
98
|
+
resumed: (name) => name + ' runs again.',
|
|
99
|
+
otherSchedules: (n) => 'Other schedules (' + n + ')',
|
|
100
|
+
scheduleMeta: (when, last) => when + (last ? ' · ' + last : ''),
|
|
101
|
+
lastRan: (iso) => 'ran ' + dayOf(iso),
|
|
102
|
+
neverRan: 'has not run',
|
|
103
|
+
firesOnlyRunning: 'A schedule fires only while the workspace server runs.',
|
|
104
|
+
|
|
105
|
+
flagTitle: 'The issues ledger has a line that is not read',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const GLOSSARY = {
|
|
109
|
+
exempt: {},
|
|
110
|
+
extra: [
|
|
111
|
+
{ id: 'issued-not-sent', re: /\b(?:sent|sends?|sending|e-?mail(?:s|ed|ing)?|shar(?:e|es|ed|ing)|post(?:s|ed|ing)? to)\b/i, why: 'A report is issued, never sent (D71). Reports has no control that moves a report out of the workspace.' },
|
|
112
|
+
],
|
|
113
|
+
guide: 'reports-page.md',
|
|
114
|
+
};
|