create-pmos 0.93.0 → 0.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/first-paint-audit.js +2 -1
- package/workspace/guide/notes.md +1 -1
- package/workspace/lib/kit-update.js +8 -0
- package/workspace/lib/report-needs.js +8 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +2 -1
- package/workspace/public/pm/components.css +34 -1
- package/workspace/public/pm/dest-reports.js +80 -28
- package/workspace/public/pm/mobile.js +1 -1
- package/workspace/public/pm/note-dock.js +1 -0
- package/workspace/public/pm/panel-cockpit.js +1 -1
- package/workspace/public/pm/panel-tabs.js +10 -3
- package/workspace/public/pm/report-tab.js +33 -0
- package/workspace/public/pm/reports-copy.js +12 -0
- package/workspace/public/pm/topbar.js +16 -2
- package/workspace/public/pm/view-notes-menu.js +2 -2
- package/workspace/skills/report-new.skill +27 -0
- package/workspace/skills/report-template.skill +34 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.95.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T23:23:47Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T23:23:47Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T23:23:47Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// budget is for the whole screen and the bar is on every screen. The calibration stays specimen A's own count: the
|
|
23
23
|
// specimen did not change. Every place +1 again for People in the top bar (the PM, 2026-10-06, people-directory), and
|
|
24
24
|
// +1 again for My work beside Agenda (the PM approved that mockup and said build it, 2026-10-07, my-work).
|
|
25
|
+
// reports: +1 for New report in the Reports header (the PM asked for it, 2026-10-07, reports-new-session).
|
|
25
26
|
const SPECIMEN_A = Object.freeze({ today: 17, initiatives: 24, board: 33, reports: 14 });
|
|
26
27
|
const BAR_DOCS = 1;
|
|
27
28
|
const BAR_PEOPLE = 1;
|
|
@@ -30,7 +31,7 @@ const BAR = BAR_DOCS + BAR_PEOPLE + BAR_MY_WORK;
|
|
|
30
31
|
// Agenda opens on the month (the PM's Agenda mockup v5, 2026-10-06): the month adds its own controls to Today's budget,
|
|
31
32
|
// at most six week rows of a week button and seven days, the two month arrows and the five lenses.
|
|
32
33
|
const AGENDA_MONTH = 6 * 8 + 2 + 5;
|
|
33
|
-
const EXPECT = Object.freeze({ today: 17 + BAR + AGENDA_MONTH, initiatives: 24 + BAR, board: 33 + BAR, reports:
|
|
34
|
+
const EXPECT = Object.freeze({ today: 17 + BAR + AGENDA_MONTH, initiatives: 24 + BAR, board: 33 + BAR, reports: 16 + BAR });
|
|
34
35
|
|
|
35
36
|
/* ── shoot.js audit(), copied verbatim (scratchpad/specimens2/shoot.js, 2026-10-05) ─────────────────────────────── */
|
|
36
37
|
// runs in the page
|
package/workspace/guide/notes.md
CHANGED
|
@@ -11,7 +11,7 @@ The Notes view shows one meeting, top to bottom. A bar at the top shows whether
|
|
|
11
11
|
|
|
12
12
|
## Start a meeting
|
|
13
13
|
|
|
14
|
-
1. Press ⌘K, type **notes** and press Enter. The view opens as a tab. It is the same in every repository.
|
|
14
|
+
1. Press the **Notes** button in the top bar, or press ⌘K, type **notes** and press Enter. The view opens as a tab. It is the same in every repository. The **Notes** button shows a red dot when notes are being recorded.
|
|
15
15
|
2. Press **New meeting**. Type a title.
|
|
16
16
|
3. Under **Your notes**, type a note and press Enter. Each note is its own line, with the meeting time you added it at. To remove a line, press the **x** beside it. Each line also shows in the transcript as **You, typed** and is labelled like a spoken line, so a decision or a next step you type goes into its section. Your notes steer the summary, and they save at once.
|
|
17
17
|
4. Press **All notes** to open an earlier note. The **⋯** menu has Copy as Markdown, New meeting, the meeting and the initiative, Summary by agent, Mark private, Settings and **Delete note**.
|
|
@@ -181,8 +181,16 @@ function upgradeApp(root, { version, logFile, spawn = cp.spawn, env = updateEnv(
|
|
|
181
181
|
try {
|
|
182
182
|
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
183
183
|
fd = fs.openSync(logFile, 'w');
|
|
184
|
+
fs.writeSync(fd, `${new Date().toISOString()} clearing npm cache…\n`);
|
|
185
|
+
fs.writeSync(fd, `${new Date().toISOString()} ${NPM} cache clean --force\n`);
|
|
184
186
|
fs.writeSync(fd, `${new Date().toISOString()} ${NPX} -y ${PKG_NAME}@${version} workspace --reinstall ${root}\n`);
|
|
185
187
|
} catch (e) { return { ok: false, error: `Could not write the log file: ${e.message}` }; }
|
|
188
|
+
try {
|
|
189
|
+
// Clear npm cache synchronously before spawning the update, so npx downloads the fresh version
|
|
190
|
+
cp.execSync(`${NPM} cache clean --force`, { cwd: String(root), env, stdio: ['ignore', fd, fd] });
|
|
191
|
+
} catch (e) {
|
|
192
|
+
try { fs.appendFileSync(logFile, `Note: cache clear had an issue, but continuing: ${e.message}\n`); } catch { /* silent */ }
|
|
193
|
+
}
|
|
186
194
|
try {
|
|
187
195
|
const child = spawn(NPX, ['-y', `${PKG_NAME}@${version}`, 'workspace', '--reinstall', String(root)],
|
|
188
196
|
{ cwd: String(root), env, detached: true, stdio: ['ignore', fd, fd], windowsHide: true, shell: IS_WIN });
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* reportsOf(root) → { ok, reports: [{ cadence, slot, path, status, bytes, mtimeMs, issued, edited }],
|
|
8
8
|
* flags, sig } the report files joined to issues.jsonl; the routes read it too
|
|
9
9
|
* cadenceOfSkill(skill) 'report-biweekly' → 'biweekly', else null
|
|
10
|
+
* cadenceOfSchedule(s) a template schedule (skill report-template) → its args; else cadenceOfSkill(s.skill)
|
|
10
11
|
*
|
|
11
12
|
* THE THREE ROW KINDS (rows are held to fixtures/today-api/provider-rows.json):
|
|
12
13
|
* report-issue kind 1. The newest draft of a cadence that the report script wrote, that is not issued, and that is
|
|
@@ -56,6 +57,11 @@ function reportsDir(root) {
|
|
|
56
57
|
const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
|
|
57
58
|
const short = (ymd) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(ymd || '')); return m ? `${Number(m[3])} ${MONTHS[Number(m[2]) - 1]}` : String(ymd || ''); };
|
|
58
59
|
const cadenceOfSkill = (skill) => { const m = /^report-([a-z][a-z0-9-]{0,39})$/.exec(String(skill || '')); return m ? m[1] : null; };
|
|
60
|
+
/** A saved template runs the generic report-template skill; its schedule's args name the template, which is its cadence id. */
|
|
61
|
+
const cadenceOfSchedule = (s) => {
|
|
62
|
+
if (s && s.skill === 'report-template') { const a = String(s.args || '').trim(); return /^[a-z][a-z0-9-]{0,39}$/.test(a) ? a : null; }
|
|
63
|
+
return cadenceOfSkill(s && s.skill);
|
|
64
|
+
};
|
|
59
65
|
/** What `git hash-object` prints for these bytes, without running git: the sha issues.jsonl records. */
|
|
60
66
|
const blobSha = (buf) => crypto.createHash('sha1').update(`blob ${buf.length}\0`).update(buf).digest('hex');
|
|
61
67
|
|
|
@@ -173,7 +179,7 @@ function noOutputRows(w, now, schedules) {
|
|
|
173
179
|
const out = [];
|
|
174
180
|
const seen = new Set();
|
|
175
181
|
for (const s of schedules) {
|
|
176
|
-
const cadence =
|
|
182
|
+
const cadence = cadenceOfSchedule(s);
|
|
177
183
|
if (!cadence || s.ws !== w.id || s.enabled === false || !s.lastRun) continue;
|
|
178
184
|
const fired = Date.parse(s.lastRun);
|
|
179
185
|
if (!Number.isFinite(fired)) continue;
|
|
@@ -210,4 +216,4 @@ function sig(w) {
|
|
|
210
216
|
return [reportsOf(w.root).sig, schedulesStamp(), kill, hour].join(':');
|
|
211
217
|
}
|
|
212
218
|
|
|
213
|
-
module.exports = { kinds, rowsFor, sig, rowsAt, reportsOf, cadenceOfSkill, blobSha, reportsDir, KILL_FILE, GRACE_MS };
|
|
219
|
+
module.exports = { kinds, rowsFor, sig, rowsAt, reportsOf, cadenceOfSkill, cadenceOfSchedule, blobSha, reportsDir, KILL_FILE, GRACE_MS };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.95.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.95.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -38,6 +38,7 @@ import { state, setState, subscribe, flush, FIXTURE, LEGACY_LAYOUT_FLAG } from '
|
|
|
38
38
|
import { isMobile, mountMobile, applyMobile } from './mobile.js';
|
|
39
39
|
/* The frame's parts (Run 3, slice 3A). Small modules with no effect at import: a fixture page loads them and calls none. */
|
|
40
40
|
import { createTopbar, unsyncedOf } from './topbar.js';
|
|
41
|
+
import { openView } from './panel-cockpit.js';
|
|
41
42
|
import { createDockRail } from './dock.js';
|
|
42
43
|
import { SEL, ensureComponentsCss, iconButton, menu, toast, sheet, button } from './primitives.js';
|
|
43
44
|
import { ensureSprite } from './icons.js';
|
|
@@ -607,7 +608,7 @@ const frEnd = el('span', { class: 'fr-end', 'aria-hidden': 'true' });
|
|
|
607
608
|
function setupFrame() {
|
|
608
609
|
ensureComponentsCss();
|
|
609
610
|
ensureSprite();
|
|
610
|
-
bar = createTopbar({ go, openPalette, repoMenu, sync: runSync });
|
|
611
|
+
bar = createTopbar({ go, openPalette, openView, repoMenu, sync: runSync });
|
|
611
612
|
document.addEventListener('pointerdown', (e) => { fr.downAt = e.target; }, true);
|
|
612
613
|
dockRail = createDockRail({ open: openDock, resize: dockWidth });
|
|
613
614
|
fr.dockW = readDockW();
|
|
@@ -2342,6 +2342,17 @@
|
|
|
2342
2342
|
font-family: var(--mono);
|
|
2343
2343
|
font-size: 11px;
|
|
2344
2344
|
}
|
|
2345
|
+
.fr-recording-dot {
|
|
2346
|
+
width: 8px;
|
|
2347
|
+
height: 8px;
|
|
2348
|
+
border-radius: 50%;
|
|
2349
|
+
background: var(--destructive);
|
|
2350
|
+
margin: 0 6px;
|
|
2351
|
+
display: none;
|
|
2352
|
+
}
|
|
2353
|
+
.fr-recording-dot--active {
|
|
2354
|
+
display: block;
|
|
2355
|
+
}
|
|
2345
2356
|
/* The body: the destination, then (when open) the detail, the dock's rail and the dock. One row, columns in order. */
|
|
2346
2357
|
.fr-body {
|
|
2347
2358
|
display: grid;
|
|
@@ -4442,9 +4453,23 @@
|
|
|
4442
4453
|
flex-wrap: wrap;
|
|
4443
4454
|
gap: 8px;
|
|
4444
4455
|
}
|
|
4456
|
+
.rp-card {
|
|
4457
|
+
align-items: stretch;
|
|
4458
|
+
padding: 10px 12px;
|
|
4459
|
+
border: 1px solid var(--border);
|
|
4460
|
+
border-radius: 8px;
|
|
4461
|
+
background: var(--card);
|
|
4462
|
+
}
|
|
4463
|
+
.rp-card > .rp-list { border-top: 0; }
|
|
4445
4464
|
.rp-viewer {
|
|
4446
4465
|
align-items: stretch;
|
|
4447
4466
|
}
|
|
4467
|
+
.rp-viewer__type {
|
|
4468
|
+
margin: 0;
|
|
4469
|
+
font-size: 12px;
|
|
4470
|
+
line-height: 16px;
|
|
4471
|
+
color: var(--muted-foreground);
|
|
4472
|
+
}
|
|
4448
4473
|
.rp-viewer__state {
|
|
4449
4474
|
margin: 0;
|
|
4450
4475
|
font-size: 12px;
|
|
@@ -5352,7 +5377,15 @@
|
|
|
5352
5377
|
|
|
5353
5378
|
@container (min-width: 720px) {
|
|
5354
5379
|
.rp-cols {
|
|
5355
|
-
grid-template-columns: minmax(
|
|
5380
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
|
|
5381
|
+
align-items: start;
|
|
5382
|
+
}
|
|
5383
|
+
.rp-col--doc .rp-viewer {
|
|
5384
|
+
position: sticky;
|
|
5385
|
+
top: 8px;
|
|
5386
|
+
max-height: calc(100vh - 16px);
|
|
5387
|
+
overflow: auto;
|
|
5388
|
+
align-content: start;
|
|
5356
5389
|
}
|
|
5357
5390
|
}
|
|
5358
5391
|
@media (pointer: coarse) {
|
|
@@ -31,6 +31,11 @@ const insts = new WeakMap(); // host -> instance
|
|
|
31
31
|
|
|
32
32
|
/** report-biweekly -> biweekly. The same join as lib/report-needs.js cadenceOfSkill. */
|
|
33
33
|
const cadenceOfSkill = (skill) => { const m = /^report-([a-z][a-z0-9-]{0,39})$/.exec(String(skill || '')); return m ? m[1] : null; };
|
|
34
|
+
/** A template schedule (skill report-template) names its template in args. The same join as lib/report-needs.js cadenceOfSchedule. */
|
|
35
|
+
const cadenceOfSchedule = (x) => {
|
|
36
|
+
if (x && x.skill === 'report-template') { const a = String(x.args || '').trim(); return /^[a-z][a-z0-9-]{0,39}$/.test(a) ? a : null; }
|
|
37
|
+
return cadenceOfSkill(x && x.skill);
|
|
38
|
+
};
|
|
34
39
|
const keyOf = (r) => r.cadence + ':' + r.slot;
|
|
35
40
|
|
|
36
41
|
function whenOf(s) {
|
|
@@ -43,7 +48,7 @@ function make(host) {
|
|
|
43
48
|
const root = el('div', { class: 'rp-page', 'data-reports': '' });
|
|
44
49
|
const inst = {
|
|
45
50
|
host, root, ws: null, loaded: false, error: null, reports: [], flags: [], sig: null, schedules: [], doc: null, docKey: null,
|
|
46
|
-
sel: null, showAll: false, otherOpen: false, stop: null, seq: 0, last: '',
|
|
51
|
+
sel: null, showAll: false, otherOpen: false, locked: false, stop: null, seq: 0, last: '',
|
|
47
52
|
};
|
|
48
53
|
return inst;
|
|
49
54
|
}
|
|
@@ -56,6 +61,7 @@ export function mountDestination(host, ctx) {
|
|
|
56
61
|
if (ctx && ctx.FIXTURE) { inst.root.replaceChildren(empty({ text: COPY.fixture })); return; }
|
|
57
62
|
const repo = (ctx && ctx.tab && ctx.tab.repo) || (ctx && ctx.state && ctx.state.product) || null;
|
|
58
63
|
const ws = repo ? wsOf(repo) : null;
|
|
64
|
+
inst.repo = repo;
|
|
59
65
|
if (ws !== inst.ws) {
|
|
60
66
|
if (inst.stop) { inst.stop(); inst.stop = null; }
|
|
61
67
|
Object.assign(inst, { ws, loaded: false, error: null, reports: [], flags: [], sig: null, schedules: [], doc: null, docKey: null, sel: null, showAll: false, last: '' });
|
|
@@ -127,10 +133,10 @@ const isDraft = (r) => !!r && !r.issued;
|
|
|
127
133
|
function draftCadence(inst) {
|
|
128
134
|
const r = selected(inst);
|
|
129
135
|
if (r) return r.cadence;
|
|
130
|
-
const s = inst.schedules.map((x) =>
|
|
136
|
+
const s = inst.schedules.map((x) => cadenceOfSchedule(x)).find(Boolean);
|
|
131
137
|
return s || 'biweekly';
|
|
132
138
|
}
|
|
133
|
-
const knownCadences = (inst) => [...new Set([...inst.reports.map((r) => r.cadence), ...inst.schedules.map((s) =>
|
|
139
|
+
const knownCadences = (inst) => [...new Set([...inst.reports.map((r) => r.cadence), ...inst.schedules.map((s) => cadenceOfSchedule(s)).filter(Boolean)])].sort();
|
|
134
140
|
|
|
135
141
|
async function afterDraft(inst, r) {
|
|
136
142
|
if (r && r.ok && r.slot) inst.sel = (r.cadence || draftCadence(inst)) + ':' + r.slot;
|
|
@@ -140,7 +146,7 @@ async function afterDraft(inst, r) {
|
|
|
140
146
|
function primaryAction(inst) {
|
|
141
147
|
const r = selected(inst);
|
|
142
148
|
if (isDraft(r)) {
|
|
143
|
-
return button({ label: COPY.issue, variant: 'primary', attrs: { 'data-rp-act': 'issue', 'data-rp-key': 'primary' },
|
|
149
|
+
return button({ label: COPY.issue, variant: 'primary', disabled: inst.locked, attrs: { 'data-rp-act': 'issue', 'data-rp-key': 'primary' },
|
|
144
150
|
onClick: () => openIssueSheet({ ws: inst.ws, cadence: r.cadence, slot: r.slot, onDone: () => refresh(inst) }) });
|
|
145
151
|
}
|
|
146
152
|
const cadence = draftCadence(inst);
|
|
@@ -148,36 +154,48 @@ function primaryAction(inst) {
|
|
|
148
154
|
onClick: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence })) });
|
|
149
155
|
}
|
|
150
156
|
|
|
151
|
-
|
|
157
|
+
/** Set the recommended report schedules up (the one write that creates schedules; the schedules route, no new one). */
|
|
158
|
+
async function seedSchedules(inst) {
|
|
159
|
+
const r = await pmPost('/api/schedules/seed', { ws: inst.ws, set: 'reports' });
|
|
160
|
+
if (r && r.ok) toast({ text: COPY.schedulesSeeded((r.created || []).length), timeout: 4000 }); else toast({ text: COPY.schedulesFailed + (r && r.error ? ' ' + r.error : ''), tone: 'error' });
|
|
161
|
+
await refresh(inst);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* the schedules, split by what they feed; the selected report's schedule carries Run now above the list */
|
|
152
165
|
function schedulesBlock(inst) {
|
|
153
|
-
const feeds = inst.schedules.filter((s) =>
|
|
154
|
-
const others = inst.schedules.filter((s) => !
|
|
166
|
+
const feeds = inst.schedules.filter((s) => cadenceOfSchedule(s));
|
|
167
|
+
const others = inst.schedules.filter((s) => !cadenceOfSchedule(s));
|
|
168
|
+
const sel = selected(inst);
|
|
155
169
|
const sec = el('section', { class: 'rp-section', 'data-rp-section': 'schedules', 'aria-label': COPY.schedulesTitle }, el('h3', { class: 'rp-section__title' }, COPY.schedulesTitle));
|
|
156
170
|
if (!feeds.length) {
|
|
157
171
|
sec.appendChild(el('p', { class: 'rp-section__empty' }, COPY.schedulesNone));
|
|
158
|
-
sec.appendChild(button({ label: COPY.schedulesSeed, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'seed', 'data-rp-key': 'seed' }, onClick:
|
|
159
|
-
const r = await pmPost('/api/schedules/seed', { ws: inst.ws, set: 'reports' });
|
|
160
|
-
if (r && r.ok) toast({ text: COPY.schedulesSeeded((r.created || []).length), timeout: 4000 }); else toast({ text: COPY.schedulesFailed + (r && r.error ? ' ' + r.error : ''), tone: 'error' });
|
|
161
|
-
await refresh(inst);
|
|
162
|
-
} }));
|
|
172
|
+
sec.appendChild(button({ label: COPY.schedulesSeed, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'seed', 'data-rp-key': 'seed' }, onClick: () => seedSchedules(inst) }));
|
|
163
173
|
}
|
|
164
174
|
const list = el('div', { class: 'rp-list', role: 'list' });
|
|
165
175
|
for (const s of feeds) {
|
|
166
176
|
const on = s.enabled !== false;
|
|
167
|
-
const cadence =
|
|
177
|
+
const cadence = cadenceOfSchedule(s);
|
|
178
|
+
const isSel = !!sel && sel.cadence === cadence;
|
|
179
|
+
const trail = [button({ label: on ? COPY.pause : COPY.resume, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': on ? 'pause' : 'resume', 'data-rp-key': 'sched:' + s.id }, onClick: async () => {
|
|
180
|
+
const r = await pmPost('/api/schedules/update', { id: s.id, enabled: !on });
|
|
181
|
+
if (!r || !r.ok) { toast({ text: COPY.schedulesFailed + (r && r.error ? ' ' + r.error : ''), tone: 'error' }); }
|
|
182
|
+
await refresh(inst);
|
|
183
|
+
} })];
|
|
184
|
+
if (isSel) trail.push(button({ label: COPY.runNow, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'run', 'data-rp-key': 'run' },
|
|
185
|
+
onClick: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence })) }));
|
|
168
186
|
list.appendChild(row({
|
|
169
187
|
title: s.name || cadenceName(cadence),
|
|
170
|
-
meta: [
|
|
188
|
+
meta: [cadenceName(cadence), whenOf(s), s.nextRun ? COPY.nextRun(s.nextRun) : '', s.lastRun ? COPY.lastRan(s.lastRun) : COPY.neverRan],
|
|
171
189
|
signals: on ? [] : [signal({ text: COPY.paused, tone: 'warning' })],
|
|
172
|
-
trail
|
|
173
|
-
|
|
174
|
-
if (!r || !r.ok) { toast({ text: COPY.schedulesFailed + (r && r.error ? ' ' + r.error : ''), tone: 'error' }); }
|
|
175
|
-
await refresh(inst);
|
|
176
|
-
} })],
|
|
177
|
-
attrs: { 'data-schedule': s.id, 'data-cadence': cadence, role: 'listitem' },
|
|
190
|
+
trail,
|
|
191
|
+
attrs: { 'data-schedule': s.id, 'data-cadence': cadence, class: isSel ? 'rp-list__row rp-list__row--on' : 'rp-list__row', role: 'listitem' },
|
|
178
192
|
}));
|
|
179
193
|
}
|
|
180
194
|
if (feeds.length) sec.appendChild(list);
|
|
195
|
+
if (feeds.length && sel && !feeds.some((s) => cadenceOfSchedule(s) === sel.cadence)) {
|
|
196
|
+
sec.appendChild(el('p', { class: 'rp-section__empty' }, COPY.oneOff(sel.cadence)));
|
|
197
|
+
sec.appendChild(button({ label: COPY.makeRecurring, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'recurring', 'data-rp-key': 'recurring' }, onClick: () => seedSchedules(inst) }));
|
|
198
|
+
}
|
|
181
199
|
if (others.length) {
|
|
182
200
|
sec.appendChild(button({ label: COPY.otherSchedules(others.length), variant: 'ghost', size: 'sm', icon: inst.otherOpen ? 'chevron-down' : 'chevron-right',
|
|
183
201
|
attrs: { 'data-rp-act': 'other', 'data-rp-key': 'other', 'aria-expanded': inst.otherOpen ? 'true' : 'false' }, onClick: () => { inst.otherOpen = !inst.otherOpen; draw(inst); } }));
|
|
@@ -192,7 +210,7 @@ function schedulesBlock(inst) {
|
|
|
192
210
|
|
|
193
211
|
/* the reports, newest first */
|
|
194
212
|
function listBlock(inst) {
|
|
195
|
-
const sec = el('section', { class: 'rp-section', 'data-rp-section': 'list', 'aria-label': COPY.list }, el('h3', { class: 'rp-section__title' }, COPY.list));
|
|
213
|
+
const sec = el('section', { class: 'rp-section rp-card', 'data-rp-section': 'list', 'aria-label': COPY.list }, el('h3', { class: 'rp-section__title' }, COPY.list));
|
|
196
214
|
if (!inst.reports.length) { sec.appendChild(el('p', { class: 'rp-section__empty' }, COPY.none)); return sec; }
|
|
197
215
|
const shown = inst.showAll ? inst.reports : inst.reports.slice(0, SHOWN);
|
|
198
216
|
// the selected report stays on screen even when it is older than the shown ones
|
|
@@ -203,7 +221,7 @@ function listBlock(inst) {
|
|
|
203
221
|
const key = keyOf(r);
|
|
204
222
|
list.appendChild(row({
|
|
205
223
|
title: COPY.rowTitle(r.cadence, r.slot),
|
|
206
|
-
meta: [r.issued ? COPY.issued(r.issued.at, r.issued.by) : COPY.draft],
|
|
224
|
+
meta: [cadenceName(r.cadence), r.issued ? COPY.issued(r.issued.at, r.issued.by) : COPY.draft],
|
|
207
225
|
signals: r.edited ? [signal({ text: COPY.edited, tone: 'warning', title: COPY.editedWhy })] : [],
|
|
208
226
|
owed: isDraft(r),
|
|
209
227
|
onOpen: () => selectReport(inst, key),
|
|
@@ -222,9 +240,10 @@ function listBlock(inst) {
|
|
|
222
240
|
/* the selected report and what can be done to it */
|
|
223
241
|
function viewerBlock(inst) {
|
|
224
242
|
const r = selected(inst);
|
|
225
|
-
const sec = el('section', { class: 'rp-section rp-viewer', 'data-rp-section': 'viewer', 'aria-label': r ? COPY.reportName(r.cadence, r.slot) : COPY.title });
|
|
243
|
+
const sec = el('section', { class: 'rp-section rp-viewer rp-card', 'data-rp-section': 'viewer', 'aria-label': r ? COPY.reportName(r.cadence, r.slot) : COPY.title });
|
|
226
244
|
if (!r) return sec;
|
|
227
245
|
const doc = inst.doc && inst.docKey === inst.sel ? inst.doc : null;
|
|
246
|
+
sec.appendChild(el('p', { class: 'rp-viewer__type' }, COPY.typeSummary(r.cadence, r.issued ? COPY.issuedState : COPY.draft)));
|
|
228
247
|
const bar = el('div', { class: 'rp-toolbar', role: 'toolbar', 'aria-label': COPY.reportName(r.cadence, r.slot) });
|
|
229
248
|
bar.appendChild(el('h3', { class: 'rp-section__title rp-toolbar__title' }, COPY.reportName(r.cadence, r.slot)));
|
|
230
249
|
const acts = el('div', { class: 'rp-toolbar__acts' });
|
|
@@ -232,41 +251,74 @@ function viewerBlock(inst) {
|
|
|
232
251
|
const present = button({ label: COPY.present, variant: 'outline', size: 'sm', icon: 'columns-2', attrs: { 'data-rp-act': 'present', 'data-rp-key': 'present' }, disabled: !doc,
|
|
233
252
|
onClick: () => doc && openSlides({ md: doc.markdown, label: COPY.reportName(r.cadence, r.slot), printable: !!r.issued, returnFocus: present }) });
|
|
234
253
|
acts.appendChild(present);
|
|
254
|
+
acts.appendChild(button({ label: inst.locked ? COPY.unlock : COPY.lock, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'lock', 'data-rp-key': 'lock', 'aria-pressed': inst.locked ? 'true' : 'false' },
|
|
255
|
+
onClick: () => { inst.locked = !inst.locked; draw(inst); } }));
|
|
256
|
+
acts.appendChild(button({ label: COPY.fullScreen, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'full', 'data-rp-key': 'full' },
|
|
257
|
+
onClick: () => { inst.locked = true; draw(inst); goFullScreen(); } }));
|
|
235
258
|
if (r.issued) {
|
|
236
259
|
acts.appendChild(button({ label: COPY.print, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'print', 'data-rp-key': 'print' }, disabled: !doc, onClick: () => doc && printReport(doc.markdown) }));
|
|
237
260
|
acts.appendChild(button({ label: COPY.copyMd, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'copy', 'data-rp-key': 'copy' }, disabled: !doc, onClick: () => doc && copyMarkdown(doc.markdown) }));
|
|
238
261
|
} else {
|
|
239
|
-
acts.appendChild(button({ label: COPY.editNotes, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'notes', 'data-rp-key': 'notes' }, disabled: !doc,
|
|
262
|
+
acts.appendChild(button({ label: COPY.editNotes, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'notes', 'data-rp-key': 'notes' }, disabled: !doc || inst.locked,
|
|
240
263
|
onClick: () => doc && openNotesSheet({ ws: inst.ws, cadence: r.cadence, slot: r.slot, markdown: doc.markdown, onSaved: () => refresh(inst) }) }));
|
|
241
|
-
acts.appendChild(button({ label: COPY.redraft, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'redraft', 'data-rp-key': 'redraft' },
|
|
264
|
+
acts.appendChild(button({ label: COPY.redraft, variant: 'outline', size: 'sm', attrs: { 'data-rp-act': 'redraft', 'data-rp-key': 'redraft' }, disabled: inst.locked,
|
|
242
265
|
onClick: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence: r.cadence, slot: r.slot, redraft: true })) }));
|
|
243
266
|
const more = iconButton({ icon: 'ellipsis', ariaLabel: COPY.more, expanded: false, size: 'sm', attrs: { 'data-rp-act': 'more', 'data-rp-key': 'more' }, onClick: (e) => {
|
|
244
267
|
const items = [{ label: COPY.draftNow, onSelect: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence: r.cadence })) }];
|
|
245
268
|
for (const c of knownCadences(inst)) if (c !== r.cadence) items.push({ label: COPY.draftNow + ' · ' + c, onSelect: async () => afterDraft(inst, await draftNow({ ws: inst.ws, cadence: c })) });
|
|
246
|
-
items.push({ sep: true }, { label: COPY.print, onSelect: () => doc && printReport(doc.markdown) }, { label: COPY.copyMd, onSelect: () => doc && copyMarkdown(doc.markdown) });
|
|
269
|
+
items.push({ sep: true }, { label: COPY.openTab, onSelect: () => openReportTab(inst, r) }, { label: COPY.print, onSelect: () => doc && printReport(doc.markdown) }, { label: COPY.copyMd, onSelect: () => doc && copyMarkdown(doc.markdown) });
|
|
247
270
|
menu({ anchor: e.currentTarget, items, ariaLabel: COPY.more });
|
|
248
271
|
} });
|
|
249
272
|
acts.appendChild(more);
|
|
250
273
|
}
|
|
251
274
|
bar.appendChild(acts);
|
|
252
275
|
sec.appendChild(bar);
|
|
276
|
+
if (inst.locked) sec.appendChild(el('p', { class: 'rp-viewer__state' }, COPY.locked));
|
|
253
277
|
if (r.issued) sec.appendChild(el('p', { class: 'rp-viewer__state' }, r.edited ? COPY.editedWhy : COPY.frozen));
|
|
254
278
|
// the report is read as its slides (Run 6): the PM-OS deck, scaled to the column
|
|
255
279
|
sec.appendChild(doc ? slideCards(doc.markdown) : el('p', { class: 'rp-section__empty' }, COPY.loading));
|
|
256
280
|
return sec;
|
|
257
281
|
}
|
|
258
282
|
|
|
283
|
+
/** New report: a session that brainstorms one report and saves its structure as a template (skill report-new). */
|
|
284
|
+
function newReportButton(inst) {
|
|
285
|
+
return button({ label: COPY.newReport, variant: 'outline', attrs: { 'data-rp-act': 'new-report', 'data-rp-key': 'new' }, onClick: () => openNewReport(inst) });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function openNewReport(inst) {
|
|
289
|
+
const { openTab } = await import('./panel-initiatives.js');
|
|
290
|
+
openTab({ id: 'skill:report-new:' + inst.ws, kind: 'skill', title: COPY.newReport, skillId: 'report-new', repo: inst.repo || null, slug: null, role: 'SKILL' });
|
|
291
|
+
const f = window.pmApp && window.pmApp.frame;
|
|
292
|
+
if (f) f.openDock();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** A full report as a dock tab: the tab id is the report key, so opening it again focuses the tab that is already open. */
|
|
296
|
+
async function openReportTab(inst, r) {
|
|
297
|
+
const { openTab } = await import('./panel-initiatives.js');
|
|
298
|
+
openTab({ id: 'report:' + inst.ws + ':' + keyOf(r), kind: 'report', title: COPY.reportName(r.cadence, r.slot), ws: inst.ws, cadence: r.cadence, slot: r.slot, repo: null, role: 'REPORT' });
|
|
299
|
+
const f = window.pmApp && window.pmApp.frame;
|
|
300
|
+
if (f) f.openDock();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Full screen: the page is read-only while it is shown full, so the lock is set first. A refused request leaves the page as it is. */
|
|
304
|
+
function goFullScreen() {
|
|
305
|
+
try {
|
|
306
|
+
const p = document.documentElement.requestFullscreen();
|
|
307
|
+
if (p && typeof p.catch === 'function') p.catch(() => {});
|
|
308
|
+
} catch (_) { /* the browser has no Full screen here; the lock is already on */ }
|
|
309
|
+
}
|
|
310
|
+
|
|
259
311
|
/** Rebuild the persistent root from the instance, if what it shows changed; keep the focus on the same control. */
|
|
260
312
|
function draw(inst) {
|
|
261
313
|
const root = inst.root;
|
|
262
314
|
if (!inst.ws) { root.replaceChildren(empty({ text: COPY.noRepo })); inst.last = 'none'; return; }
|
|
263
|
-
const fingerprint = JSON.stringify([inst.loaded, inst.error, inst.reports, inst.flags, inst.schedules, inst.sel, inst.doc && inst.doc.sha, inst.showAll, inst.otherOpen]);
|
|
315
|
+
const fingerprint = JSON.stringify([inst.loaded, inst.error, inst.reports, inst.flags, inst.schedules, inst.sel, inst.doc && inst.doc.sha, inst.showAll, inst.otherOpen, inst.locked]);
|
|
264
316
|
if (fingerprint === inst.last && root.childNodes.length) return;
|
|
265
317
|
inst.last = fingerprint;
|
|
266
318
|
const keep = document.activeElement && root.contains(document.activeElement) ? document.activeElement.getAttribute('data-rp-key') : null;
|
|
267
319
|
const scroll = root.querySelector('.rp-viewer') ? root.scrollTop : 0;
|
|
268
320
|
if (!inst.loaded) { root.replaceChildren(el('p', { class: 'rp-section__empty' }, COPY.loading)); return; }
|
|
269
|
-
const kids = [el('div', { class: 'rp-head' }, el('h2', { class: 'rp-head__title' }, COPY.title), inst.error ? null : primaryAction(inst))];
|
|
321
|
+
const kids = [el('div', { class: 'rp-head' }, el('h2', { class: 'rp-head__title' }, COPY.title), inst.error ? null : el('div', { class: 'rp-head__acts' }, primaryAction(inst), newReportButton(inst)))];
|
|
270
322
|
if (inst.error) kids.push(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, inst.error));
|
|
271
323
|
if (inst.flags && inst.flags.length) kids.push(el('p', { class: 'rp-flag' }, signal({ text: COPY.flagTitle, tone: 'warning', title: inst.flags[0].why })));
|
|
272
324
|
if (!inst.error) kids.push(el('div', { class: 'rp-cols' }, el('div', { class: 'rp-col rp-col--list' }, schedulesBlock(inst), listBlock(inst)), el('div', { class: 'rp-col rp-col--doc' }, viewerBlock(inst))));
|
|
@@ -244,7 +244,7 @@ function go(screen) {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
const frameOn = () => !!(ctx && ctx.frame);
|
|
247
|
-
const tabScreen = (tab, id) => ({ kind: (tab && (tab.kind === 'doc' || tab.kind === 'breakdown' || tab.kind === 'dest')) ? 'doc' : 'terminal', id, title: (tab && (tab.title || tab.name)) || String(id) });
|
|
247
|
+
const tabScreen = (tab, id) => ({ kind: (tab && (tab.kind === 'doc' || tab.kind === 'breakdown' || tab.kind === 'dest' || tab.kind === 'report')) ? 'doc' : 'terminal', id, title: (tab && (tab.title || tab.name)) || String(id) });
|
|
248
248
|
|
|
249
249
|
/* The frame on a phone: the PM's own moves through the panels, the palette or Settings, read back as screens. A place
|
|
250
250
|
* opened from elsewhere (⌘K, Settings › Show its initiatives) is a tab switch; Settings is pushed on top; an open
|
|
@@ -42,6 +42,7 @@ const liveId = () => (V ? V.listeningId() : null);
|
|
|
42
42
|
const titleOf = () => { if (!active) return ''; const r = noteById(active.id); return (V && V.titleOf(active.id)) || (r && r.title) || active.title || 'Untitled note'; };
|
|
43
43
|
|
|
44
44
|
export const activeId = () => (active ? active.id : null);
|
|
45
|
+
export const isRecording = () => !!(active && V && liveId() === active.id);
|
|
45
46
|
|
|
46
47
|
export function mountDock() {
|
|
47
48
|
if (host || FIXTURE || typeof document === 'undefined') return;
|
|
@@ -47,7 +47,7 @@ export const VIEWS = {
|
|
|
47
47
|
// meeting-notes Run 1: typed notes, dictation, meeting transcripts and summaries; the PM's own, the same in every repository
|
|
48
48
|
notes: { label: 'notes', mode: null, scoped: false, palette: true, hint: 'type, dictate or record a meeting' },
|
|
49
49
|
// meeting-notes Run 11: notes menu with two-column view grouped by initiative (R47)
|
|
50
|
-
notesmenu: { label: 'notes menu', mode: null, scoped: false,
|
|
50
|
+
notesmenu: { label: 'notes menu', mode: null, scoped: false, hint: 'view and organize all notes by initiative' },
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
/** The view a list mode opens as, or null when the mode is not a view. `product` is the list itself. */
|
|
@@ -63,6 +63,11 @@ import('./panel-breakdown.js')
|
|
|
63
63
|
.then((m) => { breakdownMod = m; if ((state.tabs || []).some((t) => t.kind === 'breakdown')) setState({}); })
|
|
64
64
|
.catch(() => { breakdownMod = null; });
|
|
65
65
|
|
|
66
|
+
let reportTabMod = null;
|
|
67
|
+
import('./report-tab.js')
|
|
68
|
+
.then((m) => { reportTabMod = m; if ((state.tabs || []).some((t) => t.kind === 'report')) setState({}); })
|
|
69
|
+
.catch(() => { reportTabMod = null; });
|
|
70
|
+
|
|
66
71
|
/** `{{ expr }}` — one interpolated value in its own span, the way the prototype's runtime emits it. */
|
|
67
72
|
/* A dock control's face: a lucide icon from the vendored sprite, then its word (C6: no Unicode glyph controls). */
|
|
68
73
|
const iconLabel = (name, text) => (text ? [icon(name), el('span', null, text)] : [icon(name)]);
|
|
@@ -1043,6 +1048,8 @@ function paneFor(tab) {
|
|
|
1043
1048
|
if (cockpitMod && typeof cockpitMod.mountView === 'function') cockpitMod.mountView(host, tab);
|
|
1044
1049
|
} else if (tab.kind === 'web') {
|
|
1045
1050
|
mountWeb(host, tab); // overhaul-r5 — never falls through to an empty doc tab
|
|
1051
|
+
} else if (tab.kind === 'report') {
|
|
1052
|
+
if (reportTabMod && typeof reportTabMod.mountReportTab === 'function') reportTabMod.mountReportTab(host, tab);
|
|
1046
1053
|
} else if (tab.kind === 'breakdown') {
|
|
1047
1054
|
// pm-os-absorb Run 1 — an initiative's items (Tree, Board, item sheet); an empty pane until the module lands
|
|
1048
1055
|
if (breakdownMod && typeof breakdownMod.mountBreakdown === 'function') breakdownMod.mountBreakdown(host, tab);
|
|
@@ -1205,7 +1212,7 @@ function cockpitTabNode(t, act, splitTab, vertical) {
|
|
|
1205
1212
|
const dotColor = FX.TAB_COLORS.dot[t.kind] || FX.TAB_COLORS.dot.doc;
|
|
1206
1213
|
const meta = t.kind === 'term'
|
|
1207
1214
|
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
1208
|
-
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
1215
|
+
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.kind === 'report' ? 'report' : t.role);
|
|
1209
1216
|
const br = tabBranch(t);
|
|
1210
1217
|
return el('div', {
|
|
1211
1218
|
'data-tab': t.id, 'data-tab-active': front ? '1' : '0',
|
|
@@ -1242,7 +1249,7 @@ function cockpitTabRow(t, front) {
|
|
|
1242
1249
|
const dotColor = FX.TAB_COLORS.dot[t.kind] || FX.TAB_COLORS.dot.doc;
|
|
1243
1250
|
const meta = t.kind === 'term'
|
|
1244
1251
|
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
1245
|
-
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
1252
|
+
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.kind === 'report' ? 'report' : t.role);
|
|
1246
1253
|
const br = tabBranch(t);
|
|
1247
1254
|
return el('div', {
|
|
1248
1255
|
'data-tab': t.id, 'data-tab-active': front ? '1' : '0', 'data-tab-row': '',
|
|
@@ -1319,7 +1326,7 @@ function tabNode(t, act, splitTab) {
|
|
|
1319
1326
|
* fixture render keeps the design's literal — the comparator reads this text node. */
|
|
1320
1327
|
const meta = t.kind === 'term'
|
|
1321
1328
|
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
1322
|
-
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
1329
|
+
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.kind === 'report' ? 'report' : t.role);
|
|
1323
1330
|
|
|
1324
1331
|
return el('div', {
|
|
1325
1332
|
style: 'flex:none;display:flex;align-items:center;gap:8px;padding:0 10px;background:' + bg
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* pm/report-tab.js — a full report, opened as a tab in the dock (reports-redesign follow-up).
|
|
2
|
+
*
|
|
3
|
+
* The dock tab reads the same draft the Reports viewer reads, and draws it whole: the report as a document,
|
|
4
|
+
* not its slide cards. Print and Copy are the viewer's own actions. The tab writes nothing; a report is read here
|
|
5
|
+
* and changed only from the Reports page, where the lock applies.
|
|
6
|
+
*
|
|
7
|
+
* mountReportTab(host, tab) tab: { id, ws, cadence, slot, title }
|
|
8
|
+
*/
|
|
9
|
+
import { el } from './dom.js';
|
|
10
|
+
import { button, ensureComponentsCss } from './primitives.js';
|
|
11
|
+
import { pmGet } from './http.js';
|
|
12
|
+
import { COPY } from './reports-copy.js';
|
|
13
|
+
import { renderReport, printReport, copyMarkdown } from './report-view.js';
|
|
14
|
+
|
|
15
|
+
export function mountReportTab(host, tab) {
|
|
16
|
+
const root = host.firstElementChild;
|
|
17
|
+
if (root && root.getAttribute('data-report-tab') === tab.id) return;
|
|
18
|
+
ensureComponentsCss();
|
|
19
|
+
const page = el('div', { class: 'rp-page rp-tab', 'data-report-tab': tab.id, style: 'flex:1;min-height:0;overflow:auto;padding:12px 16px' });
|
|
20
|
+
host.replaceChildren(page);
|
|
21
|
+
page.appendChild(el('p', { class: 'rp-section__empty' }, COPY.loading));
|
|
22
|
+
const q = '/api/report/draft?ws=' + encodeURIComponent(tab.ws) + '&cadence=' + encodeURIComponent(tab.cadence) + '&slot=' + encodeURIComponent(tab.slot);
|
|
23
|
+
pmGet(q).then((out) => {
|
|
24
|
+
if (!page.isConnected) return;
|
|
25
|
+
if (!out || !out.ok || !out.exists) { page.replaceChildren(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, COPY.readError)); return; }
|
|
26
|
+
const acts = el('div', { class: 'rp-toolbar__acts' },
|
|
27
|
+
button({ label: COPY.print, variant: 'outline', size: 'sm', onClick: () => printReport(out.markdown) }),
|
|
28
|
+
button({ label: COPY.copyMd, variant: 'outline', size: 'sm', onClick: () => copyMarkdown(out.markdown) }));
|
|
29
|
+
page.replaceChildren(el('div', { class: 'rp-toolbar' }, el('h3', { class: 'rp-section__title rp-toolbar__title' }, tab.title || ''), acts), renderReport(out.markdown));
|
|
30
|
+
}).catch(() => {
|
|
31
|
+
if (page.isConnected) page.replaceChildren(el('p', { class: 'rp-section__empty rp-section__empty--bad', role: 'alert' }, COPY.readError));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -48,6 +48,8 @@ export const COPY = {
|
|
|
48
48
|
reportName: (cadence, slot) => cadenceName(cadence) + ' report, ' + shortDate(slot),
|
|
49
49
|
rowTitle: (cadence, slot) => cadenceName(cadence) + ' · ' + shortDate(slot),
|
|
50
50
|
draft: 'Draft',
|
|
51
|
+
issuedState: 'Issued',
|
|
52
|
+
typeSummary: (cadence, state) => 'Slides · ' + cadenceName(cadence) + ' · ' + state,
|
|
51
53
|
issued: (at, by) => 'Issued ' + (dayOf(at) || '') + (by ? ' by ' + by : ''),
|
|
52
54
|
edited: 'Edited after issue',
|
|
53
55
|
editedWhy: 'This file changed after it was issued. The issued bytes are in the issues ledger.',
|
|
@@ -94,6 +96,10 @@ export const COPY = {
|
|
|
94
96
|
schedulesFailed: 'The schedules were not changed.',
|
|
95
97
|
pause: 'Pause',
|
|
96
98
|
resume: 'Resume',
|
|
99
|
+
runNow: 'Run now',
|
|
100
|
+
nextRun: (iso) => (dayOf(iso) ? 'next ' + dayOf(iso) : ''),
|
|
101
|
+
oneOff: (cadence) => 'No schedule drafts this ' + cadence + ' report. Make it recurring to draft it on a schedule.',
|
|
102
|
+
makeRecurring: 'Make recurring',
|
|
97
103
|
paused: 'Paused',
|
|
98
104
|
pausedMsg: (name) => name + ' is paused.',
|
|
99
105
|
resumed: (name) => name + ' runs again.',
|
|
@@ -105,7 +111,13 @@ export const COPY = {
|
|
|
105
111
|
|
|
106
112
|
flagTitle: 'The issues ledger has a line that is not read',
|
|
107
113
|
|
|
114
|
+
openTab: 'Open as tab',
|
|
115
|
+
newReport: 'New report',
|
|
108
116
|
present: 'Present',
|
|
117
|
+
lock: 'Lock',
|
|
118
|
+
unlock: 'Unlock',
|
|
119
|
+
locked: 'Locked. Edit notes, Redraft and Issue are off until you unlock.',
|
|
120
|
+
fullScreen: 'Full screen',
|
|
109
121
|
};
|
|
110
122
|
|
|
111
123
|
export const GLOSSARY = {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { el, setChildren } from './dom.js';
|
|
22
22
|
import { button, needsDot, needsCount } from './primitives.js';
|
|
23
23
|
import { COPY } from './frame-copy.js';
|
|
24
|
+
import { isRecording } from './note-dock.js';
|
|
24
25
|
|
|
25
26
|
/** The destinations the bar names, in order. Settings sits at the end of the bar, after Sync. Docs joined the bar at the
|
|
26
27
|
* PM's word (2026-10-06, docs-three-way-sync: "I thought Docs is one of the main menu"). People joined after Docs when the
|
|
@@ -39,13 +40,19 @@ export function unsyncedOf(st) {
|
|
|
39
40
|
/** The lucide symbol each place wears in the bar (UI comfort, 2026-10-07: a word alone is slow to find; the phone's tab bar already wears these). */
|
|
40
41
|
const NAV_ICON = Object.freeze({ today: 'house', mywork: 'list-checks', initiatives: 'target', board: 'square-kanban', reports: 'chart-column', docs: 'file-text', people: 'users', settings: 'settings' });
|
|
41
42
|
|
|
42
|
-
export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
|
|
43
|
+
export function createTopbar({ go, openPalette, openView, repoMenu, sync } = {}) {
|
|
43
44
|
const node = el('header', { class: 'fr-bar', 'data-shell': 'top', 'data-fr': 'bar' });
|
|
44
45
|
|
|
45
46
|
let repoBtn = null;
|
|
46
47
|
let repoLabel = null;
|
|
47
48
|
const repoSlot = el('div', { class: 'fr-bar__repo' });
|
|
48
49
|
|
|
50
|
+
const recordingIndicator = el('span', {
|
|
51
|
+
class: 'fr-recording-dot', 'data-fr-recording': '',
|
|
52
|
+
title: 'Notes are being recorded',
|
|
53
|
+
'aria-hidden': 'true',
|
|
54
|
+
});
|
|
55
|
+
|
|
49
56
|
const count = needsCount('0', { 'data-today-badge': '' });
|
|
50
57
|
const dot = needsDot();
|
|
51
58
|
const links = {};
|
|
@@ -80,6 +87,12 @@ export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
|
|
|
80
87
|
});
|
|
81
88
|
search.appendChild(el('kbd', { class: 'fr-kbd' }, COPY.searchKey));
|
|
82
89
|
|
|
90
|
+
const notes = button({
|
|
91
|
+
label: 'Notes', variant: 'outline', icon: 'notebook-pen',
|
|
92
|
+
attrs: { class: 'fr-notes', 'data-fr-view': 'notes', title: 'Open notes menu (all notes by initiative)' },
|
|
93
|
+
onClick: () => openView && openView('notesmenu'),
|
|
94
|
+
});
|
|
95
|
+
|
|
83
96
|
let syncBtn = null;
|
|
84
97
|
let syncLabel = null;
|
|
85
98
|
|
|
@@ -116,7 +129,8 @@ export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
|
|
|
116
129
|
syncLabel = want;
|
|
117
130
|
syncBtn = want ? syncButton(want, !!model.syncing) : null;
|
|
118
131
|
}
|
|
119
|
-
|
|
132
|
+
recordingIndicator.classList.toggle('fr-recording-dot--active', isRecording());
|
|
133
|
+
setChildren(node, [repoSlot, recordingIndicator, nav, search, notes, syncBtn, links.settings]);
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
update({});
|
|
@@ -181,10 +181,10 @@ export async function renderBoard(inner, ctx) {
|
|
|
181
181
|
})()
|
|
182
182
|
: el('div', { class: 'nt-menu-right' }, empty({ text: 'Select a note to view details' }));
|
|
183
183
|
|
|
184
|
-
setChildren(root, leftPanel, rightPanel);
|
|
184
|
+
setChildren(root, [leftPanel, rightPanel]);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
render();
|
|
187
|
+
await render();
|
|
188
188
|
ctx.setState = (state) => { render(); };
|
|
189
189
|
return root;
|
|
190
190
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: report-new
|
|
3
|
+
label: "New report (brainstorm, then save as a template)"
|
|
4
|
+
description: "Brainstorm one new recurring or ad hoc report with the PM: its purpose, audience, period, sections and format. Saves only the chosen structure as a template and registers its schedule. Never saves the sample artifacts."
|
|
5
|
+
run: agent
|
|
6
|
+
agent: claude
|
|
7
|
+
output:
|
|
8
|
+
- Template
|
|
9
|
+
- Schedule
|
|
10
|
+
- First draft
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
/report-new — help the PM design one new report, then save its structure as a template. You brainstorm, then write one definition file and register one schedule. The sample text, the slides and any other draft from the session are not saved.
|
|
14
|
+
|
|
15
|
+
Rules that always hold (D71):
|
|
16
|
+
1. DRAFT ONLY. A report is issued by the PM from the Reports page. Never send, post, share, email, publish or upload a report.
|
|
17
|
+
2. READ-ONLY on every source. Never change mail, chat, Jira, Confluence, calendar or any other source.
|
|
18
|
+
3. SAVE ONLY THE STRUCTURE. The template is the chosen structure: id, cadence, audience, sections, purpose and tone. Never save a sample, a slide or a draft as a template.
|
|
19
|
+
|
|
20
|
+
Procedure:
|
|
21
|
+
1. Ask, one question at a time: what the report is for; who reads it; the sources it should draw on; the sections it needs; and the format (slides, a document, or a message). Offer a sensible default for each and keep going unless the PM corrects it.
|
|
22
|
+
2. Ask for the period. Accept a cadence word (daily, weekly, bi-weekly, monthly), a single date, a range, or a phrase such as "every Tuesday", "every 2 weeks on Monday" or "the first Monday of the month". Map the phrase to the nearest schedule cadence and say what you chose. For a one-off report, ask for its date and set no recurring schedule.
|
|
23
|
+
3. Propose a template id: lowercase, starting with a letter, up to 40 characters from a-z, 0-9 and hyphens. Confirm it with the PM.
|
|
24
|
+
4. Write `<state>/reports/definitions/<id>.md`, where <state> is `pmos/state/` if it exists, otherwise `planning/`. Use this frontmatter, then the prose: `type: Reference`, `title`, `timestamp` (today), `cadence: <id>`, `audience`, and `sections:` as a list. Put the purpose, sources, length and tone in the body.
|
|
25
|
+
5. If the period is recurring, register the schedule: `pmos schedule add --skill report-template --cadence <daily|weekly|bi-weekly|monthly> --args <id> --name "<Name>"`. Use the cadence from step 2. The schedule drafts the report; it never issues one.
|
|
26
|
+
6. Run the first draft now: `pmos report draft <id>`. Then say where it is, and that the PM issues it from Reports.
|
|
27
|
+
7. Report in one short message: the template path, the schedule (or the one-off date), and the first draft path.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: report-template
|
|
3
|
+
label: "Saved report (from its template)"
|
|
4
|
+
description: "Prepare a saved report as a draft. The schedule's args name the template; the script writes every number, then this skill writes the notes between the markers. Never sends or posts."
|
|
5
|
+
run: agent
|
|
6
|
+
agent: claude
|
|
7
|
+
output:
|
|
8
|
+
- Headline
|
|
9
|
+
- Sections from the template
|
|
10
|
+
- Needs your call
|
|
11
|
+
- Sources reached and not reached
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
/report-template — prepare a saved report for THIS repository as a draft. The template id is the schedule's args, for example `quarterly-ops`. A script with no model writes the draft and every number in it; you write the words between the note markers.
|
|
15
|
+
|
|
16
|
+
Rules that always hold (D71):
|
|
17
|
+
1. DRAFT ONLY. You write a file in this repository. Never send, post, share, email, publish or upload the report anywhere; the PM does that.
|
|
18
|
+
2. READ-ONLY on every source. Never send, reply, forward, post, comment, create, edit, transition, label, archive or delete anything in mail, chat, Jira, Confluence, calendar or any other source.
|
|
19
|
+
3. SOURCED. Every claim carries a pointer the reader can open: an initiative id, a file path in this repository, or an acceptance. A claim you cannot point to is left out or marked "unverified".
|
|
20
|
+
4. NOTHING INVENTED. The script lists the sources it reached and did not reach under "Sources reached and not reached": that section is not yours to edit.
|
|
21
|
+
5. THE TEMPLATE'S RULES. The script shapes the file from the template: its sections, their order and its blocks. The template's purpose, audience, length and tone govern your words. Do not substitute your own format.
|
|
22
|
+
6. THE SCRIPT WRITES EVERY NUMBER. In a note you type no count, ratio, percentage or date.
|
|
23
|
+
7. PROSE ONLY BETWEEN NOTE MARKERS. Write only between a `<!-- pmos:note <id> -->` line and its `<!-- /pmos:note -->` line. Never edit, move or delete a marker or any line outside one. Never add a section to the file.
|
|
24
|
+
|
|
25
|
+
Procedure:
|
|
26
|
+
1. The template id is the schedule's `args`. It must match `^[a-z][a-z0-9-]{0,39}$`. If it is missing or does not match, stop and say which schedule has no valid template id.
|
|
27
|
+
2. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
|
|
28
|
+
3. Read the template: `<state>/reports/definitions/<id>.md`. If it does not exist, stop and say the template is missing. Do not invent one.
|
|
29
|
+
4. Run `pmos report draft <id>`. It writes `<state>/reports/<id>/YYYY-MM-DD.md`, with one empty note marker for each block.
|
|
30
|
+
- If it refuses because a draft for the slot exists, do not overwrite it. Write your notes into the file that exists.
|
|
31
|
+
- If it refuses because the slot is issued, stop. An issued report is frozen. Say so in one line.
|
|
32
|
+
- If it refuses for any other reason, stop and quote the refusal. Do not write the file yourself.
|
|
33
|
+
5. Read the draft, then the sources it names. Write one or two sentences for each `<!-- pmos:note <block-id> -->`. Start with "Warning:" when a block needs attention. Leave any note that already has text.
|
|
34
|
+
6. Run `pmos report draft <id> --redraft` so the script rewrites the file and keeps every note. Read the file once more.
|