create-pmos 0.92.0 → 0.94.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/guide/today.md +1 -1
- package/workspace/lib/kit-update.js +8 -0
- package/workspace/lib/report-needs.js +8 -2
- package/workspace/lib/routes-notes.js +12 -1
- 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 +172 -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 +3 -1
- package/workspace/public/pm/panel-tabs.js +10 -3
- package/workspace/public/pm/primitives.js +2 -2
- package/workspace/public/pm/report-tab.js +33 -0
- package/workspace/public/pm/reports-copy.js +12 -0
- package/workspace/public/pm/today-copy.js +2 -0
- package/workspace/public/pm/today-day.js +43 -2
- package/workspace/public/pm/topbar.js +16 -2
- package/workspace/public/pm/view-notes-menu.js +190 -0
- package/workspace/public/pm/view-notes.js +5 -17
- package/workspace/skills/report-new.skill +27 -0
- package/workspace/skills/report-template.skill +34 -0
- package/workspace/test-files.json +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.94.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:17:17Z"}
|
|
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:17:17Z"}
|
|
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:17:17Z"}
|
|
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**.
|
package/workspace/guide/today.md
CHANGED
|
@@ -32,7 +32,7 @@ An agent job can stop with its session still open, for example when the agent as
|
|
|
32
32
|
|
|
33
33
|
Each job session is named "Job ·" and the first words of the job, so the session list stays short.
|
|
34
34
|
|
|
35
|
-
The
|
|
35
|
+
The left column shows one section at a time. A row of tabs at the top chooses it: **Agenda**, **Goals**, **Jobs**, **Mentions** and **Result**. Agenda is the first one shown. The column remembers your last choice in this browser. When you select a job, a goal or a mention, the column goes to its tab. The Agenda tab holds the month. **Result** counts the jobs and lists each finished one with its draft summary or the reason it failed. The **Goals** tab holds your goals for today. The day is open when you go to Agenda, also before you press **Prepare my day**. The other things that wait for you, such as outcome checks, report drafts and sessions, are jobs too. Each one is a row of **Needs you** in the jobs. Press it to see its card, with its own buttons (for example **Draft now**), in the detail column. When the day is folded, they show as cards under it.
|
|
36
36
|
|
|
37
37
|
### The Agenda month
|
|
38
38
|
|
|
@@ -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 };
|
|
@@ -75,7 +75,7 @@ const VOCAB_TTL_MS = 60 * 1000;
|
|
|
75
75
|
const prefixes = ['/api/notes'];
|
|
76
76
|
const POSTS = new Set(['/api/notes/new', '/api/notes/save', '/api/notes/dictate', '/api/notes/record', '/api/notes/transcript',
|
|
77
77
|
'/api/notes/private', '/api/notes/pause', '/api/notes/summarize', '/api/notes/summarize-agent', '/api/notes/delete', '/api/notes/audio', '/api/notes/label',
|
|
78
|
-
'/api/notes/speaker', '/api/notes/switch', '/api/notes/whisper-setup', '/api/notes/final', '/api/notes/final-setup', '/api/notes/line', '/api/notes/recording-delete', '/api/notes/link', '/api/notes/bullet']);
|
|
78
|
+
'/api/notes/speaker', '/api/notes/switch', '/api/notes/whisper-setup', '/api/notes/final', '/api/notes/final-setup', '/api/notes/line', '/api/notes/recording-delete', '/api/notes/link', '/api/notes/bullet', '/api/notes/create-task']);
|
|
79
79
|
|
|
80
80
|
/* ── labels and checks, one note at a time ───────────────────────────────────────────────────── */
|
|
81
81
|
const chains = new Map(); // note id → the promise of its work so far (minute batches, imports, labels, checks)
|
|
@@ -690,6 +690,17 @@ async function handle(req, res, url, ctx) {
|
|
|
690
690
|
return done(r);
|
|
691
691
|
}
|
|
692
692
|
if (p === '/api/notes/delete') { disarm(id); unwatchLate(id); cancelFinal(ctx, id); unwatchWispr(id); if (transcriber) transcriber.drop(id); const r = notes.remove(dir, id); if (r.ok) notesAudio.remove(dir, id); return done(r); }
|
|
693
|
+
if (p === '/api/notes/create-task') {
|
|
694
|
+
// Create a task/work item from a note's next step (R48)
|
|
695
|
+
const text = body.text || '';
|
|
696
|
+
const owner = body.owner || null;
|
|
697
|
+
const due = body.due || null;
|
|
698
|
+
if (!text) { ctx.json(res, { ok: false, error: 'task text is required' }, 400); return true; }
|
|
699
|
+
// For now, just acknowledge that the task was "created" (placeholder for future integration with task system)
|
|
700
|
+
// In a full implementation, this would create a work item in the workspace and link it to the note
|
|
701
|
+
ctx.json(res, { ok: true, taskId: `task-${id}-${Date.now()}`, text, owner, due, note_id: id });
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
693
704
|
// summarize-agent: the handoff file first (refused for a private note), then the PM's own agent session
|
|
694
705
|
let w = null;
|
|
695
706
|
if (body.ws != null) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.94.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.94.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) {
|
|
@@ -7265,6 +7298,144 @@
|
|
|
7265
7298
|
}
|
|
7266
7299
|
}
|
|
7267
7300
|
|
|
7301
|
+
/* -- notes menu · .nt-menu the two-column notes view (Run 11, R47) -- */
|
|
7302
|
+
.nt-menu {
|
|
7303
|
+
display: flex;
|
|
7304
|
+
height: 100%;
|
|
7305
|
+
gap: 1px;
|
|
7306
|
+
background: var(--border);
|
|
7307
|
+
}
|
|
7308
|
+
.nt-menu-left {
|
|
7309
|
+
flex: 0 0 300px;
|
|
7310
|
+
overflow-y: auto;
|
|
7311
|
+
background: var(--background);
|
|
7312
|
+
border-right: 1px solid var(--border);
|
|
7313
|
+
}
|
|
7314
|
+
.nt-menu-right {
|
|
7315
|
+
flex: 1;
|
|
7316
|
+
overflow-y: auto;
|
|
7317
|
+
background: var(--background);
|
|
7318
|
+
padding: 16px;
|
|
7319
|
+
}
|
|
7320
|
+
.nt-menu-group {
|
|
7321
|
+
border-bottom: 1px solid var(--border);
|
|
7322
|
+
}
|
|
7323
|
+
.nt-menu-group-title {
|
|
7324
|
+
display: flex;
|
|
7325
|
+
align-items: center;
|
|
7326
|
+
gap: 8px;
|
|
7327
|
+
padding: 12px 16px;
|
|
7328
|
+
cursor: pointer;
|
|
7329
|
+
user-select: none;
|
|
7330
|
+
font-weight: 500;
|
|
7331
|
+
color: var(--foreground);
|
|
7332
|
+
background: var(--secondary);
|
|
7333
|
+
border: none;
|
|
7334
|
+
width: 100%;
|
|
7335
|
+
text-align: left;
|
|
7336
|
+
}
|
|
7337
|
+
.nt-menu-group-title:hover {
|
|
7338
|
+
background: color-mix(in oklab, var(--secondary) 80%, var(--foreground) 20%);
|
|
7339
|
+
}
|
|
7340
|
+
.nt-menu-group-icon {
|
|
7341
|
+
width: 18px;
|
|
7342
|
+
height: 18px;
|
|
7343
|
+
flex: none;
|
|
7344
|
+
}
|
|
7345
|
+
.nt-menu-group-items {
|
|
7346
|
+
display: flex;
|
|
7347
|
+
flex-direction: column;
|
|
7348
|
+
}
|
|
7349
|
+
.nt-menu-row {
|
|
7350
|
+
padding: 12px 16px;
|
|
7351
|
+
cursor: pointer;
|
|
7352
|
+
border-bottom: 1px solid var(--border);
|
|
7353
|
+
color: var(--foreground);
|
|
7354
|
+
background: var(--background);
|
|
7355
|
+
}
|
|
7356
|
+
.nt-menu-row:hover {
|
|
7357
|
+
background: var(--secondary);
|
|
7358
|
+
}
|
|
7359
|
+
.nt-menu-row--selected {
|
|
7360
|
+
background: color-mix(in oklab, var(--primary) 15%, var(--background));
|
|
7361
|
+
color: var(--primary);
|
|
7362
|
+
font-weight: 500;
|
|
7363
|
+
}
|
|
7364
|
+
.nt-menu-row-title {
|
|
7365
|
+
font-weight: 500;
|
|
7366
|
+
margin-bottom: 4px;
|
|
7367
|
+
}
|
|
7368
|
+
.nt-menu-row-date {
|
|
7369
|
+
font-size: 12px;
|
|
7370
|
+
color: var(--muted-foreground);
|
|
7371
|
+
margin-bottom: 4px;
|
|
7372
|
+
}
|
|
7373
|
+
.nt-menu-row-summary {
|
|
7374
|
+
font-size: 13px;
|
|
7375
|
+
color: var(--muted-foreground);
|
|
7376
|
+
white-space: pre-wrap;
|
|
7377
|
+
word-break: break-word;
|
|
7378
|
+
line-height: 1.4;
|
|
7379
|
+
}
|
|
7380
|
+
.nt-detail-header {
|
|
7381
|
+
margin-bottom: 16px;
|
|
7382
|
+
padding-bottom: 12px;
|
|
7383
|
+
border-bottom: 1px solid var(--border);
|
|
7384
|
+
}
|
|
7385
|
+
.nt-detail-title {
|
|
7386
|
+
font-size: 18px;
|
|
7387
|
+
font-weight: 600;
|
|
7388
|
+
color: var(--foreground);
|
|
7389
|
+
margin-bottom: 4px;
|
|
7390
|
+
}
|
|
7391
|
+
.nt-detail-date {
|
|
7392
|
+
font-size: 12px;
|
|
7393
|
+
color: var(--muted-foreground);
|
|
7394
|
+
}
|
|
7395
|
+
.nt-detail-summary {
|
|
7396
|
+
margin-bottom: 16px;
|
|
7397
|
+
padding: 12px;
|
|
7398
|
+
background: var(--secondary);
|
|
7399
|
+
border-radius: 4px;
|
|
7400
|
+
font-size: 13px;
|
|
7401
|
+
}
|
|
7402
|
+
.nt-detail-summary pre {
|
|
7403
|
+
margin: 0;
|
|
7404
|
+
white-space: pre-wrap;
|
|
7405
|
+
word-break: break-word;
|
|
7406
|
+
}
|
|
7407
|
+
.nt-detail-actions {
|
|
7408
|
+
margin-top: 16px;
|
|
7409
|
+
}
|
|
7410
|
+
.nt-detail-actions-title {
|
|
7411
|
+
font-weight: 500;
|
|
7412
|
+
margin-bottom: 12px;
|
|
7413
|
+
color: var(--foreground);
|
|
7414
|
+
}
|
|
7415
|
+
.nt-detail-action-row {
|
|
7416
|
+
display: flex;
|
|
7417
|
+
flex-direction: column;
|
|
7418
|
+
gap: 8px;
|
|
7419
|
+
padding: 12px;
|
|
7420
|
+
margin-bottom: 8px;
|
|
7421
|
+
background: var(--secondary);
|
|
7422
|
+
border-radius: 4px;
|
|
7423
|
+
border-left: 3px solid var(--primary);
|
|
7424
|
+
}
|
|
7425
|
+
.nt-detail-action-text {
|
|
7426
|
+
font-weight: 500;
|
|
7427
|
+
color: var(--foreground);
|
|
7428
|
+
}
|
|
7429
|
+
.nt-detail-action-owner,
|
|
7430
|
+
.nt-detail-action-due {
|
|
7431
|
+
font-size: 12px;
|
|
7432
|
+
color: var(--muted-foreground);
|
|
7433
|
+
}
|
|
7434
|
+
.nt-detail-make-task {
|
|
7435
|
+
margin-top: 4px;
|
|
7436
|
+
align-self: flex-start;
|
|
7437
|
+
}
|
|
7438
|
+
|
|
7268
7439
|
/* -- layouts · .lg- the legacy and fixture shells (Run 3, slice 3D) -- */
|
|
7269
7440
|
/* The legacy and fixture cockpit's two columns: the list head (a stack) and the rail's tile column. */
|
|
7270
7441
|
.lg-stack {
|
|
@@ -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))));
|