create-pmos 0.74.0 → 0.76.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/scripts/workflow-state.js +45 -4
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/initiatives-screen.md +10 -0
- package/workspace/guide/notes.md +15 -10
- package/workspace/lib/engine/workflow-state.js +45 -4
- package/workspace/lib/jev-documents.js +224 -0
- package/workspace/lib/notes-whisper.js +94 -22
- package/workspace/lib/notes.js +559 -59
- package/workspace/lib/routes-initiatives.js +76 -3
- package/workspace/lib/routes-notes.js +276 -46
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +142 -0
- package/workspace/public/pm/initiatives-api.js +8 -2
- package/workspace/public/pm/initiatives-copy.js +19 -0
- package/workspace/public/pm/initiatives-ui.js +88 -21
- package/workspace/public/pm/note-dock.js +12 -6
- package/workspace/public/pm/view-notes.js +245 -102
- package/workspace/server.js +12 -1
|
@@ -6326,6 +6326,148 @@
|
|
|
6326
6326
|
padding: 12px;
|
|
6327
6327
|
}
|
|
6328
6328
|
|
|
6329
|
+
/* meeting-notes Run 7: the note reads top to bottom (prototype/meeting-notes/simple-note-mockup.html). A bar with the state and
|
|
6330
|
+
one action, the title, the day, the meeting and the initiative on one line, the people, then the sections, each item led by
|
|
6331
|
+
its meeting time. The transcript folds away. The same column in the pop-up and on the full screen. */
|
|
6332
|
+
.nt-wrap:not(.nt-wrap--inline) {
|
|
6333
|
+
max-width: 760px;
|
|
6334
|
+
}
|
|
6335
|
+
.nt-pop__body {
|
|
6336
|
+
padding: 0 16px 16px;
|
|
6337
|
+
}
|
|
6338
|
+
.nt-header {
|
|
6339
|
+
flex-direction: column;
|
|
6340
|
+
align-items: stretch;
|
|
6341
|
+
flex-wrap: nowrap;
|
|
6342
|
+
gap: 8px;
|
|
6343
|
+
}
|
|
6344
|
+
.nt-bar {
|
|
6345
|
+
display: flex;
|
|
6346
|
+
align-items: center;
|
|
6347
|
+
justify-content: space-between;
|
|
6348
|
+
gap: 8px;
|
|
6349
|
+
min-height: 40px;
|
|
6350
|
+
border-bottom: 1px solid var(--border);
|
|
6351
|
+
}
|
|
6352
|
+
.nt-wrap--inline .nt-bar {
|
|
6353
|
+
position: sticky;
|
|
6354
|
+
top: 0;
|
|
6355
|
+
z-index: 1;
|
|
6356
|
+
background: var(--card);
|
|
6357
|
+
}
|
|
6358
|
+
.nt-bar .nt-live {
|
|
6359
|
+
flex: 1 1 auto;
|
|
6360
|
+
min-width: 0;
|
|
6361
|
+
overflow: hidden;
|
|
6362
|
+
white-space: nowrap;
|
|
6363
|
+
text-overflow: ellipsis;
|
|
6364
|
+
}
|
|
6365
|
+
.nt-bar .nt-acts {
|
|
6366
|
+
flex: none;
|
|
6367
|
+
flex-wrap: nowrap;
|
|
6368
|
+
gap: 4px;
|
|
6369
|
+
}
|
|
6370
|
+
.nt-live b {
|
|
6371
|
+
font-family: var(--sans);
|
|
6372
|
+
font-weight: 500;
|
|
6373
|
+
}
|
|
6374
|
+
.nt-when .nt-link + .nt-link::before,
|
|
6375
|
+
.nt-when > span:first-child::after {
|
|
6376
|
+
content: "·";
|
|
6377
|
+
margin-right: 8px;
|
|
6378
|
+
color: var(--muted-foreground);
|
|
6379
|
+
}
|
|
6380
|
+
.nt-when > span:first-child::after {
|
|
6381
|
+
margin: 0 0 0 8px;
|
|
6382
|
+
}
|
|
6383
|
+
.nt-headl {
|
|
6384
|
+
flex: none;
|
|
6385
|
+
flex-direction: column;
|
|
6386
|
+
align-items: stretch;
|
|
6387
|
+
gap: 4px;
|
|
6388
|
+
}
|
|
6389
|
+
.nt-when {
|
|
6390
|
+
display: flex;
|
|
6391
|
+
flex-wrap: wrap;
|
|
6392
|
+
align-items: center;
|
|
6393
|
+
gap: 4px 8px;
|
|
6394
|
+
}
|
|
6395
|
+
.nt-when .nt-link {
|
|
6396
|
+
display: inline-flex;
|
|
6397
|
+
align-items: center;
|
|
6398
|
+
color: var(--foreground);
|
|
6399
|
+
}
|
|
6400
|
+
.nt-people {
|
|
6401
|
+
display: flex;
|
|
6402
|
+
align-items: center;
|
|
6403
|
+
gap: 8px;
|
|
6404
|
+
font-size: 12px;
|
|
6405
|
+
color: var(--muted-foreground);
|
|
6406
|
+
}
|
|
6407
|
+
.nt-avs {
|
|
6408
|
+
display: inline-flex;
|
|
6409
|
+
gap: 2px;
|
|
6410
|
+
}
|
|
6411
|
+
.nt-av {
|
|
6412
|
+
width: 24px;
|
|
6413
|
+
height: 24px;
|
|
6414
|
+
display: inline-grid;
|
|
6415
|
+
place-items: center;
|
|
6416
|
+
font-size: 11px;
|
|
6417
|
+
font-weight: 500;
|
|
6418
|
+
color: var(--foreground);
|
|
6419
|
+
background: var(--muted);
|
|
6420
|
+
}
|
|
6421
|
+
.nt-body2 {
|
|
6422
|
+
display: flex;
|
|
6423
|
+
flex-direction: column;
|
|
6424
|
+
gap: 16px;
|
|
6425
|
+
}
|
|
6426
|
+
.nt-notecol,
|
|
6427
|
+
.nt-wrap--inline .nt-notecol {
|
|
6428
|
+
padding: 0;
|
|
6429
|
+
}
|
|
6430
|
+
.nt-trans,
|
|
6431
|
+
.nt-wrap--inline .nt-trans {
|
|
6432
|
+
padding: 12px 0 0;
|
|
6433
|
+
border-left: 0;
|
|
6434
|
+
border-top: 1px solid var(--border);
|
|
6435
|
+
}
|
|
6436
|
+
.nt-fold {
|
|
6437
|
+
cursor: pointer;
|
|
6438
|
+
}
|
|
6439
|
+
.nt-src {
|
|
6440
|
+
font-weight: 400;
|
|
6441
|
+
letter-spacing: normal;
|
|
6442
|
+
text-transform: none;
|
|
6443
|
+
}
|
|
6444
|
+
.nt-item {
|
|
6445
|
+
display: grid;
|
|
6446
|
+
grid-template-columns: 44px minmax(0, 1fr) auto;
|
|
6447
|
+
align-items: baseline;
|
|
6448
|
+
border-bottom: 0;
|
|
6449
|
+
padding: 2px 0;
|
|
6450
|
+
}
|
|
6451
|
+
.nt-item--plain {
|
|
6452
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
6453
|
+
}
|
|
6454
|
+
.nt-addrow {
|
|
6455
|
+
display: grid;
|
|
6456
|
+
grid-template-columns: 44px minmax(0, 1fr);
|
|
6457
|
+
align-items: center;
|
|
6458
|
+
gap: 8px;
|
|
6459
|
+
}
|
|
6460
|
+
.nt-addrow .nt-at {
|
|
6461
|
+
font-variant-numeric: tabular-nums;
|
|
6462
|
+
}
|
|
6463
|
+
.nt-ln__meta .nt-at {
|
|
6464
|
+
font-variant-numeric: tabular-nums;
|
|
6465
|
+
}
|
|
6466
|
+
.nt-spk--typed {
|
|
6467
|
+
color: var(--foreground);
|
|
6468
|
+
font-weight: 500;
|
|
6469
|
+
}
|
|
6470
|
+
|
|
6329
6471
|
/* -- layouts · .lg- the legacy and fixture shells (Run 3, slice 3D) -- */
|
|
6330
6472
|
/* The legacy and fixture cockpit's two columns: the list head (a stack) and the rail's tile column. */
|
|
6331
6473
|
.lg-stack {
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
* POST /api/initiatives { ws, title, lane, anchor, intent?, from? } the New-initiative form and a triage card (one body, no events)
|
|
13
13
|
* POST /api/initiatives/rescan { ws } the Re-scan press: read the repository again now (nothing is written)
|
|
14
14
|
* POST /api/initiatives/duplicates { ws } documents that share an initiative and a role (S10; nothing is written)
|
|
15
|
-
* POST /api/initiatives/tidy { ws, keep }
|
|
15
|
+
* POST /api/initiatives/tidy { ws, keep, kinds? } record the kinds the PM confirmed (S11), then move every other
|
|
16
|
+
* document of each kept group to the archive (S10)
|
|
17
|
+
* POST /api/initiatives/jev-check { ws, confirm?, … } Jev's plan (nothing sent), or the PM-confirmed check: each
|
|
18
|
+
* document's kind and the duplicate to keep (S11)
|
|
16
19
|
*
|
|
17
20
|
* Every GET of /api/initiatives answers 202 { ok: false, pending: true } until the repository's first derivation lands (2C,
|
|
18
21
|
* about 260 ms a record); a read waits and asks again, so a caller sees the answer or a real error, never the 202.
|
|
@@ -75,7 +78,10 @@ export async function rescan(ws, opts) {
|
|
|
75
78
|
|
|
76
79
|
/** The duplicates a tidy offers (S10), and the tidy itself: `keep` is [{ id, role, keep }] for each group to tidy. */
|
|
77
80
|
export const duplicates = (ws) => pmPost('/api/initiatives/duplicates', { ws });
|
|
78
|
-
export const tidy = (ws, keep) => pmPost('/api/initiatives/tidy', { ws, keep });
|
|
81
|
+
export const tidy = (ws, keep, kinds) => pmPost('/api/initiatives/tidy', kinds && kinds.length ? { ws, keep, kinds } : { ws, keep });
|
|
82
|
+
/** Jev's check (S11). With no `plan` it answers the gates and the plan and sends nothing; with the plan the PM confirmed it asks Jev. */
|
|
83
|
+
export const jevCheck = (ws, plan) => pmPost('/api/initiatives/jev-check', plan
|
|
84
|
+
? { ws, confirm: true, expect_requests: plan.requests, expect_fingerprint: plan.fingerprint } : { ws });
|
|
79
85
|
|
|
80
86
|
export const createInitiative = (ws, fields) => pmPost('/api/initiatives', { ws, ...fields });
|
|
81
87
|
|
|
@@ -22,6 +22,8 @@ export const GLOSSARY = {
|
|
|
22
22
|
guide: 'initiatives-screen.md',
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
const KIND = { brief: 'problem brief', draft: 'PRD', review: 'evaluation', handoff: 'handoff', none: 'none of these' };
|
|
26
|
+
|
|
25
27
|
export const COPY = {
|
|
26
28
|
title: 'Initiatives',
|
|
27
29
|
views: 'Initiatives view',
|
|
@@ -56,6 +58,23 @@ export const COPY = {
|
|
|
56
58
|
tidyNone: 'Nothing to archive: every group is off.',
|
|
57
59
|
tidyArchived: (n) => `Archived ${n} duplicate ${n === 1 ? 'document' : 'documents'}.`,
|
|
58
60
|
tidyFailed: (why) => `Could not tidy: ${why}`,
|
|
61
|
+
// Jev checks each document's kind and picks the duplicate to keep, before the tidy (scan-documents S11)
|
|
62
|
+
jevTitle: 'Check documents with Jev',
|
|
63
|
+
jevPlan: (p) => `Jev reads ${p.documents} ${p.documents === 1 ? 'document' : 'documents'} tied to initiatives and says what kind each one is: problem brief, PRD, evaluation, handoff or none of these. Then it picks the version to keep where an initiative has more than one of a kind. This sends ${p.requests} ${p.requests === 1 ? 'request' : 'requests'} to ${p.host}${p.cached ? `; ${p.cached} ${p.cached === 1 ? 'document was' : 'documents were'} checked before and ${p.cached === 1 ? 'is' : 'are'} not sent again` : ''}. Only each document's title, headings and opening are sent, never its path or the whole file.`,
|
|
64
|
+
jevWithheld: (n) => `${n} ${n === 1 ? 'document is' : 'documents are'} not sent: ${n === 1 ? 'it is' : 'they are'} in an excluded folder or may contain a secret.`,
|
|
65
|
+
jevGo: (n) => (n ? `Check with Jev (${n} ${n === 1 ? 'request' : 'requests'})` : 'Check with Jev'),
|
|
66
|
+
jevSkip: 'Scan without Jev',
|
|
67
|
+
jevReading: 'Jev is reading the documents…',
|
|
68
|
+
jevFailed: (why) => `Jev could not check the documents: ${why}`,
|
|
69
|
+
jevShut: (why) => `Jev did not check the documents: ${why}`,
|
|
70
|
+
jevPaused: (why) => `${why} The scan uses the answers it has.`,
|
|
71
|
+
jevClean: (n) => `Jev checked ${n} ${n === 1 ? 'document' : 'documents'}: each kind is right and there are no duplicates.`,
|
|
72
|
+
jevKindsHead: (n) => `Jev reads ${n} ${n === 1 ? 'document' : 'documents'} as a different kind`,
|
|
73
|
+
jevKindsBody: 'A changed kind is written to the kinds file in the repository. The scan uses it from now on.',
|
|
74
|
+
jevKindLine: (c) => `${c.title} · ${c.path}: ${KIND[c.was] || c.was} → ${KIND[c.kind] || c.kind}`,
|
|
75
|
+
jevKindToggle: 'Use this kind',
|
|
76
|
+
jevConfirm: (k, n) => [k ? `Change ${k} ${k === 1 ? 'kind' : 'kinds'}` : '', n ? `archive ${n}` : ''].filter(Boolean).join(', ') + ' and scan',
|
|
77
|
+
jevRecorded: (n) => `Changed the kind of ${n} ${n === 1 ? 'document' : 'documents'}.`,
|
|
59
78
|
rescanDone: (t) => `Read again at ${t}`,
|
|
60
79
|
rescanFailed: (why) => `Could not re-scan: ${why}`,
|
|
61
80
|
rescanSlow: 'The re-scan is still running. The view updates when it ends.',
|
|
@@ -7,15 +7,18 @@
|
|
|
7
7
|
* checkBySheet({…}) a sheet with a date and Save or Clear
|
|
8
8
|
* say(out, ok) a toast for the answer of a write: the success text, or the server's refusal
|
|
9
9
|
* rescanRepo(ws) read the repository again now, with a toast at the start and at the end (scan-consistency R6);
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* in a document repository, Jev's check first when its gates are open (scan-documents S11),
|
|
11
|
+
* then the tidy sheet when there are kinds to change or duplicates (S10)
|
|
12
|
+
* jevPlanSheet(plan) what Jev would read and how many requests it sends, and Cancel, Scan without Jev or Check
|
|
13
|
+
* tidySheet(duplicates) the kinds Jev would change, the groups of duplicate documents, the one to keep in each,
|
|
14
|
+
* and Cancel, Skip or Archive
|
|
12
15
|
* No native dialog is used anywhere here. Every control is a primitive (primitives.js).
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import { el } from './dom.js';
|
|
16
19
|
import { button, field, signal, sheet, tabs, toast } from './primitives.js';
|
|
17
20
|
import { COPY } from './initiatives-copy.js';
|
|
18
|
-
import { rescan, duplicates, tidy } from './initiatives-api.js';
|
|
21
|
+
import { rescan, duplicates, tidy, jevCheck } from './initiatives-api.js';
|
|
19
22
|
|
|
20
23
|
export const horizonLabel = (h) => COPY.horizons[h] || h;
|
|
21
24
|
export const progressText = (row) => (row && row.progress && row.progress.label) || '';
|
|
@@ -52,22 +55,25 @@ export function scanMoves(before, after) {
|
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
57
|
* The tidy sheet (scan-documents S10): each group of documents that share an initiative and a role, with the one to keep
|
|
55
|
-
* chosen, a switch to leave the group as it is, and three answers. Resolves { keep: [{ id, role, keep }] } on the
|
|
56
|
-
* 'skip' on Scan without tidying, and null on Cancel, Escape or the backdrop.
|
|
58
|
+
* chosen, a switch to leave the group as it is, and three answers. Resolves { keep: [{ id, role, keep }], kinds } on the
|
|
59
|
+
* confirm, 'skip' on Scan without tidying, and null on Cancel, Escape or the backdrop. After Jev's check (S11) it also lists
|
|
60
|
+
* the kinds Jev would change, each with a switch, and each group says which version Jev picks; `kinds` is the switched-on ones.
|
|
57
61
|
*/
|
|
58
62
|
export function tidySheet(d) {
|
|
59
63
|
return new Promise((resolve) => {
|
|
60
64
|
let settled = false;
|
|
61
65
|
const finish = (v) => { if (settled) return; settled = true; resolve(v); };
|
|
62
66
|
const state = d.groups.map((g) => ({ g, on: true, keep: g.keep }));
|
|
67
|
+
const fixes = (d.corrections || []).map((c) => ({ c, on: true }));
|
|
63
68
|
const count = () => state.reduce((n, x) => n + (x.on ? x.g.files.length - 1 : 0), 0);
|
|
69
|
+
const label = () => (fixes.length ? COPY.jevConfirm(fixes.filter((x) => x.on).length, count()) : COPY.tidyConfirm(count()));
|
|
64
70
|
const day = (t) => (t ? new Date(t * 1000).toISOString().slice(0, 10) : '');
|
|
65
71
|
const err = el('p', { class: 'in-error', role: 'alert', 'data-in-error': '' });
|
|
66
72
|
err.hidden = true;
|
|
67
73
|
const groups = state.map((x, gi) => {
|
|
68
74
|
const box = el('fieldset', { class: 'in-tidy__group', 'data-in-tidy-group': x.g.id + ':' + x.g.role });
|
|
69
75
|
const toggle = el('input', { type: 'checkbox', checked: true, 'aria-label': COPY.tidyGroupToggle, 'data-in-tidy-on': '' });
|
|
70
|
-
toggle.addEventListener('change', () => { x.on = toggle.checked; if (x.on) box.removeAttribute('data-off'); else box.setAttribute('data-off', ''); go.textContent =
|
|
76
|
+
toggle.addEventListener('change', () => { x.on = toggle.checked; if (x.on) box.removeAttribute('data-off'); else box.setAttribute('data-off', ''); go.textContent = label(); });
|
|
71
77
|
const head = el('legend', { class: 'in-tidy__head' }, toggle, `${x.g.title} · ${COPY.tidyRole[x.g.role] || x.g.role}`);
|
|
72
78
|
const files = x.g.files.map((f) => {
|
|
73
79
|
const radio = el('input', { type: 'radio', name: 'in-tidy-' + gi, value: f.path, checked: f.path === x.keep, 'data-in-tidy-keep': f.path });
|
|
@@ -76,21 +82,34 @@ export function tidySheet(d) {
|
|
|
76
82
|
return el('label', { class: 'in-tidy__file' }, radio, el('span', null, el('span', { class: 'in-tidy__path' }, f.path), meta ? el('span', { class: 'in-note' }, ' · ' + meta) : null));
|
|
77
83
|
});
|
|
78
84
|
const shared = (x.g.shared || []).map((p) => el('p', { class: 'in-note in-tidy__file' }, COPY.tidyShared(p)));
|
|
79
|
-
|
|
85
|
+
const jev = x.g.jev && x.g.jev.words ? [el('p', { class: 'in-note in-tidy__file', 'data-in-tidy-jev': '' }, x.g.jev.words)] : [];
|
|
86
|
+
box.replaceChildren(head, ...jev, ...files, ...shared);
|
|
80
87
|
return box;
|
|
81
88
|
});
|
|
89
|
+
const kindsBox = fixes.length ? (() => {
|
|
90
|
+
const box = el('fieldset', { class: 'in-tidy__group', 'data-in-tidy-kinds': '' });
|
|
91
|
+
const rows = fixes.map((x) => {
|
|
92
|
+
const t = el('input', { type: 'checkbox', checked: true, 'aria-label': COPY.jevKindToggle, 'data-in-tidy-kind': x.c.path });
|
|
93
|
+
t.addEventListener('change', () => { x.on = t.checked; go.textContent = label(); });
|
|
94
|
+
return el('label', { class: 'in-tidy__file' }, t, el('span', null, el('span', { class: 'in-tidy__path' }, COPY.jevKindLine(x.c)), x.c.words ? el('span', { class: 'in-note' }, ' · ' + x.c.words) : null));
|
|
95
|
+
});
|
|
96
|
+
box.replaceChildren(el('legend', { class: 'in-tidy__head' }, COPY.jevKindsHead(fixes.length)), el('p', { class: 'in-note in-tidy__file' }, COPY.jevKindsBody), ...rows);
|
|
97
|
+
return box;
|
|
98
|
+
})() : null;
|
|
82
99
|
const inits = new Set(d.groups.map((g) => g.id)).size;
|
|
83
100
|
const go = button({
|
|
84
|
-
label:
|
|
101
|
+
label: label(), variant: 'primary', attrs: { 'data-in-tidy-confirm': '' },
|
|
85
102
|
onClick: () => {
|
|
86
103
|
const keep = state.filter((x) => x.on).map((x) => ({ id: x.g.id, role: x.g.role, keep: x.keep }));
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
const kinds = fixes.filter((x) => x.on).map((x) => ({ path: x.c.path, kind: x.c.kind, was: x.c.was, p: x.c.p }));
|
|
105
|
+
if (!keep.length && !kinds.length) { err.textContent = COPY.tidyNone; err.hidden = false; return; }
|
|
106
|
+
finish(kinds.length ? { keep, kinds } : { keep }); s.close();
|
|
89
107
|
},
|
|
90
108
|
});
|
|
91
109
|
const s = sheet({
|
|
92
|
-
mode: 'auto', title: COPY.tidyTitle,
|
|
93
|
-
body: [el('p', { class: 'in-note' }, COPY.tidyBody(d.groups.length, inits, d.archive))
|
|
110
|
+
mode: 'auto', title: fixes.length ? COPY.jevTitle : COPY.tidyTitle,
|
|
111
|
+
body: [d.groups.length ? el('p', { class: 'in-note' }, COPY.tidyBody(d.groups.length, inits, d.archive)) : null,
|
|
112
|
+
el('div', { class: 'in-tidy' }, ...(kindsBox ? [kindsBox] : []), ...groups), err].filter(Boolean),
|
|
94
113
|
attrs: { 'data-in-sheet': 'tidy' },
|
|
95
114
|
onClose: () => finish(null),
|
|
96
115
|
actions: [
|
|
@@ -102,25 +121,73 @@ export function tidySheet(d) {
|
|
|
102
121
|
});
|
|
103
122
|
}
|
|
104
123
|
|
|
105
|
-
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
124
|
+
/**
|
|
125
|
+
* Jev's plan (scan-documents S11): how many documents Jev would read and how many requests that sends, before anything
|
|
126
|
+
* is sent. Resolves 'jev' on the check, 'skip' on Scan without Jev, and null on Cancel, Escape or the backdrop.
|
|
127
|
+
*/
|
|
128
|
+
export function jevPlanSheet(plan) {
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
let settled = false;
|
|
131
|
+
const finish = (v) => { if (settled) return; settled = true; resolve(v); };
|
|
132
|
+
const s = sheet({
|
|
133
|
+
mode: 'auto', title: COPY.jevTitle,
|
|
134
|
+
body: [el('p', { class: 'in-note' }, COPY.jevPlan(plan)), plan.withheld_count || plan.excluded ? el('p', { class: 'in-note' }, COPY.jevWithheld((plan.withheld_count || 0) + (plan.excluded || 0))) : null].filter(Boolean),
|
|
135
|
+
attrs: { 'data-in-sheet': 'jev-plan' },
|
|
136
|
+
onClose: () => finish(null),
|
|
137
|
+
actions: [
|
|
138
|
+
button({ label: COPY.cancel, variant: 'outline', onClick: () => s.close(), attrs: { 'data-in-cancel': '' } }),
|
|
139
|
+
button({ label: COPY.jevSkip, variant: 'outline', onClick: () => { finish('skip'); s.close(); }, attrs: { 'data-in-jev-skip': '' } }),
|
|
140
|
+
button({ label: COPY.jevGo(plan.requests), variant: 'primary', onClick: () => { finish('jev'); s.close(); }, attrs: { 'data-in-jev-go': '' } }),
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Jev's part of a scan (S11): the plan, the PM's confirm, then the check. Answers { cancelled } on Cancel, { review } with
|
|
147
|
+
* the kinds to change and the groups when Jev answered, or { note } with what to say when Jev did not check. */
|
|
148
|
+
async function jevStep(ws) {
|
|
149
|
+
const j = await jevCheck(ws);
|
|
150
|
+
if (!j || !j.ok || j.profile !== 'pm-artifacts') return {};
|
|
151
|
+
if (!j.gates || !j.gates.open) return { note: COPY.jevShut(j.gates && j.gates.sentence ? j.gates.sentence : 'it is switched off.') };
|
|
152
|
+
if (!j.plan || !j.plan.documents) return {};
|
|
153
|
+
const go = await jevPlanSheet(j.plan);
|
|
154
|
+
if (!go) return { cancelled: true };
|
|
155
|
+
if (go === 'skip') return {};
|
|
156
|
+
toast({ text: COPY.jevReading, timeout: 6000 });
|
|
157
|
+
const r = await jevCheck(ws, j.plan);
|
|
158
|
+
if (!r || !r.ok) return { failed: COPY.jevFailed((r && r.error) || 'no answer') };
|
|
159
|
+
const pre = r.paused ? COPY.jevPaused(r.paused) + ' ' : '';
|
|
160
|
+
if (!(r.corrections || []).length && !(r.groups || []).length) return { note: pre + COPY.jevClean(r.checked || 0) };
|
|
161
|
+
return { review: { corrections: r.corrections || [], groups: r.groups || [], archive: r.archive }, note: pre || null };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Re-scan: read the repository again now. In a document repository, Jev's check comes first when its gates are open
|
|
165
|
+
* (S11), then the tidy sheet: the kinds Jev would change and the duplicates by Jev's kinds and picks. With Jev's gates
|
|
166
|
+
* shut, or on Scan without Jev, the tidy sheet lists the duplicates the file names show (S10). A toast says the scan
|
|
167
|
+
* started and another says how it ended. Answers the rescan answer; the caller reloads what it shows when `ok`. */
|
|
108
168
|
export async function rescanRepo(ws) {
|
|
109
169
|
let first = null;
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
170
|
+
const jv = await jevStep(ws);
|
|
171
|
+
if (jv.cancelled) return { ok: false, cancelled: true };
|
|
172
|
+
if (jv.failed) toast({ text: jv.failed, tone: 'error', timeout: 8000 });
|
|
173
|
+
let d = jv.review || null;
|
|
174
|
+
if (!d) { const dup = await duplicates(ws); if (dup && dup.ok && Array.isArray(dup.groups) && dup.groups.length) d = dup; }
|
|
175
|
+
if (d) {
|
|
112
176
|
const pick = await tidySheet(d);
|
|
113
177
|
if (!pick) return { ok: false, cancelled: true };
|
|
114
|
-
if (pick !== 'skip') first = () => tidy(ws, pick.keep);
|
|
178
|
+
if (pick !== 'skip') first = () => tidy(ws, pick.keep, pick.kinds);
|
|
115
179
|
}
|
|
116
180
|
toast({ text: COPY.rescanning, timeout: 4000 });
|
|
117
181
|
const a = await rescan(ws, first ? { first } : null);
|
|
118
182
|
const t = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
119
183
|
const moved = a && a.ok && a.after ? scanMoves(a.before, a.after) : null;
|
|
120
|
-
const
|
|
184
|
+
const td = a && a.tidied;
|
|
185
|
+
const archived = (td && td.kinds_written ? COPY.jevRecorded(td.kinds_written) + ' ' : '')
|
|
186
|
+
+ (td && Array.isArray(td.moved) && td.moved.length ? COPY.tidyArchived(td.moved.length) + ' ' : '');
|
|
187
|
+
const note = jv.note ? ' ' + jv.note : '';
|
|
121
188
|
if (!a || !a.ok) toast({ text: first && a && !a.after && !a.slow ? COPY.tidyFailed((a && a.error) || 'no answer') : COPY.rescanFailed((a && a.error) || 'no answer'), tone: 'error', timeout: 8000 });
|
|
122
|
-
else if (a.slow) toast({ text: archived + COPY.rescanSlow, timeout: 6000 });
|
|
123
|
-
else toast({ text: archived + (moved ? COPY.rescanResult(moved.checked, moved.moves, (a.after && a.after.scan) || null) : COPY.rescanDone(t)), timeout: 10000 });
|
|
189
|
+
else if (a.slow) toast({ text: archived + COPY.rescanSlow + note, timeout: 6000 });
|
|
190
|
+
else toast({ text: archived + (moved ? COPY.rescanResult(moved.checked, moved.moves, (a.after && a.after.scan) || null) : COPY.rescanDone(t)) + note, timeout: 10000 });
|
|
124
191
|
try { if (a && a.ok) window.dispatchEvent(new CustomEvent('pmos:initiatives-changed')); } catch { /* no window */ }
|
|
125
192
|
return a || { ok: false };
|
|
126
193
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* mountDock() → draws the dock on every render (pm/app.js boot)
|
|
12
12
|
* takeNotes(e, opts, btn) → makes a note linked to the event, starts listening, and keeps it minimized
|
|
13
13
|
* openDock(id) → shows a note in the pop-up
|
|
14
|
-
* minimize() · activeId()
|
|
14
|
+
* minimize() · activeId() · forget(id)
|
|
15
15
|
*/
|
|
16
16
|
import { el } from './dom.js';
|
|
17
17
|
import { button, iconButton, layer, toast, ensureComponentsCss } from './primitives.js';
|
|
@@ -74,6 +74,13 @@ export async function openDock(id) {
|
|
|
74
74
|
setState({});
|
|
75
75
|
requestAnimationFrame(() => { const t = host && host.querySelector('[data-nt-body]'); if (t) t.focus(); });
|
|
76
76
|
}
|
|
77
|
+
/** A note was deleted: when it is the one in the dock, the dock lets it go. */
|
|
78
|
+
export function forget(id) {
|
|
79
|
+
if (!active || active.id !== id) return;
|
|
80
|
+
active = null; open = false; write();
|
|
81
|
+
if (V) V.setPopped(null);
|
|
82
|
+
setState({});
|
|
83
|
+
}
|
|
77
84
|
function close() {
|
|
78
85
|
if (liveId() === (active && active.id)) return;
|
|
79
86
|
active = null; open = false; write();
|
|
@@ -127,11 +134,10 @@ function draw() {
|
|
|
127
134
|
live ? null : iconButton({ icon: 'x', ariaLabel: 'Put the note away', onClick: close, attrs: { 'data-nt-dock-close': '' } })));
|
|
128
135
|
return;
|
|
129
136
|
}
|
|
137
|
+
// the note draws its own bar (status, Stop, Minimize and the ⋯ menu): the pop-up adds no second head (Run 7)
|
|
130
138
|
const body = el('div', { class: 'nt-pop__body' });
|
|
131
|
-
const pop = el('section', { class: 'nt-pop', role: 'dialog', 'aria-label': 'Meeting note', 'data-nt-pop': active.id },
|
|
132
|
-
el('div', { class: 'nt-pop__head' }, el('span', { class: 'nt-sec__h' }, live ? 'Taking notes' : 'Meeting note'),
|
|
133
|
-
button({ label: 'Minimize', variant: 'ghost', size: 'sm', onClick: minimize, attrs: { 'data-nt-minimize': '' } })),
|
|
134
|
-
body);
|
|
139
|
+
const pop = el('section', { class: 'nt-pop', role: 'dialog', 'aria-label': 'Meeting note', 'data-nt-pop': active.id }, body);
|
|
135
140
|
host.replaceChildren(pop);
|
|
136
|
-
if (!v.renderInline(body, viewCtx, active.id)) body.appendChild(el('p', { class: 'nt-hint' }, 'Another note is being taken.')
|
|
141
|
+
if (!v.renderInline(body, viewCtx, active.id, { onMinimize: minimize })) body.appendChild(el('p', { class: 'nt-hint' }, 'Another note is being taken.'),
|
|
142
|
+
button({ label: 'Minimize', variant: 'ghost', size: 'sm', onClick: minimize, attrs: { 'data-nt-minimize': '' } }));
|
|
137
143
|
}
|