create-pmos 0.515.0 → 0.516.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 +22 -48
- package/payload/KIT-VERSION +2 -2
- 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/payload/state/log.md +1 -1
- package/workspace/briefing-premium-calendar-test.js +6 -3
- package/workspace/briefing-premium-insights-test.js +2 -2
- package/workspace/changelog.json +21 -0
- package/workspace/fixtures/premium-viewer-fixtures.js +4 -2
- package/workspace/guide/agent-capabilities.md +73 -0
- package/workspace/guide/index.md +2 -0
- package/workspace/guide/reports-needs-you.md +34 -0
- package/workspace/lib/report-acts.js +189 -0
- package/workspace/lib/routes-insights-premium.js +17 -3
- package/workspace/lib/routes-ready-reports.js +8 -1
- package/workspace/lib/routes-report-setups.js +21 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-vw-meet-test.js +9 -5
- package/workspace/public/pm/cozy-insights.css +34 -0
- package/workspace/public/pm/home-app.js +3 -1
- package/workspace/public/pm/premium-insights.js +112 -14
- package/workspace/public/premium-intelligence.html +1 -1
- package/workspace/test-lists/reports-needs-you.json +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.516.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": {
|
|
@@ -32,6 +32,27 @@
|
|
|
32
32
|
"audit"
|
|
33
33
|
],
|
|
34
34
|
"pmosChanges": [
|
|
35
|
+
{
|
|
36
|
+
"v": "0.516.0",
|
|
37
|
+
"at": "2026-11-04T00:00:07.601Z",
|
|
38
|
+
"f": 4,
|
|
39
|
+
"prs": [
|
|
40
|
+
{
|
|
41
|
+
"n": 1209,
|
|
42
|
+
"t": "Runbook of what Claude does behind the scenes, in the guide",
|
|
43
|
+
"s": "Runbook of what Claude does behind the scenes, in the guide",
|
|
44
|
+
"k": "x",
|
|
45
|
+
"h": "See what Claude does behind the scenes",
|
|
46
|
+
"d": "Claude's background jobs were explained one page at a time, and nothing listed them all together. One guide page lists every job Claude runs by itself: when it runs, what it reads, where you see the result, how to stop it, and what Claude fixes without asking."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"n": 1208,
|
|
50
|
+
"t": "Reports that need you come first, one ✓ each (redesign R)",
|
|
51
|
+
"s": "Reports that need you come first, one ✓ each (redesign R)",
|
|
52
|
+
"k": "new"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
},
|
|
35
56
|
{
|
|
36
57
|
"v": "0.515.0",
|
|
37
58
|
"at": "2026-11-03T23:11:51.264Z",
|
|
@@ -415,53 +436,6 @@
|
|
|
415
436
|
"d": "After a chat changed something, the Viewer still showed the old version until you refreshed. Now it updates on its own."
|
|
416
437
|
}
|
|
417
438
|
]
|
|
418
|
-
},
|
|
419
|
-
{
|
|
420
|
-
"v": "0.496.0",
|
|
421
|
-
"at": "2026-11-03T00:29:32.573Z",
|
|
422
|
-
"f": 4,
|
|
423
|
-
"prs": [
|
|
424
|
-
{
|
|
425
|
-
"n": 1172,
|
|
426
|
-
"t": "Initiative chats find the initiative's workflow through pmos, never a lane",
|
|
427
|
-
"s": "Initiative chats find the initiative's workflow through pmos, never a lane",
|
|
428
|
-
"k": "fix",
|
|
429
|
-
"h": "Initiative chats find the right workflow",
|
|
430
|
-
"d": "An initiative's chat could miss the workflow it should follow. Now it always finds it, including the ones in your own library."
|
|
431
|
-
},
|
|
432
|
-
{
|
|
433
|
-
"n": 1170,
|
|
434
|
-
"t": "Keep a rejected ✦ suggestion rejected while its facts hold",
|
|
435
|
-
"s": "Keep a rejected ✦ suggestion rejected while its facts hold",
|
|
436
|
-
"k": "fix",
|
|
437
|
-
"h": "A no stays a no",
|
|
438
|
-
"d": "A ✦ suggestion you turned down could come back the next day. Now it stays rejected while nothing about it changes."
|
|
439
|
-
},
|
|
440
|
-
{
|
|
441
|
-
"n": 1171,
|
|
442
|
-
"t": "Tasks keep their mapped stage after a workflow switch on Home and Board",
|
|
443
|
-
"s": "Tasks keep their mapped stage after a workflow switch on Home and Board",
|
|
444
|
-
"k": "fix",
|
|
445
|
-
"h": "Tasks keep their stage",
|
|
446
|
-
"d": "Switching an initiative's workflow could move its tasks to the wrong stage. Now they keep the stage they were mapped to."
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
"n": 1173,
|
|
450
|
-
"t": "Calmer Board top bar",
|
|
451
|
-
"s": "Calmer Board top bar",
|
|
452
|
-
"k": "new",
|
|
453
|
-
"h": "A calmer Board top bar",
|
|
454
|
-
"d": "The Board's top bar was crowded. Now it is lighter, with fewer controls in your way."
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
"n": 1165,
|
|
458
|
-
"t": "Pages show something at once while they read",
|
|
459
|
-
"s": "Pages show something at once while they read",
|
|
460
|
-
"k": "new",
|
|
461
|
-
"h": "Pages show something at once",
|
|
462
|
-
"d": "A page stayed blank while it read its data. Now it shows its shape right away and fills in as data arrives."
|
|
463
|
-
}
|
|
464
|
-
]
|
|
465
439
|
}
|
|
466
440
|
]
|
|
467
441
|
}
|
package/payload/KIT-VERSION
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
kernel-source: pmos@
|
|
2
|
-
assembled: 2026-11-
|
|
1
|
+
kernel-source: pmos@454e5b13
|
|
2
|
+
assembled: 2026-11-04
|
|
3
3
|
mode: vendor
|
|
@@ -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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T00:00:04Z"}
|
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T00:00:04Z"}
|
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T00:00:04Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/payload/state/log.md
CHANGED
|
@@ -36,19 +36,22 @@ const json = (r, b, status = 200) => r.fulfill({ status, contentType: 'applicati
|
|
|
36
36
|
srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
|
|
37
37
|
await T.waitForServer(BASE);
|
|
38
38
|
browser = await chromium.launch({ executablePath: exe });
|
|
39
|
-
|
|
39
|
+
/* One clock for the page and the stand-ins: today at 11:00, so "Rail review" (30 minutes on) and the meeting 40 minutes back are today
|
|
40
|
+
* at any hour the suite runs. Unpinned, after 23:30 the review fell on tomorrow and C2 failed. */
|
|
41
|
+
const NOW = new Date(); NOW.setHours(11, 0, 0, 0);
|
|
42
|
+
const W = F.world(NOW);
|
|
40
43
|
const tap = (p, sel) => p.$eval('#czViewer ' + sel, (b) => b.click());
|
|
41
44
|
const text = (p) => p.evaluate(() => (document.querySelector('#czViewer') || {}).innerText || '');
|
|
42
45
|
const weekReads = (S) => S.gets.filter((g) => g.startsWith('/api/calendar-views')).length;
|
|
43
46
|
const ALL = [];
|
|
44
|
-
const open = async (url, o) => { const X = await F.open(browser, BASE, url, { noLive: true, ...o }); ALL.push(X); return X; };
|
|
47
|
+
const open = async (url, o) => { const X = await F.open(browser, BASE, url, { noLive: true, now: NOW, ...o }); ALL.push(X); return X; };
|
|
45
48
|
|
|
46
49
|
/* C1 the strip (on a Saturday or Sunday no day of the Mon–Fri strip is today, so none is pressed) */
|
|
47
50
|
const A = await open('/premium-viewer.html?open=calendar');
|
|
48
51
|
const p = A.page;
|
|
49
52
|
// Viewer kit (XvACal): the calendar opens on the day, with today's summary first; the week and month are in ⋯
|
|
50
53
|
ok('C1 the calendar opens in the Viewer card on the day', await until(() => p.evaluate(() => document.querySelectorAll('#czViewer .vzc-dy').length === 5 && document.querySelectorAll('#czViewer .vzc-de').length > 0), 15000));
|
|
51
|
-
const weekday = ![0, 6].includes(
|
|
54
|
+
const weekday = ![0, 6].includes(NOW.getDay());
|
|
52
55
|
await until(() => p.evaluate(() => [...document.querySelectorAll('#czViewer .vzc-dy')].every((d) => d.getBoundingClientRect().height >= 44)), 8000); // the Meetings stylesheet loads after the day is drawn
|
|
53
56
|
const strip = await p.evaluate(() => [...document.querySelectorAll('#czViewer .vzc-dy')].map((d) => ({ dow: d.querySelector('span').textContent, on: d.getAttribute('aria-pressed'), h: Math.round(d.getBoundingClientRect().height) })));
|
|
54
57
|
ok('C1 the strip has Monday to Friday and today is pressed on a weekday', strip.map((d) => d.dow).join() === 'MON,TUE,WED,THU,FRI' && strip.filter((d) => d.on === 'true').length === (weekday ? 1 : 0), JSON.stringify(strip));
|
|
@@ -390,9 +390,9 @@ const BMR = { ok: true, state: 'ready', model: { tiles: [
|
|
|
390
390
|
const R = await open(), q = R.page;
|
|
391
391
|
await q.click('#moreB'); await q.click('#moreMenu [data-more=\"library\"]');
|
|
392
392
|
/* re-pointed (premium-uncovered-features, IsLibrary): the bar hands New report to Home, Found in your work is cards, the reports are Report · Next · Status */
|
|
393
|
-
const lb = await q.evaluate(() => ({ side: !document.querySelector('#libSide').hidden, nr: document.querySelector('#newRep').getAttribute('href'), found: document.querySelector('#listRows .ins-fdh b').textContent, fcards: [...document.querySelectorAll('#listRows .ins-fcard')].map((c) => c.dataset.f + ':' + [...c.querySelectorAll('.ins-btn[data-fx]')].map((b) => b.textContent).join('/')).join(), heads: [...document.querySelectorAll('#repAsks .ins-rlh span')].map((s) => s.textContent).join('|'), rows: [...document.querySelectorAll('#repNeeds .ins-rl:not(.ins-rlh), #listRows .ins-rl:not(.ins-rlh)')].map((r) => r.querySelector('.nm b').textContent + '|' + r.querySelector('.nx').textContent + '|' + r.querySelector('.st').textContent).join(' / '), setups: document.querySelectorAll('#repNeeds [data-setup], #listRows [data-setup]').length, asks: document.querySelectorAll('#repNeeds [data-ask], #listRows [data-ask]').
|
|
393
|
+
const lb = await q.evaluate(() => ({ side: !document.querySelector('#libSide').hidden, nr: document.querySelector('#newRep').getAttribute('href'), found: document.querySelector('#listRows .ins-fdh b').textContent, fcards: [...document.querySelectorAll('#listRows .ins-fcard')].map((c) => c.dataset.f + ':' + [...c.querySelectorAll('.ins-btn[data-fx]')].map((b) => b.textContent).join('/')).join(), heads: [...document.querySelectorAll('#repAsks .ins-rlh span')].map((s) => s.textContent).join('|'), rows: [...document.querySelectorAll('#repNeeds .ins-rl:not(.ins-rlh), #listRows .ins-rl:not(.ins-rlh)')].map((r) => r.querySelector('.nm b').textContent + '|' + r.querySelector('.nx').textContent + '|' + r.querySelector('.st').textContent).join(' / '), needs: [...document.querySelectorAll('#repNeeds .ins-nrow .nm b')].map((b) => b.textContent).join('|'), setups: document.querySelectorAll('#repNeeds [data-setup], #listRows [data-setup]').length, asks: new Set([...document.querySelectorAll('#repNeeds [data-ask], #listRows [data-ask]')].map((e) => e.dataset.ask)).size, cards: [...document.querySelectorAll('#listRows .ins-dcard')].map((c) => c.dataset.d + (c.querySelector('.ins-pinned') ? '*' : '')).join(), cap: document.querySelector('#repCap').textContent, old: !!document.querySelector('#listSeg, #sugB') }));
|
|
394
394
|
ok('L1 Reports and dashboards opens as a side panel from ⋯; New report in Home hands off there, and Found in your work holds the finding as a card', lb.side && /^\/premium\.html\?ws=w1&ask=insights&q=Make%20a%20new%20report/.test(lb.nr) && lb.found === 'Found in your work · 1' && lb.fcards === 'f-1:Set it up/Not a report' && lb.cap === '9' && !lb.old, JSON.stringify(lb));
|
|
395
|
-
ok('L1 the reports table is Report, Next and Status (what needs you first, redesign A); a draft says "Draft ready for" its date; a request says
|
|
395
|
+
ok('L1 the reports table is Report, Next and Status (what needs you first, redesign A); a draft says "Draft ready for" its date; a request says Working; a ready proposal waits in Needs you (redesign R)', lb.heads === 'Report|Next|Status' && /Draft ready for Oct 14/.test(lb.rows) && /Partner update\|First draft [^|]+\|Runs every other monday/i.test(lb.rows) && /(^|\|)Churn for finance(\||$)/.test(lb.needs) && lb.setups === 1 && lb.asks === 2, JSON.stringify([lb.rows, lb.needs]));
|
|
396
396
|
ok('L1 dashboards are cards; only the pinned one carries the pin', lb.cards === 'all*,pd-1,p:Growth,p:Reliability', lb.cards);
|
|
397
397
|
const rs = []; await q.route('**/api/report-setups/*', (r) => { rs.push([new URL(r.request().url()).pathname.replace('/api/report-setups/', ''), JSON.parse(r.request().postData() || '{}')]); r.fulfill({ contentType: 'application/json', body: '{"ok":true,"drafted":{"slot":"2026-10-23","path":"planning/reports/x/2026-10-23.md"}}' }); });
|
|
398
398
|
const lastRs = () => rs[rs.length - 1] || [null, {}];
|
package/workspace/changelog.json
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "0.516.0",
|
|
4
|
+
"at": "2026-11-04T00:00:07.601Z",
|
|
5
|
+
"f": 4,
|
|
6
|
+
"prs": [
|
|
7
|
+
{
|
|
8
|
+
"n": 1209,
|
|
9
|
+
"t": "Runbook of what Claude does behind the scenes, in the guide",
|
|
10
|
+
"s": "Runbook of what Claude does behind the scenes, in the guide",
|
|
11
|
+
"k": "x",
|
|
12
|
+
"h": "See what Claude does behind the scenes",
|
|
13
|
+
"d": "Claude's background jobs were explained one page at a time, and nothing listed them all together. One guide page lists every job Claude runs by itself: when it runs, what it reads, where you see the result, how to stop it, and what Claude fixes without asking."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"n": 1208,
|
|
17
|
+
"t": "Reports that need you come first, one ✓ each (redesign R)",
|
|
18
|
+
"s": "Reports that need you come first, one ✓ each (redesign R)",
|
|
19
|
+
"k": "new"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
},
|
|
2
23
|
{
|
|
3
24
|
"v": "0.515.0",
|
|
4
25
|
"at": "2026-11-03T23:11:51.264Z",
|
|
@@ -62,6 +62,7 @@ const REPORT = '---\ntype: Report\ntitle: "Password login and sign-up"\n---\n\n#
|
|
|
62
62
|
|
|
63
63
|
function install(page, o = {}) {
|
|
64
64
|
const W = world(o.now || new Date());
|
|
65
|
+
const shift = o.now ? o.now.getTime() - Date.now() : 0; // a pinned clock (o.now): the stand-ins answer in the page's time
|
|
65
66
|
const S = { posts: [], gets: [], scene: o.scene || 'notes', W };
|
|
66
67
|
const json = (r, b, status = 200) => r.fulfill({ status, contentType: 'application/json', body: JSON.stringify(b) });
|
|
67
68
|
const body = (r) => { try { return JSON.parse(r.request().postData() || '{}'); } catch { return {}; } };
|
|
@@ -118,8 +119,8 @@ function install(page, o = {}) {
|
|
|
118
119
|
const u = new URL(r.request().url()), ev = u.searchParams.get('event'), note = u.searchParams.get('note');
|
|
119
120
|
const live = ev === 'ev-live' || note === 'n2';
|
|
120
121
|
/* a note with no calendar event has no event, brief or topics */
|
|
121
|
-
if (!ev && note && note !== 'n1' && note !== 'n2') return json(r, { ok: true, now_ms: Date.now(), event: null, note: { id: note, state: (S.store[note] || {}).state || 'ended' }, brief: null, material: [], topics: [], runs: [], captured: {}, tail: [] });
|
|
122
|
-
const base = { ok: true, now_ms: Date.now(), event: live ? { id: 'ev-live', title: 'Design crit', at: W.LIVE_NOTE.meeting.start, end: W.LIVE_NOTE.meeting.end, attendees: ['Ana', 'Wawan'] } : ev === 'ev-rail' ? { id: 'ev-rail', title: 'Rail review', at: iso(W.RAIL), attendees: ['Sam', 'Wawan'] } : { id: 'ev-sync', title: 'Viewer sync', at: W.NOTE.meeting.start, end: W.NOTE.meeting.end, attendees: ['Ana', 'Dita', 'Wawan'] },
|
|
122
|
+
if (!ev && note && note !== 'n1' && note !== 'n2') return json(r, { ok: true, now_ms: Date.now() + shift, event: null, note: { id: note, state: (S.store[note] || {}).state || 'ended' }, brief: null, material: [], topics: [], runs: [], captured: {}, tail: [] });
|
|
123
|
+
const base = { ok: true, now_ms: Date.now() + shift, event: live ? { id: 'ev-live', title: 'Design crit', at: W.LIVE_NOTE.meeting.start, end: W.LIVE_NOTE.meeting.end, attendees: ['Ana', 'Wawan'] } : ev === 'ev-rail' ? { id: 'ev-rail', title: 'Rail review', at: iso(W.RAIL), attendees: ['Sam', 'Wawan'] } : { id: 'ev-sync', title: 'Viewer sync', at: W.NOTE.meeting.start, end: W.NOTE.meeting.end, attendees: ['Ana', 'Dita', 'Wawan'] },
|
|
123
124
|
note: live ? { id: 'n2', state: 'recording' } : ev === 'ev-rail' ? null : { id: 'n1', state: 'ended' },
|
|
124
125
|
brief: { gist: ['Agree the rail before the Commit.'], purpose: 'Agree the rail before the Commit.', matters: [{ tag: 'Late', tone: 'bad', text: 'The SSO spike', meta: 'due 14 Oct' }, { tag: 'Changed', tone: '', text: 'The report panel design', meta: 'Oct 21' }], last: { id: 'n0', at: '2026-10-15T10:00:00Z', decisions: [{ text: 'a' }], actions: [{ text: 'b' }, { text: 'c' }] }, prep: 'Read the rail PRD.', initiative: { id: 'viewer', title: 'Viewer', ws: 'w1' } },
|
|
125
126
|
material: [{ kind: 'pmos-deck', title: 'Release deck', ref: 'decks/release.md', from: 'you' }, { kind: 'pmos-doc', title: 'Rail PRD', ref: 'planning/prd/rail.md', from: 'you' }, { kind: 'link', title: 'Figma: report panel', url: 'https://x.example/f', topic: 'The report panel' }],
|
|
@@ -189,6 +190,7 @@ async function open(browser, base, url, o = {}) {
|
|
|
189
190
|
const ctx = await browser.newContext({ viewport: { width: o.w || 1280, height: o.h || 900 }, acceptDownloads: true });
|
|
190
191
|
const page = await ctx.newPage(), errors = [];
|
|
191
192
|
page.on('pageerror', (e) => errors.push(String((e && e.message) || e)));
|
|
193
|
+
if (o.now) await page.clock.setSystemTime(o.now); // the page's Date starts at o.now and runs on; timers are real
|
|
192
194
|
const S = await install(page, o);
|
|
193
195
|
await page.addInitScript(RECORDER);
|
|
194
196
|
if (o.init) await page.addInitScript(o.init);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Guide
|
|
3
|
+
title: "What Claude does behind the scenes"
|
|
4
|
+
description: "The runbook of the workspace agent: each job Claude runs by itself, when it runs, what it reads, where its result shows, how to stop it, what it fixes by itself, the rules every job follows, and how to see what ran."
|
|
5
|
+
timestamp: 2026-11-03
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# What Claude does behind the scenes
|
|
9
|
+
|
|
10
|
+
Claude works in the background while the workspace runs. It reads your records and the tools you connected, it drafts, and it checks. It does not decide for you. A result waits for your ✓, most often under the blue ✦ of an initiative or as one ✦ in Home's chat. A background job never sends, posts, pushes or merges anything.
|
|
11
|
+
|
|
12
|
+
This page lists every job, so you know what runs and why. Each row links to the page with the full detail.
|
|
13
|
+
|
|
14
|
+
## The rules every job follows
|
|
15
|
+
|
|
16
|
+
- **Propose-only.** A background job files proposals. Nothing lands on an initiative, a task or a metric until you press ✓. ✕ drops a proposal, and Undo brings back what was there.
|
|
17
|
+
- **Every claim has a source.** An AI job files through one shared guard. The guard checks that each quote is really at the file and line it cites. A claim with no source that checks out is dropped. A proposal with a key that would write, apply, send or run a command is dropped.
|
|
18
|
+
- **Few at a time.** A run shows at most 3 proposals. In a quiet week it shows nothing.
|
|
19
|
+
- **Tested first.** A new version of a job shows nothing until its replay passes: its own known cases, and the guard's own cases (a made-up quote, a missing source, a path outside the repository, a write key). Each of those must be dropped.
|
|
20
|
+
- **Your login, your connections.** A job runs as a background Claude session with your own login, model, profile and connectors. The workspace holds no password or token. `PMOS_HEADLESS=off` sends every job back to a terminal pane.
|
|
21
|
+
- **Your hours.** Scheduled jobs run inside your working hours and never inside your quiet hours.
|
|
22
|
+
- **A spending limit.** Claude's checks spend at most the monthly credit you set in Settings › Claude's checks ($5, $10, $20 or $50). At the limit Claude pauses. It never spends beyond the limit.
|
|
23
|
+
- **Every run is logged.** Each job writes its own ledger in the runtime folder on this computer. Only you see it, and it never goes into a repository.
|
|
24
|
+
|
|
25
|
+
## The jobs
|
|
26
|
+
|
|
27
|
+
| Job | When it runs | What it reads | Where you see it | How to stop it |
|
|
28
|
+
|---|---|---|---|---|
|
|
29
|
+
| [Initiative Driver](initiative-driver.md) | An initiative is created, moved or sent back. The unblock sweep runs every 30 minutes in working hours. At most 3 runs at once. | The initiative record, its tasks and its Jira tickets | The Agent strip of the initiative, and Needs you | Board settings › Agent: Off. `PMOS_DRIVER=off` |
|
|
30
|
+
| [Found elsewhere](claude-proposes.md) | You add an initiative | Drive, Docs, Sheets, Slides, Jira, the calendar, meeting notes, Gmail, Slack and Confluence | The initiative's ✦ | `PMOS_PROPOSE=off` |
|
|
31
|
+
| [New dates, done and tidy names](claude-proposes.md) | Each morning, and when a date passes | The tasks of the initiative, and the evidence that work happened | The initiative's ✦ | `PMOS_PROPOSE=off` |
|
|
32
|
+
| [Grill](claude-proposes.md) | Before an initiative leaves a stage, when a forecast turns at risk, when scope or dates move a lot, and once a month on running work with late work. Also when you write "grill this" in the initiative's chat. | The initiative, your stored research, your meeting notes and the decisions that stand | "Claude grilled it" under the initiative's ✦ | Settings › Claude's checks: Off. `PMOS_GRILL=off` |
|
|
33
|
+
| [You decided this before](ask-the-past.md) | You add an initiative that repeats an earlier decision | `DECISIONS.md`, the decision records, the PM journal, `planning/out-of-scope/` and the initiative records | The initiative's ✦ | `pmos ai off past-repeat` |
|
|
34
|
+
| [Expected outcomes](outcomes.md) | Once a day. At most 3 drafts a day for each product. | The PRD's success criteria, or the anchored key result | "Expected outcome" under the initiative's ✦. Today when a check is due. | `pmos ai off outcomes`. `PMOS_OUTCOMES=off` |
|
|
35
|
+
| [Metric drafts](metric-drafts.md) | A product with no North Star, the first 14 days of a quarter, a program with no goal, and an initiative that gets a PRD | The last 8 weeks of real readings | The page of that product, program or initiative | `PMOS_METRIC_DRAFT=off` |
|
|
36
|
+
| [Horizon scan](horizon-scan.md) | Daily by default, at most once a day. Scan now: at most 3 a day. | The public web for your kept watch list, and your repository | Report documents and proposals you approve | Cadence: Off. `PMOS_SCAN=off` |
|
|
37
|
+
| [What you might have missed](what-you-missed.md) | Each Monday, for the week before. It uses no model. | Records only: tasks, dates, initiatives and Jira status | One ✦ in Home's chat, at most 3 items | `pmos ai off drift`. `PMOS_DRIFT=off` |
|
|
38
|
+
| [Your impact](your-impact.md) | Claude reads once a day. Home shows the week after it ends. | Slack, mail, Jira, your meeting notes, your documents and your key results | Adler's sprout on Home, and your impact log | Settings › General › Your impact. `PMOS_IMPACT=off` |
|
|
39
|
+
| [Morning brief and day check](proactive-engine.md) | The morning brief at 06:45, once you schedule it. The day check every 60 minutes by default (30, 60 or off). | Calendar, Wispr Flow, Slack, Jira and Confluence are on by default. Mail, GitHub, Drive, Figma and websites are off by default. Mail: subjects and senders only. | Today | Each source is a setting. `pmos proactive settings` |
|
|
40
|
+
| [Day tasks](proactive-engine.md) | You press Prepare my day. At most 5 sessions at once. | The sources of the brief, read-only | Today: a draft waits for Approve or Revise | Do not press Prepare my day |
|
|
41
|
+
| [Next prompt and Sharper](next-prompt.md) | After Claude replies, and when you stop typing a rough prompt | The reply and your message | Cards above the chat box. Nothing is sent. | Settings › Claude. `pmos ai off next-prompt`, `pmos ai off sharpen-prompt` |
|
|
42
|
+
| [Rehearse a plan](rehearse.md) | You ask Claude to move a date or cut a task or an initiative | Task links, initiative records, goals and program charters | Your session, before Claude makes the change | `pmos ai off rehearse` |
|
|
43
|
+
| [Data refresh](data-freshness.md) | In working hours: the calendar every 30 minutes, and Jira, My work and your tools every hour. Tracker items on Friday 15:30. | Each tool with your own connection, read-only | Each page shows how old its data is | Settings › Data refresh |
|
|
44
|
+
|
|
45
|
+
Ask about the past runs only when you ask a question in an initiative's chat. It answers from the records, with the file and line of each claim, and says **No record found** when no record answers.
|
|
46
|
+
|
|
47
|
+
## What Claude fixes by itself
|
|
48
|
+
|
|
49
|
+
Some problems are safe to fix with no question. Claude fixes these by itself and tells you only when it cannot.
|
|
50
|
+
|
|
51
|
+
- **A product folder on the wrong branch.** A document repository whose folder is not on its default branch looks empty. The workspace puts it back: it saves the records, checks that the merge has no conflict, merges, and pushes with no force. It never deletes or rewrites a branch. It never touches a code repository. When it cannot do this safely, Today shows one "Needs repair" card in plain words, with **Ask Claude to sort it out**. It tries again after 30 minutes, or after 10 minutes when it only waited for the network. `PMOS_AUTO_REPAIR=0` stops it.
|
|
52
|
+
- **Folders of finished work.** About every 6 hours the workspace removes a linked worktree only when its branch is merged, it holds no unsaved file, nothing used it for 14 days, and no session uses it. The branch is kept. `PMOS_AUTO_CLEAN=0` stops it.
|
|
53
|
+
- **A report source that does not answer.** A scheduled report waits up to 4 hours for a source that failed its check. Today shows "Check <source> for <report>" with **Check again**. Then the report is drafted without that source, and it says so. It never guesses a number.
|
|
54
|
+
- **A failed run.** A metric draft that failed tries again the next day. A Driver run that failed or timed out writes its reason in the ledger.
|
|
55
|
+
- **A source that was not reached.** It is shown as not reached, never as read. Today says which sources were not read.
|
|
56
|
+
|
|
57
|
+
Sync by itself is on by default. `PMOS_AUTO_SYNC=0` stops the sync, the repair and the clean-up.
|
|
58
|
+
|
|
59
|
+
## What a background job never does
|
|
60
|
+
|
|
61
|
+
- It sends, posts, replies, pushes and merges nothing. A message to a person is a draft that you send. A Jira change is a drafted comment in your outbox.
|
|
62
|
+
- It does not create an initiative. The Horizon scan proposes one, and you approve it.
|
|
63
|
+
- It does not record a verdict on an outcome. The verdict is always yours.
|
|
64
|
+
|
|
65
|
+
When you ask an agent session to do a piece of work, it can finish it, including acts that were yours alone. Each line it writes names the agent, so you see who did it. See [PM-only acts](pm-only-acts.md).
|
|
66
|
+
|
|
67
|
+
## See what ran, and turn it off
|
|
68
|
+
|
|
69
|
+
- `pmos ai status`: whether each AI job is on, whether its replay passed, and what its last run filed.
|
|
70
|
+
- `pmos ai log [<job>]`: the last audit lines. Each dropped proposal shows why.
|
|
71
|
+
- `pmos ai off [<job>]` and `pmos ai on [<job>]`: turn every AI job, or one job, off or on. `PMOS_AI=off` in the environment wins over the setting.
|
|
72
|
+
|
|
73
|
+
The AI jobs are `past-repeat`, `decision-memory`, `outcomes`, `drift`, `rehearse`, `next-prompt` and `sharpen-prompt`. A background run cannot change the switch.
|
package/workspace/guide/index.md
CHANGED
|
@@ -41,6 +41,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
41
41
|
|
|
42
42
|
## Where to read next
|
|
43
43
|
|
|
44
|
+
- [What Claude does behind the scenes](agent-capabilities.md): every job Claude runs by itself, when it runs, what it reads, where you see it, how to stop it, and what it fixes by itself.
|
|
44
45
|
- [First run](welcome.md): test your Claude login, describe the project, choose its folder, search your tools read-only, and set up PMOS from Home.
|
|
45
46
|
- [The Briefing](briefing.md): the console's first page: Today's meetings, Needs you (Done, Not yet, Drop) and the Portfolio pulse.
|
|
46
47
|
- [Start the workspace without a terminal](launcher.md): the PMOS app, the login item and `pmos launcher`.
|
|
@@ -81,6 +82,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
81
82
|
- [Initiatives › Timeline](initiatives-timeline.md): when each initiative runs, one row per initiative, and how to reorder rows by their grip.
|
|
82
83
|
- [Workflows](workflows.md): Settings › Workflows, the stages each kind of initiative goes through, New workflow in the chat, and `pmos workflow`.
|
|
83
84
|
- [Intelligence reads your OKR file](intelligence-okr-goals.md): each key result of the OKR file is a row, with its reading or the source it needs; Did it work?, All products and Delivery trends.
|
|
85
|
+
- [Reports and dashboards: what needs you](reports-needs-you.md): the reports that wait for you come first, each with one ✓ (Issue, Confirm the definition, Move to a product, Resume); the first dashboard is drafted from the OKR file.
|
|
84
86
|
- [Expected outcomes](outcomes.md): every initiative says what should change once it ships and when to check it. Claude drafts each one from the PRD or the KR, you keep it with ✓, and `pmos outcomes`.
|
|
85
87
|
- [The next prompt, suggested and sharpened](next-prompt.md): after a reply, up to two prompts to send next; while you type a rough prompt, a sharper version in your style (Polish, Prompt engineer, Your own).
|
|
86
88
|
- [Rehearse a plan](rehearse.md): before you approve a date change or a cut, Claude shows what it breaks or helps in other initiatives, goals and program milestones, with the record each effect comes from, and `pmos rehearse`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Guide
|
|
3
|
+
title: "Reports and dashboards: what needs you"
|
|
4
|
+
description: "How the Reports and dashboards panel in Intelligence lists the reports that wait for you first, each with one ✓, drafts the first dashboard from the OKR file, and how + makes a report."
|
|
5
|
+
timestamp: 2026-11-03
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Reports and dashboards: what needs you
|
|
9
|
+
|
|
10
|
+
Open Intelligence (`/premium-intelligence.html`), then **⋯ › Reports and dashboards**. The panel shows these parts, in this order:
|
|
11
|
+
|
|
12
|
+
1. **Needs you.** Each report that waits for you. A row shows the report name, the draft date, the readers, and the first line of the headline. Press the name to open the report or its setup. Each row has one ✓:
|
|
13
|
+
- **Issue**: the draft is ready. The ✓ issues the draft as you see it. Send it from your own mail.
|
|
14
|
+
- **Confirm the definition**: an agent proposed the definition of this report (for example the daily report), and nobody confirmed it yet. The ✓ changes `status: draft` to `status: stable` in the definition. It changes nothing else.
|
|
15
|
+
- **Move to <product>**: the report is about a different product than the one it is in. For example, the Scarlet assessment report was set up in the IDPF workspace. The ✓ moves the definition, the drafts and the setup to that product. A report that has an issued issue is not moved, because an issued report stays where it was issued.
|
|
16
|
+
- **Resume**: the report is paused. The ✓ starts its schedule again.
|
|
17
|
+
2. **Drafting on their moment.** The ready-made reviews (quarter, milestone, stage, did it work). They draft when their moment comes. Open the fold to see each switch.
|
|
18
|
+
3. **Weekly updates.** One line tells how many are on and off. Open the fold to see each switch.
|
|
19
|
+
4. **Yours.** The other reports that you set up.
|
|
20
|
+
5. **Dashboards.** When no dashboard is saved, Claude drafts the first one from the OKR file: one tile for each key result. A tile that has a reading shows the value, its reader and the date. A tile with no reading shows the source it needs, and its ✓ opens Settings › Connectors. Press ✓ on the card to keep the dashboard. It keeps the key results that have a reading.
|
|
21
|
+
|
|
22
|
+
A report from the daily, weekly or monthly definition has the title of its definition, for example "Daily report", not its id. Its preview is the headline. The line that says the definition is a draft is not the preview.
|
|
23
|
+
|
|
24
|
+
## + in the panel
|
|
25
|
+
|
|
26
|
+
**+** opens the report that Claude drafts from the view on the screen. It does not open an empty chat. With all products on the screen, **+** opens Home, because a report is for one product.
|
|
27
|
+
|
|
28
|
+
## Edit chart
|
|
29
|
+
|
|
30
|
+
**Edit chart** on a key result that has no reading shows the connector card for its source. When no dashboard is saved, **Save chart** keeps the drafted dashboard first.
|
|
31
|
+
|
|
32
|
+
## The product page
|
|
33
|
+
|
|
34
|
+
The ✦ line on a product page is the same line as on Intelligence for that product. "Your weekly update is ready to send" shows only in a week with news. In a quiet week the Weekly update row says "Nothing new this week · skipped".
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* report-acts.js — the one ✓ of a report that waits for the PM, in Reports and dashboards (Reports and dashboards redesign R, frame RgR1Panel;
|
|
3
|
+
* brief /mnt/project-files/intelligence-grill/brief-E-R-build.md step 9).
|
|
4
|
+
*
|
|
5
|
+
* defTitleOf(def) → the definition's title as a report name: "Daily report definition" → "Daily report". PURE
|
|
6
|
+
* reportAboutOf(text, boards, here) → { ws, name } of the one OTHER product a report names, or null. PURE
|
|
7
|
+
* reportActOf({ id, setup, rows, def, about, sha }) → { act, label, … } | null the report's one press. PURE
|
|
8
|
+
* reportActsOf(facts) → { [report id]: act } for every report of the model, and { names, about }. PURE
|
|
9
|
+
* leadOf(text) the opening sentence, past a line that only says the definition is not confirmed yet. PURE
|
|
10
|
+
*
|
|
11
|
+
* WHY. The grill (3 Nov, on 0.514.0) found the five drafts that wait for the PM at the bottom of the panel, under "Yours", named by their id
|
|
12
|
+
* ("daily"), previewed by the warning "this definition is a draft", and with no press that settles them. Each waiting report now has ONE ✓,
|
|
13
|
+
* the act that unblocks it, in this order:
|
|
14
|
+
* move the report is about another product than the one it sits in (Scarlet assessment in the IDPF workspace) → "Move to Scarlet"
|
|
15
|
+
* confirm its draft follows a definition an agent proposed and nobody has confirmed (status: draft, no setup) → "Confirm the definition"
|
|
16
|
+
* issue a draft waits and its definition is confirmed → "Issue" (POST /api/report/issue with the sha of the file as read)
|
|
17
|
+
* resume a set-up report is paused → "Resume"
|
|
18
|
+
* A report with none of these is not waiting for the PM and gets no ✓ (zero is silence).
|
|
19
|
+
*
|
|
20
|
+
* WHAT A NAME MATCHES. A product is named when its board name, as a whole word of four letters or more, is in the report's name or purpose and
|
|
21
|
+
* the product it sits in is not named too. Two other products named → no move: the report does not say which one it is about.
|
|
22
|
+
*/
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const RS = () => require('./report-setups.js'); // read late: report-setups is wide, and this file only writes through its one writer
|
|
26
|
+
const refuse = (status, error) => ({ ok: false, status, error });
|
|
27
|
+
const clean = (s) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
|
|
28
|
+
const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
29
|
+
|
|
30
|
+
/** "Daily report definition" → "Daily report"; "Scarlet assessment report definition" → "Scarlet assessment report". '' when there is none. PURE. */
|
|
31
|
+
function defTitleOf(def) {
|
|
32
|
+
const t = clean(def && def.title).replace(/^["']|["']$/g, '');
|
|
33
|
+
if (!t || (def && t === def.id)) return '';
|
|
34
|
+
return t.replace(/\s+(definition|template)$/i, '').replace(/\s+template$/i, '').trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A product's name in `text` as a whole word (case-insensitive). Names shorter than four letters never match: "PI" is not a product name in prose. */
|
|
38
|
+
function mentionsProduct(text, name) {
|
|
39
|
+
const n = clean(name);
|
|
40
|
+
if (n.length < 4) return false;
|
|
41
|
+
return new RegExp('(^|[^A-Za-z0-9])' + escRe(n) + '(?![A-Za-z0-9])', 'i').test(String(text || ''));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The one other product a report is about. boards: [{ ws, name }]; here: the ws it sits in. → { ws, name } | null. PURE. */
|
|
45
|
+
function reportAboutOf(text, boards, here) {
|
|
46
|
+
const B = (boards || []).filter((b) => b && b.ws && b.name);
|
|
47
|
+
const mine = B.find((b) => b.ws === here);
|
|
48
|
+
if (mine && mentionsProduct(text, mine.name)) return null;
|
|
49
|
+
const hit = B.filter((b) => b.ws !== here && mentionsProduct(text, b.name));
|
|
50
|
+
return hit.length === 1 ? { ws: hit[0].ws, name: clean(hit[0].name) } : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A report's one press. rows: its files, newest first ({ slot, state, path, archived }); setup: its setup or null; def: its definition or null.
|
|
54
|
+
* → { act: 'move'|'confirm'|'issue'|'resume', label, … } | null. PURE. */
|
|
55
|
+
function reportActOf({ id, setup, rows, def, about, sha }) {
|
|
56
|
+
const live = (rows || []).filter((r) => r && !r.archived);
|
|
57
|
+
const draft = live.find((r) => r.state !== 'issued') || null;
|
|
58
|
+
const issued = live.some((r) => r.state === 'issued');
|
|
59
|
+
if (about && setup && !issued) return { act: 'move', label: 'Move to ' + about.name, to: about.ws, toName: about.name, id };
|
|
60
|
+
if (setup && setup.paused) return { act: 'resume', label: 'Resume', id };
|
|
61
|
+
if (!draft) return null;
|
|
62
|
+
// an agent-written definition (the daily, weekly and monthly skills write status: draft and only the PM confirms it). A report the PM set up
|
|
63
|
+
// wrote its own definition with that press, so its draft is issued
|
|
64
|
+
if (!setup && def && def.status === 'draft') return { act: 'confirm', label: 'Confirm the definition', id, slot: draft.slot };
|
|
65
|
+
return { act: 'issue', label: 'Issue', id, slot: draft.slot, sha: sha || null, path: draft.path || '' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Every report's press and display name. f: { rows, setups, defs, boards, here, shas }. → { acts, names, about }. PURE. */
|
|
69
|
+
function reportActsOf(f) {
|
|
70
|
+
const rows = (f.rows || []).filter(Boolean), setups = (f.setups || []).filter(Boolean), defs = new Map((f.defs || []).filter(Boolean).map((d) => [d.id, d]));
|
|
71
|
+
const ids = [...new Set(setups.map((s) => s.id).concat(rows.map((r) => r.cadence)))];
|
|
72
|
+
const acts = {}, nameOf = {}, about = {};
|
|
73
|
+
for (const id of ids) {
|
|
74
|
+
const setup = setups.find((s) => s.id === id) || null, def = defs.get(id) || null, mine = rows.filter((r) => r.cadence === id);
|
|
75
|
+
const t = defTitleOf(def);
|
|
76
|
+
if (!setup && t) nameOf[id] = t;
|
|
77
|
+
const a = setup ? reportAboutOf([setup.name, setup.purpose].join(' · '), f.boards, f.here) : null;
|
|
78
|
+
if (a) about[id] = a;
|
|
79
|
+
const draft = mine.find((r) => !r.archived && r.state !== 'issued');
|
|
80
|
+
const act = reportActOf({ id, setup, rows: mine, def, about: a, sha: draft && f.shas ? f.shas[draft.path] : null });
|
|
81
|
+
if (act) acts[id] = act;
|
|
82
|
+
}
|
|
83
|
+
return { acts, names: nameOf, about };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A line that only says the definition is not confirmed: the daily skill puts it first in the Headline (report-daily.skill step 6). */
|
|
87
|
+
const WARN = /\bdefinition\b[^.]*\b(draft|unconfirmed|not (yet )?confirmed|proposed|waits? for your)\b|\b(draft|unconfirmed|proposed)\b[^.]*\bdefinition\b/i;
|
|
88
|
+
/** The opening sentence of a report, past the definition warning: its headline is the preview. `lead` is the caller's leadOf (insights-premium). PURE. */
|
|
89
|
+
function leadPastWarning(text, lead) {
|
|
90
|
+
const first = lead(text);
|
|
91
|
+
if (!first || !WARN.test(first)) return first;
|
|
92
|
+
const t = String(text || ''), at = t.indexOf(first.replace(/…$/, '').slice(0, 40));
|
|
93
|
+
// the same paragraph's next sentence, else the next paragraph
|
|
94
|
+
if (at >= 0) {
|
|
95
|
+
const rest = t.slice(at).replace(/^[^]*?[.!?](\s+|$)/, '');
|
|
96
|
+
const next = lead('\n\n' + rest);
|
|
97
|
+
if (next && !WARN.test(next)) return next;
|
|
98
|
+
}
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── the writes of the ✓ (their routes: lib/routes-report-setups.js confirm and move) ─────────────────────────────────────────────
|
|
103
|
+
/** Confirm a definition (Reports and dashboards redesign R): the PM's ✓ turns `status: draft` into `status: stable` in its frontmatter. Only
|
|
104
|
+
* that line changes. A definition with no frontmatter status, or one confirmed already, is left as it is. → { ok, path, sha, unchanged? } */
|
|
105
|
+
function confirmDefinition(root, id, opts = {}) {
|
|
106
|
+
if (!RS().ID_RE.test(String(id || ''))) return refuse(400, 'the id is lower-case letters, digits and "-"');
|
|
107
|
+
const file = RS().definitionFile(root, id), rel = require('./report-draft.js').definitionPath(root, id);
|
|
108
|
+
let cur;
|
|
109
|
+
try { cur = fs.readFileSync(file, 'utf8'); } catch { return refuse(404, `there is no definition ${rel}`); }
|
|
110
|
+
const m = /^(---\r?\n)([\s\S]*?)(\r?\n---)/.exec(cur);
|
|
111
|
+
if (!m) return refuse(409, `${rel} has no frontmatter, so there is no status to confirm`);
|
|
112
|
+
if (!/^status:\s*["']?draft["']?\s*$/m.test(m[2])) return { ok: true, path: rel, sha: RS().shaOf(cur), unchanged: true };
|
|
113
|
+
const next = m[1] + m[2].replace(/^status:\s*["']?draft["']?\s*$/m, 'status: stable') + m[3] + cur.slice(m[0].length);
|
|
114
|
+
const tmp = file + '.tmp-' + process.pid;
|
|
115
|
+
try { fs.writeFileSync(tmp, next); fs.renameSync(tmp, file); } catch (e) { try { fs.unlinkSync(tmp); } catch { /* none */ } return refuse(500, 'could not write the definition; nothing changed: ' + String((e && e.message) || e).slice(0, 160)); }
|
|
116
|
+
void opts;
|
|
117
|
+
return { ok: true, path: rel, sha: RS().shaOf(next) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Copy a folder's files, recursively, refusing to overwrite. → the repository-relative paths written under `to`. */
|
|
121
|
+
function copyReportTree(from, to) {
|
|
122
|
+
const out = [];
|
|
123
|
+
fs.mkdirSync(to, { recursive: true });
|
|
124
|
+
for (const e of fs.readdirSync(from, { withFileTypes: true })) {
|
|
125
|
+
const a = path.join(from, e.name), b = path.join(to, e.name);
|
|
126
|
+
if (e.isDirectory()) out.push(...copyReportTree(a, b));
|
|
127
|
+
else if (e.isFile()) { fs.copyFileSync(a, b, fs.constants.COPYFILE_EXCL); out.push(b); }
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Move a set-up report to the product it is about (Reports and dashboards redesign R, "✦ Move to Scarlet"): its definition, its drafts and its
|
|
134
|
+
* setup line go to `toRoot`, and it is removed here. A report with an ISSUED issue is refused (409): an issued report is never moved or deleted,
|
|
135
|
+
* it stays where it was issued. The id must be free in the target. Everything is copied before anything is removed here, so a failed copy
|
|
136
|
+
* leaves this workspace as it was (and removes what it copied). → { ok, id, moved: [rel] } | { ok: false, status, error }
|
|
137
|
+
*/
|
|
138
|
+
function moveReportTo(root, id, toRoot, opts = {}) {
|
|
139
|
+
const abs = path.resolve(String(root)), to = path.resolve(String(toRoot || ''));
|
|
140
|
+
if (!toRoot || to === abs) return refuse(400, 'say which product to move it to');
|
|
141
|
+
const file = RS().ledgerFile(abs), st = RS().fold(RS().readLines(file)), cur = st.setups.get(id);
|
|
142
|
+
if (!cur) return refuse(404, `no recurring report ${String(id).slice(0, 40)}`);
|
|
143
|
+
const docs = (require('./report-needs.js').reportsOf(abs).reports || []).filter((r) => r.cadence === id);
|
|
144
|
+
if (docs.some((r) => r.issued)) return refuse(409, `${cur.name} has issued reports. An issued report stays where it was issued, so it was not moved`);
|
|
145
|
+
const tst = RS().fold(RS().readLines(RS().ledgerFile(to)));
|
|
146
|
+
const srcDef = RS().definitionFile(abs, id), dstDef = RS().definitionFile(to, id);
|
|
147
|
+
const srcDir = path.join(RS().stateDir(abs), 'reports', id), dstDir = path.join(RS().stateDir(to), 'reports', id);
|
|
148
|
+
if (tst.setups.has(id) || fs.existsSync(dstDef) || fs.existsSync(dstDir)) return refuse(409, `${id} is taken in that product already. Nothing was moved`);
|
|
149
|
+
const now = opts.now == null ? new Date() : new Date(opts.now);
|
|
150
|
+
const written = [];
|
|
151
|
+
try {
|
|
152
|
+
if (fs.existsSync(srcDef)) {
|
|
153
|
+
const t = fs.readFileSync(srcDef, 'utf8').replace(/^(product:\s*).*$/m, '$1' + JSON.stringify(path.basename(to)));
|
|
154
|
+
fs.mkdirSync(path.dirname(dstDef), { recursive: true }); fs.writeFileSync(dstDef, t, { flag: 'wx' }); written.push(dstDef);
|
|
155
|
+
}
|
|
156
|
+
if (fs.existsSync(srcDir)) written.push(...copyReportTree(srcDir, dstDir));
|
|
157
|
+
} catch (e) {
|
|
158
|
+
for (const w of written.reverse()) { try { fs.unlinkSync(w); } catch { /* gone */ } }
|
|
159
|
+
try { fs.rmSync(dstDir, { recursive: true, force: true }); } catch { /* gone */ }
|
|
160
|
+
return refuse(500, 'the copy failed, so nothing was moved: ' + String((e && e.message) || e).slice(0, 160));
|
|
161
|
+
}
|
|
162
|
+
// the setup line in the target, as it is here, with its new home; then the schedule moves and it is removed here
|
|
163
|
+
const line = { ev: 'setup' };
|
|
164
|
+
for (const k of ['id', 'name', 'purpose', 'audience', 'source', 'output', 'schedule', 'sources', 'sections', 'learned_from', 'fixes', 'last_run', 'def_sha', 'from', 'storyline', 'ready']) if (cur[k] !== undefined) line[k] = cur[k];
|
|
165
|
+
line.product = path.basename(to); line.ws = opts.toWsId || null; line.moved_from = path.basename(abs);
|
|
166
|
+
RS().append(RS().ledgerFile(to), line, { now, by: opts.by });
|
|
167
|
+
if (cur.status === 'paused') RS().append(RS().ledgerFile(to), { ev: 'pause', id }, { now, by: opts.by });
|
|
168
|
+
if (cur.source === 'work') {
|
|
169
|
+
RS().unregisterSchedule(opts.scheduleStore, cur, opts.wsId);
|
|
170
|
+
if (cur.status !== 'paused') RS().registerSchedule(opts.scheduleStore, Object.assign({}, cur, { product: line.product }), { wsId: opts.toWsId, lastRun: cur.last_run, now });
|
|
171
|
+
}
|
|
172
|
+
RS().append(file, { ev: 'remove', id }, { now, by: opts.by });
|
|
173
|
+
try { if (fs.existsSync(srcDef)) fs.unlinkSync(srcDef); } catch { /* the copy is in place; a stale definition here is harmless */ }
|
|
174
|
+
try { fs.rmSync(srcDir, { recursive: true, force: true }); } catch { /* the drafts stay here too */ }
|
|
175
|
+
return { ok: true, id, moved: written.map((w) => path.relative(to, w)) };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The product page's ✦ line is Intelligence's (RgR4Make, "one brain"). The grill found the product page saying "2 things are behind: KR-C4,
|
|
179
|
+
* KR-A1 · weekly update ready" while Intelligence said "I have not read a goal yet". Both now read the same model's line (lib/insights-premium.js
|
|
180
|
+
* okrOf → model.okr.line); "Your weekly update is ready to send" follows it only in a week with news. Without that line, the dashboard's own
|
|
181
|
+
* line stays. PURE. */
|
|
182
|
+
function productLineOfIntel(intel, { deskReady, news, fallback }) {
|
|
183
|
+
const L = intel && intel.okr && intel.okr.line;
|
|
184
|
+
if (!L || !L.head) return fallback;
|
|
185
|
+
const rest = [L.sub || '', deskReady && news ? 'Your weekly update is ready to send.' : ''].filter(Boolean).join(' ');
|
|
186
|
+
return { lead: L.head, rest, tone: L.tone === 'crit' || L.tone === 'behind' ? 'warn' : 'calm' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { defTitleOf, reportAboutOf, reportActOf, reportActsOf, leadPastWarning, WARN, confirmDefinition, moveReportTo, productLineOfIntel };
|
|
@@ -34,14 +34,19 @@ async function via(mod, pathAndQuery, req, ctx) {
|
|
|
34
34
|
}
|
|
35
35
|
/** The opening sentence of each listed report file (insights-one-list L6), read from the workspace's repository. A file that cannot be read has none. */
|
|
36
36
|
const LEADS_MAX = 60;
|
|
37
|
-
function leadsOf(ctx, ws, list) {
|
|
37
|
+
function leadsOf(ctx, ws, list, shas) {
|
|
38
38
|
const out = {};
|
|
39
39
|
let root = null;
|
|
40
40
|
try { root = require('path').resolve(ctx.getWs(ws).root); } catch { return out; }
|
|
41
41
|
for (const r of list.slice().sort((a, b) => String(b.slot).localeCompare(String(a.slot))).slice(0, LEADS_MAX)) {
|
|
42
42
|
const rel = String(r.path || '');
|
|
43
43
|
if (!rel || rel.includes('..') || require('path').isAbsolute(rel)) continue;
|
|
44
|
-
try {
|
|
44
|
+
try {
|
|
45
|
+
const buf = require('fs').readFileSync(require('path').join(root, rel));
|
|
46
|
+
// Reports and dashboards redesign R: the headline is the preview, never the line that says the definition is a draft
|
|
47
|
+
out[rel] = require('./report-acts.js').leadPastWarning(buf.toString('utf8').slice(0, 20000), Insights.leadOf);
|
|
48
|
+
if (shas) shas[rel] = require('crypto').createHash('sha1').update(`blob ${buf.length}\0`).update(buf).digest('hex'); // what Issue sends: the file as read
|
|
49
|
+
} catch { /* an unreadable file has no lead */ }
|
|
45
50
|
}
|
|
46
51
|
return out;
|
|
47
52
|
}
|
|
@@ -96,7 +101,11 @@ module.exports = {
|
|
|
96
101
|
const su = await via(setups, '/api/report-setups' + q, req, ctx).catch(() => ({ status: 500, body: null }));
|
|
97
102
|
const setupsOk = su.status === 200 && !!su.body && Array.isArray(su.body.setups);
|
|
98
103
|
if (!setupsOk) missing.push('report setups');
|
|
99
|
-
const
|
|
104
|
+
const shas = {};
|
|
105
|
+
const leads = reportsOk ? leadsOf(ctx, ws, rp.body.reports || [], shas) : {};
|
|
106
|
+
// Reports and dashboards redesign R: each report's definition (its title names it; status: draft is "Confirm the definition")
|
|
107
|
+
let defs = [];
|
|
108
|
+
if (pick) { try { defs = require('./report-setups.js').definitionsOf(require('path').resolve(ctx.getWs(pick.ws).root)).map((d) => ({ id: d.id, title: d.title, status: d.status })); } catch { defs = []; } }
|
|
100
109
|
// premium-agreed-design (G1, G2): the board's initiatives, so a goal can name what moves it and Outcomes can list shipped work. Read-only.
|
|
101
110
|
let initiatives = null;
|
|
102
111
|
if (pick && pick.id) {
|
|
@@ -137,6 +146,11 @@ module.exports = {
|
|
|
137
146
|
outcomes, reports: reportsOk ? rp.body.reports || [] : [], reportsOk, missing, saved, mine,
|
|
138
147
|
setups: setupsOk ? su.body : null, pinned, leads, archived, initiatives, goalMeta, ins, budgets, okr, promises, self: isSelf(ctx, pick),
|
|
139
148
|
});
|
|
149
|
+
// Reports and dashboards redesign R (RgR1Panel): each waiting report's one ✓, its display name, and the other product it is about (lib/report-acts.js)
|
|
150
|
+
if (model && model.reports) {
|
|
151
|
+
const R = require('./report-acts.js').reportActsOf({ rows: model.reports.rows || [], setups: model.reports.setups || [], defs, boards: (model.board && model.board.switcher) || [], here: model.board ? model.board.ws : null, shas });
|
|
152
|
+
Object.assign(model.reports, { acts: R.acts, names: R.names, about: R.about });
|
|
153
|
+
}
|
|
140
154
|
// batch 16 item 1: each ✦ strip finding about a goal that is behind carries one drafted move (lib/insight-moves.js), done ones marked
|
|
141
155
|
if (pick) {
|
|
142
156
|
try {
|
|
@@ -225,7 +225,14 @@ async function dashboardAnswer(req, ctx, w, scope, from) {
|
|
|
225
225
|
}
|
|
226
226
|
if (!X.model) X.f.goalDraft = null;
|
|
227
227
|
else if (!RR.goalsOf(X.f.intel, scope, Date.parse(X.f.today + 'T00:00:00Z'), false).goals.length) X.f.goalDraft = { title: '', sub: '', anchor: scope };
|
|
228
|
-
|
|
228
|
+
const d = RR.dashboardOf(X.f);
|
|
229
|
+
// Reports and dashboards redesign R (RgR4Make): a product page's ✦ line is Intelligence's line for that product (lib/report-acts.js). A held
|
|
230
|
+
// source keeps its own warning; "ready to send" follows only in a week with news (something behind, a ticket closed, an update drafted)
|
|
231
|
+
if (d && d.level === 'product' && !d.heldBy) {
|
|
232
|
+
const R = d.reports || [], news = (d.goals || []).some((g) => g.dot === 'behind') || !!d.done || R.some((r) => r.kind === 'weekly' && r.state === 'drafted' && !r.desk);
|
|
233
|
+
d.handled = require('./report-acts.js').productLineOfIntel(X.f.intel, { deskReady: R.some((r) => r.kind === 'weekly' && r.state === 'drafted' && r.desk), news, fallback: d.handled });
|
|
234
|
+
}
|
|
235
|
+
return { status: 200, body: { ok: true, dashboard: d } };
|
|
229
236
|
}
|
|
230
237
|
|
|
231
238
|
/** The scopes of a workspace: its product, its programs (the ones whose charter is here) and its live initiatives. */
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* POST /api/report-setups/pause | resume { ws, id } → { ok, setup }
|
|
18
18
|
* POST /api/report-setups/edit { ws, id, schedule?, name?, audience?, output? } → { ok, setup } change a TEMPLATE (premium-reports-from-chat L1).
|
|
19
19
|
* schedule { cadence: 'off' } pauses it; any other schedule changes it and resumes it
|
|
20
|
+
* POST /api/report-setups/confirm { ws, id } → { ok, path, sha } ✓ Confirm the definition: status: draft → stable (redesign R)
|
|
21
|
+
* POST /api/report-setups/move { ws, id, to } → { ok, id, to, moved } ✦ Move to <product>: definition, drafts and setup go to
|
|
22
|
+
* the workspace `to`; a report with an issued issue is 409 (redesign R)
|
|
20
23
|
* POST /api/report-setups/remove { ws, id } → { ok } delete a TEMPLATE: its schedule stops; its drafts and issued reports stay (L2)
|
|
21
24
|
* POST /api/report-setups/from-dashboard { ws, key, name, output, schedule, audience } → { ok, setup }
|
|
22
25
|
* POST /api/report-setups/finder { ws, cadence?, sources? } → { ok, finder }
|
|
@@ -62,7 +65,7 @@ const headless = require('./headless.js');
|
|
|
62
65
|
|
|
63
66
|
const text = (v) => (typeof v === 'string' ? v : '');
|
|
64
67
|
const bad = (status, error) => ({ status, body: { ok: false, error } });
|
|
65
|
-
const POSTS = ['ask', 'change', 'cancel', 'setup', 'draft', 'dismiss', 'pause', 'resume', 'from-dashboard', 'finder', 'edit', 'remove', 'documents/discard', 'documents/archive', 'strip-dismiss'];
|
|
68
|
+
const POSTS = ['ask', 'change', 'cancel', 'setup', 'draft', 'dismiss', 'pause', 'resume', 'from-dashboard', 'finder', 'edit', 'remove', 'documents/discard', 'documents/archive', 'strip-dismiss', 'confirm', 'move'];
|
|
66
69
|
const IR = require('./intel-report.js');
|
|
67
70
|
const MADE_BY = ['schedule', 'you', 'agent'];
|
|
68
71
|
|
|
@@ -508,6 +511,23 @@ async function handle(req, res, url, ctx) {
|
|
|
508
511
|
const now = new Date();
|
|
509
512
|
return send({ status: 200, body: { ok: true, setup: setupOut(RS.fold(RS.readLines(file)).setups.get(id), now, documentsOf(ctx, w), copyIndex(root), root) } });
|
|
510
513
|
}
|
|
514
|
+
if (sub === 'confirm') {
|
|
515
|
+
// Reports and dashboards redesign R: ✓ Confirm the definition (status: draft → stable). Only the PM's press confirms one (report-daily.skill)
|
|
516
|
+
const r = require('./report-acts.js').confirmDefinition(root, text(body.id), opts);
|
|
517
|
+
if (!r.ok) return fail(r);
|
|
518
|
+
if (ctx.invalidateNeedsAgg) ctx.invalidateNeedsAgg();
|
|
519
|
+
return send({ status: 200, body: r });
|
|
520
|
+
}
|
|
521
|
+
if (sub === 'move') {
|
|
522
|
+
// Reports and dashboards redesign R: ✦ Move to <product>. The target is a workspace this server knows; an issued report is never moved (409)
|
|
523
|
+
const t = wsOf(ctx, text(body.to));
|
|
524
|
+
if (!t || !text(body.to)) return send(bad(404, 'no such product to move it to'));
|
|
525
|
+
if (t.id === w.id) return send(bad(400, 'it is in that product already'));
|
|
526
|
+
const r = require('./report-acts.js').moveReportTo(root, text(body.id), t.root, Object.assign({}, opts, { toWsId: t.id }));
|
|
527
|
+
if (!r.ok) return fail(r);
|
|
528
|
+
if (ctx.invalidateNeedsAgg) ctx.invalidateNeedsAgg();
|
|
529
|
+
return send({ status: 200, body: Object.assign({ to: t.id }, r) });
|
|
530
|
+
}
|
|
511
531
|
if (sub === 'remove') {
|
|
512
532
|
const r = RS.remove(root, text(body.id), opts);
|
|
513
533
|
if (!r.ok) return fail(r);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.516.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.516.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@anthropic-ai/sdk": "^1.3.0",
|
package/workspace/package.json
CHANGED
|
@@ -39,6 +39,10 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
39
39
|
srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
|
|
40
40
|
await T.waitForServer(BASE);
|
|
41
41
|
browser = await chromium.launch({ executablePath: exe });
|
|
42
|
+
/* One clock for the page and the stand-ins: today at 11:00, so "Rail review" (30 minutes on) is today at any hour the suite runs.
|
|
43
|
+
* Unpinned, after 23:30 it fell on tomorrow and S6 lost its Record. */
|
|
44
|
+
const NOW = new Date(); NOW.setHours(11, 0, 0, 0);
|
|
45
|
+
const open = (u, o) => F.open(browser, BASE, u, { now: NOW, ...o });
|
|
42
46
|
const body = (p) => p.evaluate(() => (document.querySelector('#czViewer .cz-vw-body') || {}).innerText || '');
|
|
43
47
|
const press = (p, sel) => p.$eval('#czViewer ' + sel, (b) => b.click());
|
|
44
48
|
const folds = (p) => p.evaluate(() => [...document.querySelectorAll('#czViewer [data-czs-fold]')].map((x) => x.dataset.czsFold + (x.getAttribute('aria-expanded') === 'true' ? '*' : '')).join());
|
|
@@ -46,7 +50,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
46
50
|
const sum = (p) => p.evaluate(() => { const s = [...document.querySelectorAll('#czViewer .czs-sum')].find((x) => x.offsetParent) || document.querySelector('#czViewer .czs-sum'); return s ? { line: (s.querySelector('.czs-line') || {}).innerText || '', text: (s.querySelector('.czs-p') || {}).innerText || '', meta: (s.querySelector('.czs-meta') || {}).innerText || '', pri: (s.querySelector('[data-czs-act]') || {}).textContent || '' } : null; });
|
|
47
51
|
|
|
48
52
|
/* an ended meeting with notes and a recording */
|
|
49
|
-
const A = await
|
|
53
|
+
const A = await open('/premium-viewer.html?open=meeting¬e=n1');
|
|
50
54
|
const p = A.page;
|
|
51
55
|
const up = await until(() => p.evaluate(() => !!document.querySelector('#czViewer .vz-mtg[data-mk="notes"]') && !!document.querySelector('#czViewer .czs-sum')), 15000);
|
|
52
56
|
ok('0 the meeting opens in the Viewer on its own page', up);
|
|
@@ -128,7 +132,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
128
132
|
await A.ctx.close();
|
|
129
133
|
|
|
130
134
|
/* S6 Before: a meeting to come, no note */
|
|
131
|
-
const B = await
|
|
135
|
+
const B = await open('/premium-viewer.html?open=meeting&event=ev-rail&date=' + F.dayKey(F.world(NOW).RAIL), { scene: 'prep' });
|
|
132
136
|
const q = B.page;
|
|
133
137
|
const prepUp = await until(() => q.evaluate(() => !!document.querySelector('#czViewer .vz-mtg[data-mk="prep"]') && !!document.querySelector('#czViewer .czs-sum')), 15000);
|
|
134
138
|
const s6 = await sum(q);
|
|
@@ -169,7 +173,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
169
173
|
await B.ctx.close();
|
|
170
174
|
|
|
171
175
|
/* S7 During: a note that records */
|
|
172
|
-
const C = await
|
|
176
|
+
const C = await open('/premium-viewer.html?open=meeting¬e=n2', { init: () => { window.__premiumHolding = { id: 'n2' }; } });
|
|
173
177
|
const r = C.page;
|
|
174
178
|
const liveUp = await until(() => r.evaluate(() => !!document.querySelector('#czViewer .vz-mtg[data-mk="live"]') && !!document.querySelector('#czViewer .czs-sum')), 15000);
|
|
175
179
|
const s7 = await sum(r);
|
|
@@ -196,7 +200,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
196
200
|
await C.ctx.close();
|
|
197
201
|
|
|
198
202
|
/* S10 a note that is not there */
|
|
199
|
-
const D = await
|
|
203
|
+
const D = await open('/premium-viewer.html?open=meeting¬e=nope');
|
|
200
204
|
ok('S10 a note that is not there says so, and the card still opens', await until(() => D.page.evaluate(() => !!document.querySelector('#czViewer') && /Untitled note|could not|not/.test(document.querySelector('#czViewer').innerText))));
|
|
201
205
|
await D.ctx.close();
|
|
202
206
|
|
|
@@ -204,7 +208,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
204
208
|
// the meeting's own presses: the summary's (the shell's primary button is the shell's) and the folds; a press may keep its drawn size and take a 44 px hit area from ::before
|
|
205
209
|
const HITS = () => { window.hits = () => [...document.querySelectorAll('#czViewer .czs-sum button:not(.czs-pri), #czViewer [data-czs-fold]')].filter((b) => { if (!b.getClientRects().length) return false; const pb = getComputedStyle(b, '::before'), hit = pb.content !== 'none' && pb.position === 'absolute' ? parseFloat(pb.height) || 0 : 0; return Math.max(b.getBoundingClientRect().height, hit) < 44; }).map((b) => (b.dataset.a || b.dataset.czsFold || b.className) + ':' + Math.round(b.getBoundingClientRect().height)); };
|
|
206
210
|
for (const [u, o] of [['/premium-viewer.html?open=meeting¬e=n1', {}], ['/premium-viewer.html?open=meeting¬e=n2', { init: () => { window.__premiumHolding = { id: 'n2' }; } }]]) {
|
|
207
|
-
const E = await
|
|
211
|
+
const E = await open(u, { w: 390, h: 844, ...o });
|
|
208
212
|
await E.page.evaluate(HITS);
|
|
209
213
|
await until(() => E.page.evaluate(() => !!document.querySelector('#czViewer .vz-mtg') && !!document.querySelector('#czViewer .czs-sum')), 15000);
|
|
210
214
|
const fl = await until(async () => { const v = await E.page.evaluate(() => ({ over: document.documentElement.scrollWidth - innerWidth, full: (() => { const r = document.querySelector('#czViewer').getBoundingClientRect(); return r.left === 0 && Math.round(r.width) === innerWidth; })(),
|
|
@@ -955,3 +955,37 @@ body.ins-trv #pGoals .ins-trstrip { display: flex; } /* the narrow form hides
|
|
|
955
955
|
.ins-row.ins-pcols { grid-template-columns: minmax(0, 1fr); row-gap: 6px; border: 1px solid var(--cz-hair); border-radius: 18px; background: var(--cz-card); padding: 12px 14px; }
|
|
956
956
|
.ins-allp .ins-th { display: none; }
|
|
957
957
|
}
|
|
958
|
+
|
|
959
|
+
/* Reports and dashboards redesign R (RgR1Panel, RgR3Dash): Needs you rows with one ✓ each, the two ready-made folds, the drafted first dashboard */
|
|
960
|
+
.ins-nlist { flex: none; display: flex; flex-direction: column; border: 1px solid var(--cz-hair); border-radius: 16px; overflow: hidden; margin: 4px 0 8px; }
|
|
961
|
+
.ins-nrow { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border-top: 1px solid var(--cz-hair); }
|
|
962
|
+
.ins-nrow:first-child { border-top: 0; }
|
|
963
|
+
.ins-nrow.sel { background: var(--cz-bluebg); }
|
|
964
|
+
.ins-nrow .nm { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; border: 0; background: none; padding: 0; font: inherit; color: inherit; text-align: left; cursor: pointer; }
|
|
965
|
+
.ins-nrow .nm b { font-size: var(--fs-body); font-weight: var(--fw-strong); line-height: var(--lh-body); }
|
|
966
|
+
.ins-nrow .nm small { font-size: var(--fs-meta); color: var(--cz-ink3); line-height: var(--lh-meta); }
|
|
967
|
+
.ins-nrow .hl { font-size: var(--fs-meta); color: var(--cz-ink2); line-height: var(--lh-meta); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
968
|
+
.ins-nrow .ac { flex: none; display: flex; align-items: center; gap: 8px; }
|
|
969
|
+
.ins-nrow .ac .note { white-space: nowrap; }
|
|
970
|
+
@media (hover: hover) { .ins-nrow:not(.sel):hover { background: rgba(244, 243, 240, .5); } }
|
|
971
|
+
.ins-mlist { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; font-size: var(--fs-meta); line-height: var(--lh-meta); }
|
|
972
|
+
.ins-libfold[open] .ins-mlist { display: none; }
|
|
973
|
+
.ins-libfold:not([open]) .ins-libin { display: none; }
|
|
974
|
+
.ins-ddraft { grid-column: 1 / -1; cursor: default; }
|
|
975
|
+
.ins-dtiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
|
976
|
+
.ins-dtile { position: relative; display: flex; flex-direction: column; gap: 2px; border: 1px solid var(--cz-hair); border-radius: 14px; padding: 10px 12px; min-width: 0; }
|
|
977
|
+
.ins-dtile.w { border-style: dashed; border-color: var(--cz-hair2, rgba(42,46,53,.14)); }
|
|
978
|
+
.ins-dtile .lb { font-size: var(--fs-meta); color: var(--cz-ink2); line-height: var(--lh-meta); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
979
|
+
.ins-dtile .kr { color: var(--cz-ink3); margin-right: 6px; }
|
|
980
|
+
.ins-dtile .big { font-size: var(--fs-panel); font-weight: var(--fw-strong); line-height: var(--lh-panel, 1.3); }
|
|
981
|
+
.ins-dtile .ft { display: flex; align-items: center; gap: 6px; justify-content: space-between; }
|
|
982
|
+
.ins-dtile .ft .cz-ib { flex: none; }
|
|
983
|
+
.ins-tdot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; background: var(--cz-dot-q); }
|
|
984
|
+
.ins-tdot.ok { background: var(--cz-dot-ok); } .ins-tdot.risk { background: var(--cz-dot-risk); } .ins-tdot.late { background: var(--cz-dot-late); }
|
|
985
|
+
.ins-dact { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
|
|
986
|
+
@media (max-width: 760px) {
|
|
987
|
+
.ins-nlist { border: 0; gap: 8px; overflow: visible; }
|
|
988
|
+
.ins-nrow { border: 1px solid var(--cz-hair); border-radius: 16px; background: var(--cz-card); }
|
|
989
|
+
.ins-nrow:first-child { border-top: 1px solid var(--cz-hair); }
|
|
990
|
+
.ins-nrow .ac .note { display: none; }
|
|
991
|
+
}
|
|
@@ -986,7 +986,9 @@ function productPageHtml(x, ws, vid, name, g) {
|
|
|
986
986
|
const tRow = (t) => row(`data-hp="ini" data-k="${esc(t.ini)}"`, '', t.name, t.of, t.late && t.due ? `<span class="pv-late">${esc(lateDays(t.due) + ' days late')}</span>` : '');
|
|
987
987
|
const U = own ? H.weeklyUpdate(S.D, S.D.now) : null, sent = U && (U.sent || []).some((y) => y.week === U.week.iso);
|
|
988
988
|
const R = d ? DB.reportsHtml(d, { open: x.open }) : '';
|
|
989
|
-
|
|
989
|
+
// redesign R (RgR4Make): "Ready to send" only in a week with news (something at risk or waiting for a decision); a quiet week says so
|
|
990
|
+
const news = U && U.sections.some(([k, , L]) => k !== 'ok' && L.length);
|
|
991
|
+
const extra = own ? (R ? '' : row('data-hp="update"', '', 'Weekly update', sent ? 'Sent this week' : news ? 'Ready to send' : 'Nothing new this week · skipped'))
|
|
990
992
|
: `<a class="pv-row" href="${esc(ownHome)}" target="_blank" rel="noopener"><span class="pv-rt"><b>Weekly update</b><small>In ${esc(name)}'s own Home, in a new tab</small></span></a>`;
|
|
991
993
|
const reps = ((d && d.reports) || []).filter((r) => r.kind !== 'heads-up');
|
|
992
994
|
const tabs = [
|
|
@@ -1902,9 +1902,15 @@ async function saveNewDash() {
|
|
|
1902
1902
|
let chartDash = null;
|
|
1903
1903
|
function chartHost(D) { return D && D.saved ? D : savedDash().find((d) => d.id === M.pinned) || savedDash()[0] || null; }
|
|
1904
1904
|
function openChartEdit(goalId, D0) {
|
|
1905
|
-
|
|
1905
|
+
let D = chartHost(D0 || curDash());
|
|
1906
1906
|
const g = M.goals.find((x) => x.id === goalId) || M.goals.find((x) => x.key) || M.goals[0];
|
|
1907
1907
|
if (!g) { toast('There is no goal to chart yet. Add one with New goal.'); return; }
|
|
1908
|
+
// Reports and dashboards redesign R (RgR3Dash): a key result nothing reads opens its connector card, never "no reading yet, make a dashboard first"
|
|
1909
|
+
const kw = !g.readings ? okrKrs().find((k) => k.goal === g.id && k.need && !k.decision) : null;
|
|
1910
|
+
if (kw) { openKr(kw.key); return; }
|
|
1911
|
+
// with no saved dashboard, the chart goes on the one drafted from the OKR file; Save chart keeps that dashboard first
|
|
1912
|
+
if (!D) { const dd = draftedDash(); if (dd && dd.charts.length) D = { id: null, drafted: true, saved: false, name: dd.name, purpose: 'Drafted from the OKR file: one tile per key result with a reading.', charts: dd.charts, main: true }; }
|
|
1913
|
+
chartDash = D;
|
|
1908
1914
|
const on = D && D.charts.includes(g.id), cs = (D && (M.chartSet || {})[D.id] || {})[g.id] || {};
|
|
1909
1915
|
openSide({ kind: 'chart', eye: D ? 'Chart on ' + D.name : 'Chart · no dashboard yet', title: g.title, go: 'Save chart', onGo: saveChart,
|
|
1910
1916
|
goOff: !D ? 'Make a dashboard first, with New dashboard' : '',
|
|
@@ -1962,8 +1968,13 @@ function chartSvg(o) {
|
|
|
1962
1968
|
return `<svg class="ins-ch2" viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(o.pts.length + ' readings as a line')}">${tgt}${pv}<polygon class="ar" points="${line[0].split(',')[0]},${H} ${line.join(' ')} ${last[0]},${H}"/>${pj}<polyline class="ln" points="${line.join(' ')}"/>${ship}<circle class="dt" cx="${last[0]}" cy="${last[1]}" r="3.6" style="fill:${TONE_BAR[o.tone] || TONE_BAR.q}"/></svg>`;
|
|
1963
1969
|
}
|
|
1964
1970
|
async function saveChart() {
|
|
1965
|
-
|
|
1971
|
+
let D = chartDash; const id = $('#chGoal').value, m = $('#sideMsg'); if (!D) return;
|
|
1966
1972
|
$('#sideGo').disabled = true; m.textContent = 'Saving.';
|
|
1973
|
+
if (D.drafted) {
|
|
1974
|
+
const k = await saveDash({ name: D.name, purpose: D.purpose, charts: D.charts.includes(id) ? D.charts : D.charts.concat(id), main: true });
|
|
1975
|
+
if (!k || k.ok !== true || !k.dashboard) { $('#sideGo').disabled = false; m.textContent = (k && k.error) || 'The dashboard could not be kept. Nothing changed.'; return; }
|
|
1976
|
+
D = Object.assign({}, k.dashboard, { saved: true }); chartDash = D;
|
|
1977
|
+
}
|
|
1967
1978
|
const res = await pmPost('/api/insights-records/chart', { ws: M.board.ws, board: M.board.id, dash: D.id, goal: id, form: segVal('Form'), range: segVal('Range'), compare: $('#chCmp').value,
|
|
1968
1979
|
target: $('#chTgt').getAttribute('aria-checked') === 'true', forecast: $('#chFc').getAttribute('aria-checked') === 'true', note: $('#chNote').value.trim(), add: !D.charts.includes(id) });
|
|
1969
1980
|
$('#sideGo').disabled = false;
|
|
@@ -2013,7 +2024,7 @@ function reportGroups() {
|
|
|
2013
2024
|
for (const f of R.missing ? [] : R.rows) { if (!by.has(f.cadence)) by.set(f.cadence, []); by.get(f.cadence).push(f); }
|
|
2014
2025
|
const out = [], shown = (fs) => fs && fs.some((x) => !x.archived);
|
|
2015
2026
|
for (const s of R.setupsOk === false ? [] : (R.setups || [])) { const fs = by.get(s.id); by.delete(s.id); out.push(shown(fs) ? { kind: 'file', x: { id: s.id, name: s.name, setup: s, files: fs } } : { kind: 'setup', x: s }); }
|
|
2016
|
-
for (const [id, fs] of by) if (shown(fs)) out.push({ kind: 'file', x: { id, name: id, setup: null, files: fs } });
|
|
2027
|
+
for (const [id, fs] of by) if (shown(fs)) out.push({ kind: 'file', x: { id, name: (R.names && R.names[id]) || id, setup: null, files: fs } }); // redesign R: a definition report is named by its title
|
|
2017
2028
|
return out;
|
|
2018
2029
|
}
|
|
2019
2030
|
/** A schedule said short, for the status pill: "Every other Monday at 09:00" is "Every other Monday". */
|
|
@@ -2053,31 +2064,118 @@ function drawList() {
|
|
|
2053
2064
|
const reps = reportGroups().filter((g) => hit(g.x.name)), dashes = M.dashboards.filter((d) => hit(dashName(d)));
|
|
2054
2065
|
const nRep = reps.length + asks.length;
|
|
2055
2066
|
$('#repCap').textContent = String(nRep + dashes.length);
|
|
2056
|
-
$('#newRep').href = homeAsk('insights', 'Make a new report: ');
|
|
2067
|
+
$('#newRep').href = homeAsk('insights', 'Make a new report: '); // with no product picked, Home still makes it; with one, + opens the drafted report (below)
|
|
2057
2068
|
const fbox = found.length ? `<div class="ins-fd" aria-label="Found in your work"><div class="ins-fdh"><b>Found in your work · ${found.length}</b><span class="note">Things you write by hand often</span></div>`
|
|
2058
2069
|
+ found.map((f) => `<div class="ins-fcard" data-f="${esc(f.id)}"><button type="button" class="nm" data-fx="open" aria-label="${esc(f.name)}: what PMOS found"><b>${esc(f.name)}</b><small>${esc(f.why)}</small></button><button type="button" class="ins-btn sm dark" data-fx="setup">Set it up</button><button type="button" class="ins-btn sm ghost" data-fx="dismiss">Not a report</button></div>`).join('') + '</div>' : '';
|
|
2059
2070
|
const empty = R.missing ? '<div class="ins-empty"><b>The report files could not be read</b>, so none is listed.</div>'
|
|
2060
2071
|
: R.setupsOk === false ? '<div class="ins-empty"><b>The report setups could not be read</b>, so what you set up is not listed.</div>' : '';
|
|
2061
|
-
/*
|
|
2062
|
-
* (
|
|
2063
|
-
|
|
2072
|
+
/* Reports and dashboards redesign R (RgR1Panel; A's IgA5Reports): Needs you first, each waiting report with one line of its headline and its one ✓
|
|
2073
|
+
* (Issue · Confirm the definition · Move to <product> · Resume, lib/report-acts.js), then Drafting on their moment and Weekly updates (the
|
|
2074
|
+
* ready-made switches, folded), then Yours, then the dashboards. Needs you is first on a phone too, before the one-product note. */
|
|
2075
|
+
const acts = R.acts || {};
|
|
2076
|
+
const waits = reps.filter((G) => acts[G.x.id]), waitAsks = asks.filter((a) => a.state === 'ready');
|
|
2064
2077
|
const rest = reps.filter((G) => !waits.includes(G)), restAsks = asks.filter((a) => !waitAsks.includes(a));
|
|
2065
2078
|
const head3 = '<div class="ins-rl ins-rlh" role="presentation"><span>Report</span><span>Next</span><span>Status</span></div>';
|
|
2066
|
-
const
|
|
2079
|
+
const nNeed = waits.length + waitAsks.length;
|
|
2080
|
+
const needs = nNeed ? `<div class="ins-sec"><h3>Needs you</h3><span class="q">${nNeed}</span></div><div class="ins-nlist" id="repNeeds" role="list">${waits.map(needRowHtml).join('')}${waitAsks.map(askNeedHtml).join('')}</div>` : '';
|
|
2067
2081
|
const table = rest.length + restAsks.length ? `<div class="ins-sec"><h3>Yours</h3><span class="q">${rest.length + restAsks.length}</span></div><div class="ins-rtbl" id="repAsks">${head3}<div role="list">${rest.map(repRowHtml).join('')}${restAsks.map(askRowHtml).join('')}</div></div>`
|
|
2068
2082
|
: !empty && !needs ? '<div class="ins-empty"><b>No report is set up and no report file is on record.</b> Reports are made in Home.</div>' : '';
|
|
2069
|
-
const
|
|
2070
|
-
const
|
|
2071
|
-
$('#repBody').innerHTML = `${oneNote()}${
|
|
2072
|
-
|
|
2083
|
+
const dd = savedDash().length ? null : draftedDash();
|
|
2084
|
+
const dashBlock = `<div class="ins-sec"><h3>Dashboards</h3><span class="q">${dashes.length && !dd ? dashes.length + ' · a dashboard is a saved view of Goals' : dd ? '1 drafted · ' + esc(dd.name) : 'none saved · select rows, then Save'}</span></div><div class="ins-dgrid" role="list">${dd ? draftedCard(dd) : ''}${dashes.map(dashCard).join('')}</div>`;
|
|
2085
|
+
$('#repBody').innerHTML = `${needs}${oneNote()}${libFolds(LIB.L) || '<div id="readyLib"></div>'}<div id="listRows" aria-label="Dashboards and reports">${fbox}${empty}${table}${dashBlock}</div>`;
|
|
2086
|
+
wireFolds();
|
|
2073
2087
|
if (!LIB.read && M.board && M.board.ws) { LIB.read = true; RL.readLib(M.board.ws).then((L) => { LIB.L = L; if (!$('#libSide').hidden) drawLib(); }); }
|
|
2074
2088
|
}
|
|
2089
|
+
const wireFolds = () => { for (const fd of $$('#repBody .ins-libfold')) fd.addEventListener('toggle', () => { LIB.fold[fd.dataset.fold] = fd.open; }); };
|
|
2090
|
+
/** The ready-made switches as two folds (RgR1Panel): the moment reports (up to four peek lines: what each drafts from), then the weekly updates
|
|
2091
|
+
* (one line: how many are on and off). The switches themselves are inside, unchanged (pm/ready-lib.js). Zero is silence: an empty fold is not drawn. */
|
|
2092
|
+
function libFolds(L) {
|
|
2093
|
+
if (!L || !(L.groups || []).length) return '';
|
|
2094
|
+
const onlyKind = (weekly) => Object.assign({}, L, { lead: weekly ? L.lead : null, overlap: weekly ? L.overlap : null,
|
|
2095
|
+
groups: L.groups.map((g) => Object.assign({}, g, { rows: (g.rows || []).filter((r) => (r.kind === 'weekly') === weekly), more: weekly ? g.more : null }))
|
|
2096
|
+
.filter((g) => g.rows.length || (weekly && g.more && g.more.count)) });
|
|
2097
|
+
const all = L.groups.flatMap((g) => (g.rows || []).concat((g.more && g.more.rows) || []).map((r) => Object.assign({ level: g.level, gname: g.name }, r)));
|
|
2098
|
+
const moments = all.filter((r) => r.kind !== 'weekly' && r.on), wk = all.filter((r) => r.kind === 'weekly'), wkOn = wk.filter((r) => r.on).length;
|
|
2099
|
+
const peek = moments.slice(0, 4).map((r) => `<li><b>${esc(r.name)}</b>${r.level === 'initiative' ? '' : ' · ' + esc(r.gname)}${r.sub ? ` <span class="note">${esc(r.sub)}</span>` : ''}</li>`).join('');
|
|
2100
|
+
const mHtml = RL.libHtml(onlyKind(false), { open: LIB.open, yours: false }), wHtml = RL.libHtml(onlyKind(true), { open: LIB.open, yours: false });
|
|
2101
|
+
const foldBox = (k, title, sub, pk, body) => (body ? `<details class="ins-libfold" data-fold="${k}"${LIB.fold[k] ? ' open' : ''}><summary><b>${esc(title)}</b> <span class="note">${esc(sub)}</span></summary>${pk}<div class="ins-libin">${body}</div></details>` : '');
|
|
2102
|
+
return `<div id="readyLib">${foldBox('moment', 'Drafting on their moment', 'nothing to set up', peek ? `<ul class="ins-mlist">${peek}</ul>` : '', mHtml)}`
|
|
2103
|
+
+ `${foldBox('weekly', 'Weekly updates', wk.length ? (wkOn ? wkOn + ' on' : 'none on') + (wk.length - wkOn ? ' · ' + (wk.length - wkOn) + ' off' : '') : '', '', wHtml)}</div>`;
|
|
2104
|
+
}
|
|
2105
|
+
/** A waiting report (RgR1Panel): its name, when it was drafted and for whom, one line of its headline, and its one ✓. */
|
|
2106
|
+
function needRowHtml(G) {
|
|
2107
|
+
const st = repState(G), a = (M.reports.acts || {})[G.x.id], s = st.s, d = st.draft;
|
|
2108
|
+
const about = (M.reports.about || {})[G.x.id];
|
|
2109
|
+
const sub = [d ? 'Drafted ' + md(day(d.slot)) : s && s.paused ? 'Paused' : '', s && s.audience ? 'for ' + s.audience : '', !s && a && a.act === 'confirm' ? 'its definition waits for your word' : '',
|
|
2110
|
+
s && s.next && !s.paused ? 'next ' + s.next : '', about ? 'in ' + (M.board.name || 'this product') + ', about ' + about.name : ''].filter(Boolean).join(' · ');
|
|
2111
|
+
const lead = d && d.lead ? d.lead : s && s.latest && s.latest.lead ? s.latest.lead : '';
|
|
2112
|
+
const key = G.kind + ':' + G.x.id;
|
|
2113
|
+
return `<div class="ins-nrow${selRow === key ? ' sel' : ''}" role="listitem"><button type="button" class="nm" ${G.kind === 'setup' ? `data-setup="${esc(G.x.id)}"` : `data-rpt="${esc(G.x.id)}"`}><b>${esc(G.x.name)}</b><small>${esc(sub)}</small>${lead ? `<span class="hl">${esc(lead)}</span>` : ''}</button>`
|
|
2114
|
+
+ `<span class="ac"><span class="note">${esc(a.label.charAt(0).toLowerCase() + a.label.slice(1))}</span><button type="button" class="cz-ib sm solid" data-nact="${esc(G.x.id)}" aria-label="${esc(a.label + ': ' + G.x.name)}" title="${esc(a.label)}">${SV(I.chk, 2.4)}</button></span></div>`;
|
|
2115
|
+
}
|
|
2116
|
+
/** A proposal from Home that is ready: what was asked, and its ✓, which opens the proposal (Set it up is one press there). */
|
|
2117
|
+
const askNeedHtml = (a) => `<div class="ins-nrow" role="listitem"><button type="button" class="nm" data-ask="${esc(a.id)}"><b>${esc(a.proposal && a.proposal.name ? a.proposal.name : a.text)}</b><small>${esc('You asked in Home: ' + a.text)}</small></button>`
|
|
2118
|
+
+ `<span class="ac"><span class="note">set it up</span><button type="button" class="cz-ib sm solid" data-ask="${esc(a.id)}" aria-label="${esc('Open the proposal: ' + a.text)}" title="Open the proposal">${SV(I.chk, 2.4)}</button></span></div>`;
|
|
2119
|
+
/** The one ✓ of a waiting report: Issue, Confirm the definition, Move to <product> or Resume. One press, then the panel reads again. */
|
|
2120
|
+
async function needPress(b) {
|
|
2121
|
+
const id = b.dataset.nact, a = (M.reports.acts || {})[id]; if (!a) return;
|
|
2122
|
+
const G = reportGroups().find((x) => x.x.id === id), name = G ? G.x.name : id;
|
|
2123
|
+
b.disabled = true;
|
|
2124
|
+
const r = a.act === 'issue' ? (a.sha ? await pmPost('/api/report/issue', { ws: M.board.ws, cadence: id, slot: a.slot, sha: a.sha }) : { ok: false, error: 'The draft could not be read, so it was not issued. Open it and issue it from the reader.' })
|
|
2125
|
+
: a.act === 'confirm' ? await pmPost('/api/report-setups/confirm', { ws: M.board.ws, id })
|
|
2126
|
+
: a.act === 'move' ? await pmPost('/api/report-setups/move', { ws: M.board.ws, id, to: a.to })
|
|
2127
|
+
: await pmPost('/api/report-setups/resume', { ws: M.board.ws, id });
|
|
2128
|
+
if (!r || r.ok !== true) { b.disabled = false; toast((r && r.error) || 'Nothing changed. The server did not say why.'); return; }
|
|
2129
|
+
toast(a.act === 'issue' ? 'Issued ' + name + '. Send it from your own mail.' : a.act === 'confirm' ? 'Confirmed the definition of ' + name + '. Its drafts follow it as it is.'
|
|
2130
|
+
: a.act === 'move' ? 'Moved ' + name + ' to ' + a.toName + '. It drafts there from now on.' : 'Resumed ' + name + '.');
|
|
2131
|
+
await load();
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
/* ── the first dashboard, drafted from the OKR file (RgR3Dash) ──────────────────────────────────────────────────────────────────────
|
|
2135
|
+
* With no saved dashboard, the panel offers one tile per key result of the OKR file (A's model.okrFile): a key result with a reader shows its
|
|
2136
|
+
* reading with the reader and its date; one without shows what it needs and the connector ✓. Nothing is a number without a reading. ✓ Keep saves
|
|
2137
|
+
* it as a dashboard of the read key results (POST /api/premium-dashboards/save); the waiting ones join it once a reader reads them. */
|
|
2138
|
+
function draftedDash() {
|
|
2139
|
+
const F = M.okrFile; if (!F || !F.total) return null;
|
|
2140
|
+
const tiles = F.objectives.flatMap((o) => o.krs.map((k) => {
|
|
2141
|
+
const g = k.goal ? M.goals.find((x) => x.id === k.goal) : null, rd = g && g.reading;
|
|
2142
|
+
if (k.read && g) return { k, read: true, big: g.nowTxt || (rd && rd.txt) || '—', dot: rd && rd.dot === 'behind' ? 'risk' : rd && rd.dot === 'broken' ? 'late' : 'ok', meta: [rd && rd.source ? rd.source : 'its reader', rd && rd.at ? md(day(rd.at)) : ''].filter(Boolean).join(' · ') };
|
|
2143
|
+
if (k.decision) return { k, read: false, big: 'Not yet', dot: 'none', meta: k.due ? 'a decision, due ' + md(day(k.due)) : 'a decision' };
|
|
2144
|
+
return { k, read: false, big: '—', dot: 'none', meta: k.need ? 'needs ' + k.need : 'no source named', need: k.need || '' };
|
|
2145
|
+
}));
|
|
2146
|
+
return { name: (F.period ? F.period.label + ' ' : '') + 'key results', tiles, charts: tiles.filter((t) => t.read).map((t) => t.k.goal), nRead: tiles.filter((t) => t.read).length };
|
|
2147
|
+
}
|
|
2148
|
+
const okrTileHtml = (t) => `<div class="ins-dtile${t.read ? '' : ' w'}" role="listitem"><span class="lb"><span class="kr">${esc(t.k.token)}</span>${esc(t.k.title)}</span><b class="big">${esc(t.big)}</b>`
|
|
2149
|
+
+ `<span class="ft"><span class="note"><span class="ins-tdot ${t.dot}" aria-hidden="true"></span>${esc(t.meta)}</span>${t.need ? `<button type="button" class="cz-ib sm" data-ddash="con" aria-label="${esc('Connect ' + t.need)}" title="${esc('Connect ' + t.need)}">${SV(I.chk, 2.4)}</button>` : ''}</span></div>`;
|
|
2150
|
+
function draftedCard(D) {
|
|
2151
|
+
return `<div class="ins-dcard ins-ddraft" role="listitem"><span class="ins-dh"><b>${esc(D.name)}</b><span class="cz-pill q">Drafted</span></span>`
|
|
2152
|
+
+ `<p class="note">${esc('I drafted it from your OKR file: one tile per key result. ' + (D.nRead ? D.nRead + ' read' + (D.tiles.length - D.nRead ? ', ' + (D.tiles.length - D.nRead) + ' wait on a source, and each tile says which.' : '.') : 'None has a reading yet, and each tile says what it needs.'))}</p>`
|
|
2153
|
+
+ `<div class="ins-dtiles" role="list">${D.tiles.slice(0, 8).map(okrTileHtml).join('')}</div>`
|
|
2154
|
+
+ `<span class="ins-dact">${D.nRead ? `<span class="note">keep it, and it opens first</span><button type="button" class="cz-ib sm solid" data-ddash="keep" aria-label="Keep this dashboard" title="Keep this dashboard">${SV(I.chk, 2.4)}</button>` : '<span class="note">kept once one key result has a reading</span>'}</span></div>`;
|
|
2155
|
+
}
|
|
2156
|
+
async function draftedPress(b) {
|
|
2157
|
+
if (b.dataset.ddash === 'con') { import('./cozy-settings.js').then((S) => S.openSettings('connectors')).catch(() => toast('Open Settings › Connectors.')); return; }
|
|
2158
|
+
const D = draftedDash(); if (!D || !D.charts.length) return;
|
|
2159
|
+
b.disabled = true;
|
|
2160
|
+
const r = await saveDash({ name: D.name, purpose: 'Drafted from the OKR file: one tile per key result with a reading.', charts: D.charts, main: true });
|
|
2161
|
+
if (!r || r.ok !== true) { b.disabled = false; toast((r && r.error) || 'The dashboard could not be saved. Nothing changed.'); return; }
|
|
2162
|
+
toast('Kept ' + D.name + '. It opens first.'); await load();
|
|
2163
|
+
}
|
|
2075
2164
|
/* the ready-made group before Yours (batch 17 item 1, PbA6): read once per open of the panel, again after a switch */
|
|
2076
|
-
const LIB = { L: null, read: false, open: new Set(), fold:
|
|
2077
|
-
const drawLib = () => { const el = $('#readyLib'); if (el) el.
|
|
2165
|
+
const LIB = { L: null, read: false, open: new Set(), fold: {} };
|
|
2166
|
+
const drawLib = () => { const el = $('#readyLib'); if (el) { el.outerHTML = libFolds(LIB.L) || '<div id="readyLib"></div>'; wireFolds(); } };
|
|
2167
|
+
/* + in the panel header (RgR4Make): the drafted report of this view, never a blank chat. With all products on screen there is no one product to
|
|
2168
|
+
* draft for, so it keeps its link to Home. */
|
|
2169
|
+
$('#newRep').addEventListener('click', (e) => {
|
|
2170
|
+
if (PF || !M || !M.board || !M.board.ws) return;
|
|
2171
|
+
e.preventDefault();
|
|
2172
|
+
const D = curDash(); RI.openDraft(D && D.saved ? 'dashboard:' + D.id : 'view');
|
|
2173
|
+
});
|
|
2078
2174
|
RL.wireLib($('#repBody'), { get ws() { return M && M.board ? M.board.ws : ''; }, open: LIB.open, toast: (t) => toast(t), redraw: drawLib,
|
|
2079
2175
|
reload: async () => { LIB.L = await RL.readLib(M.board.ws); drawLib(); } });
|
|
2080
2176
|
$('#repBody').addEventListener('click', async (e) => {
|
|
2177
|
+
const na = e.target.closest('[data-nact]'); if (na) { if (!na.disabled) needPress(na); return; }
|
|
2178
|
+
const dk = e.target.closest('[data-ddash]'); if (dk) { draftedPress(dk); return; }
|
|
2081
2179
|
const d = e.target.closest('[data-d]'); if (d) { setView(d.dataset.d); return; }
|
|
2082
2180
|
const fx = e.target.closest('[data-fx]');
|
|
2083
2181
|
if (fx) {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
|
|
69
69
|
<!-- Reports and dashboards (⋯; the old Library): Found in your work, requests from Home, the reports, then the dashboards -->
|
|
70
70
|
<aside class="ins-side ins-lib" id="libSide" role="dialog" aria-modal="false" aria-labelledby="libTitle" tabindex="-1" hidden>
|
|
71
|
-
<div class="ins-sh"><div><h2 id="libTitle">Reports and dashboards</h2></div><span class="ins-st"><a class="cz-ib" id="newRep" href="/premium.html?ask=insights" aria-label="New report
|
|
71
|
+
<div class="ins-sh"><div><h2 id="libTitle">Reports and dashboards</h2></div><span class="ins-st"><a class="cz-ib" id="newRep" href="/premium.html?ask=insights" aria-label="New report: Claude drafts it from this view" title="New report: Claude drafts it from this view"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></a></span><button type="button" class="cz-ib" id="libX" aria-label="Close" title="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
|
|
72
72
|
<div class="ins-sb" id="repBody"></div>
|
|
73
73
|
<span class="vh" id="repCap"></span>
|
|
74
74
|
</aside>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Reports and dashboards redesign R: each waiting report has one ✓ (Issue, Confirm the definition, Move to a product, Resume), a definition report is named by its title, ✦ Move takes a report to the product it is about, and the product page's ✦ line is Intelligence's (lib/report-acts.js).",
|
|
3
|
+
"modules": ["report-acts.test.js"],
|
|
4
|
+
"seconds": { "report-acts.test.js": 1 }
|
|
5
|
+
}
|