create-pmos 0.534.0 → 0.536.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 +27 -35
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/briefing-premium-sources-test.js +14 -16
- package/workspace/changelog.json +26 -0
- package/workspace/ev-everywhere-test.js +2 -2
- package/workspace/guide/data-freshness.md +10 -4
- package/workspace/guide/reports.md +11 -1
- package/workspace/lib/refresh-run.js +7 -2
- package/workspace/lib/report-engine.js +27 -6
- package/workspace/lib/report-library.js +1 -1
- package/workspace/lib/report-orders.js +218 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-report-flow.js +1 -1
- package/workspace/lib/routes-report-orders.js +161 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-briefing-depth-test.js +4 -1
- package/workspace/public/pm/cozy-ev.css +11 -2
- package/workspace/public/pm/cozy-evmodel.js +77 -0
- package/workspace/public/pm/cozy-settings.js +42 -27
- package/workspace/public/pm/cozy-vw-rp.css +40 -0
- package/workspace/public/pm/premium-report-read.js +90 -9
- package/workspace/public/pm/report-orders-ui.js +115 -0
- package/workspace/skills/report-write.skill +1 -1
- package/workspace/test-lists/report-orders.json +11 -0
|
@@ -57,6 +57,7 @@ import { presentInRecording } from './cozy-rec-bridge.js'; // while a meeting
|
|
|
57
57
|
import { placeholderHtml } from './cozy-placeholder.js';
|
|
58
58
|
import { vzStyle } from './premium-vw-open.js';
|
|
59
59
|
import { reviewHtml, chatOf as rfChatOf, ICON as RF_ICON } from './report-flow.js'; // Claude's review and the template icon (batch 22 item 14, canvas RfH2, RfH6)
|
|
60
|
+
import * as OU from './report-orders-ui.js'; // batch 27 item 16: the orders of a draft (canvas TpA, TpD, TpE)
|
|
60
61
|
import { adler } from './cozy-adler.js'; // registers window.czViews; vzStyle adds the Viewer's sheet (cozy-vw.css)
|
|
61
62
|
import { cycleGroups, cycleName, briefModel, briefGlanceHtml, briefConfirmHtml, briefWhyHtml, briefLooksHtml, paperModel, paperHtml, liveModel, liveHtml, presentHtml, dayMon, reportSummary,
|
|
62
63
|
rpSumHtml, rpBarHtml, cadenceWords, goesOutWords, FORMATS, RP_ICONS } from './rp-parts.js';
|
|
@@ -73,7 +74,7 @@ const P = {
|
|
|
73
74
|
warn: '<path d="M12 4l9 16H3zM12 10v4M12 17h.01"/>', bin: '<path d="M5 7h14M10 7V4h4v3M7 7l1 13h8l1-13"/>', box: '<path d="M4 5h16v4H4zM5 9v10h14V9M10 13h4"/>', lock: '<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/>',
|
|
74
75
|
redo2: '<path d="M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7"/>', undo: '<path d="M9 14 4 9l5-5M4 9h11a5 5 0 0 1 0 10h-3"/>', redo: '<path d="m15 14 5-5-5-5M20 9H9a5 5 0 0 0 0 10h3"/>',
|
|
75
76
|
people: '<path d="M9 11a3.5 3.5 0 1 0 0-7a3.5 3.5 0 1 0 0 7M2.5 20a6.5 6.5 0 0 1 13 0M16 4.5a3.5 3.5 0 0 1 0 6.5M18 14a6 6 0 0 1 3.5 6"/>', list: '<path d="M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01"/>',
|
|
76
|
-
plus: '<path d="M12 5v14M5 12h14"/>', dl: '<path d="M12 4v11M7 10l5 5 5-5M5 20h14"/>', live: '<path d="M3 17l5-5 4 4 8-8M16 8h4v4"/>', layout: '<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M4 10h16M10 10v10"/>',
|
|
77
|
+
plus: '<path d="M12 5v14M5 12h14"/>', grid: '<rect x="4" y="4" width="7" height="7" rx="1"/><rect x="13" y="4" width="7" height="7" rx="1"/><rect x="4" y="13" width="7" height="7" rx="1"/><rect x="13" y="13" width="7" height="7" rx="1"/>', mark: '<path d="M7 4h10v16l-5-4-5 4z"/>', dl: '<path d="M12 4v11M7 10l5 5 5-5M5 20h14"/>', live: '<path d="M3 17l5-5 4 4 8-8M16 8h4v4"/>', layout: '<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M4 10h16M10 10v10"/>',
|
|
77
78
|
theme: '<path d="M12 3a9 9 0 1 0 0 18c1 0 1.5-.8 1.5-1.6 0-1-.8-1.4-.8-2.4 0-1 .8-1.6 1.8-1.6H17a4 4 0 0 0 4-4c0-4.6-4-8.4-9-8.4z"/><circle cx="7.5" cy="11" r="1"/><circle cx="10.5" cy="7.5" r="1"/><circle cx="15" cy="7.5" r="1"/>',
|
|
78
79
|
};
|
|
79
80
|
const ic = (k, n) => svg(P[k], n);
|
|
@@ -437,7 +438,7 @@ function deckHtml() {
|
|
|
437
438
|
if (!L.length) return '<p class="vz-empty">This cycle has no slides. Read it as a page.</p>';
|
|
438
439
|
const i = R.i = Math.max(0, Math.min(R.i, L.length - 1));
|
|
439
440
|
return `<div class="vz-slide" tabindex="0" role="group" aria-roledescription="slide" aria-label="Slide ${i + 1} of ${L.length}: ${esc(L[i].h)}. Use the arrow keys to change slide." data-r="slide">${slideCard(L[i])}</div>`
|
|
440
|
-
+ `<div class="rp-nav">${ib('prev', ic('prev', 15), 'Previous slide', { off: i === 0 })}<span class="rp-count" aria-live="polite">Slide ${i + 1} of ${L.length}</span>${ib('next', ic('next', 15), 'Next slide', { off: i === L.length - 1 })}</div>`
|
|
441
|
+
+ `<div class="rp-nav">${ib('prev', ic('prev', 15), 'Previous slide', { off: i === 0 })}<span class="rp-count" aria-live="polite">Slide ${i + 1} of ${L.length}</span>${ib('next', ic('next', 15), 'Next slide', { off: i === L.length - 1 })}${R.pyr && !R.cyc && R.ord && R.ord.engine && !R.edit ? ib('ord|slide', ic('mark', 15), 'Save this slide\'s layout to My slides', { cls: ' ro-slmark', off: R.busy }) : ''}</div>`
|
|
441
442
|
+ `<div class="vz-thumbs rp-thumbs" role="group" aria-label="Slides">`
|
|
442
443
|
+ L.map((x, j) => `<button type="button" class="vz-th rp-th${x.k === 'title' ? ' rp-tt' : ''}" data-r="go|${j}"${j === i ? ' aria-current="true"' : ''} aria-label="Slide ${j + 1}: ${esc(x.h)}" title="${j + 1} · ${esc(x.h)}"><span>${slideCard(x, true)}</span></button>`).join('')
|
|
443
444
|
+ '</div>';
|
|
@@ -473,11 +474,11 @@ function rfhHtml() {
|
|
|
473
474
|
R.archived ? null : { act: 'share', label: 'Share', icon: 'share', exp: R.pop === 'share', pop: 'dialog' },
|
|
474
475
|
{ act: 'dlm', label: 'Download', icon: 'dl', exp: R.pop === 'dl' }] });
|
|
475
476
|
const view = R.err ? `<p class="vz-msg" data-tone="bad" role="alert">The document could not be read: ${esc(R.err)}</p>` : !R.W ? '<p class="vz-empty" role="status">Reading the report.</p>'
|
|
476
|
-
: R.view === 'doc' ? pageHtml() : deckHtml();
|
|
477
|
+
: R.gal ? galView() : (R.view === 'doc' ? pageHtml() : deckHtml()) + ordAssHtml();
|
|
477
478
|
const conf = confirm && R.BF ? czSections([{ id: 'confirm', title: 'Please confirm', count: confirm, open: true, render: () => briefConfirmHtml(R.BF, { issued: R.issued, busy: R.busy }) + (R.bfMsg ? `<p class="vz-msg" role="status">${esc(R.bfMsg)}</p>` : '') }], { key: 'report' }) : '';
|
|
478
479
|
const pri = R.archived ? { act: 'unarch|' + R.row.path, label: 'Bring back', icon: 'back' } : R.issued ? null : { act: 'issue', label: 'Issue it', icon: 'send' };
|
|
479
480
|
const foot = pri ? `<div class="rp-foot"><button type="button" class="rp-pri" data-r="${esc(pri.act)}" aria-label="${esc(pri.label)}" title="${esc(pri.label)}"${R.busy ? ' disabled' : ''}><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${RP_ICONS[pri.icon]}"/></svg></button></div>` : '';
|
|
480
|
-
return `<div class="vz-rpb rp-rfh">${R.conf ? confHtml() : ''}${chips}${st}${R.W && !R.err ? tools : ''}${view}${R.W && !R.err && !R.issued ? rvHtml() : ''}${conf}${askHtml()}${foot}</div>`;
|
|
481
|
+
return `<div class="vz-rpb rp-rfh">${R.conf ? confHtml() : ''}${chips}${st}${R.W && !R.err ? ordRowHtml() + tools : ''}${view}${R.W && !R.err && !R.issued ? rvHtml() : ''}${conf}${askHtml()}${foot}</div>`;
|
|
481
482
|
}
|
|
482
483
|
function bodyHtml() {
|
|
483
484
|
if (!R.edit && rfhOn()) return rfhHtml();
|
|
@@ -488,8 +489,85 @@ function bodyHtml() {
|
|
|
488
489
|
+ (R.view === 'deck' ? '' : `<div class="vz-r1">${WAIT('Page editor')}</div>`) + '<div class="vz-ed" data-vz-ed></div></div>';
|
|
489
490
|
}
|
|
490
491
|
const view = R.err ? `<p class="vz-msg" data-tone="bad" role="alert">The document could not be read: ${esc(R.err)}</p>` : !R.W ? '<p class="vz-empty" role="status">Reading the report.</p>'
|
|
491
|
-
: R.view === 'deck' ? deckHtml() : R.view === 'live' ? liveView() : pageHtml();
|
|
492
|
-
return `<div class="vz-rpb">${R.conf ? confHtml() : R.W ? sumHtml() : ''}${R.W && !R.err ? cutsHtml() : ''}${R.msg ? `<p class="vz-msg" role="status" data-tone="${R.msg.tone || ''}">${esc(R.msg.text)}</p>` : ''}${R.edit ? '' : waitHtml()}${R.W && !R.err ? barHtml() : ''}${view}${R.W && !R.err && !R.issued ? rvHtml() : ''}${R.W && !R.err ? foldsHtml() : ''}${askHtml()}</div>`;
|
|
492
|
+
: R.gal ? galView() : (R.view === 'deck' ? deckHtml() : R.view === 'live' ? liveView() : pageHtml()) + ordAssHtml();
|
|
493
|
+
return `<div class="vz-rpb">${R.conf ? confHtml() : R.W ? sumHtml() + ordRowHtml() : ''}${R.W && !R.err ? cutsHtml() : ''}${R.msg ? `<p class="vz-msg" role="status" data-tone="${R.msg.tone || ''}">${esc(R.msg.text)}</p>` : ''}${R.edit ? '' : waitHtml()}${R.W && !R.err ? barHtml() : ''}${view}${R.W && !R.err && !R.issued ? rvHtml() : ''}${R.W && !R.err ? foldsHtml() : ''}${askHtml()}</div>`;
|
|
494
|
+
}
|
|
495
|
+
/* ── the orders of a draft (batch 27 item 16; canvas TpA, TpD, TpE; report-orders-ui.js, GET /api/report-orders) ── */
|
|
496
|
+
const ORD_HIDE = 'pmos-report-orders-hidden';
|
|
497
|
+
function ordHidden() { try { return new Set(JSON.parse(localStorage.getItem(ORD_HIDE) || '[]')); } catch { return new Set(); } }
|
|
498
|
+
const PICS = new Map(); // order id → its slides drawn small with placeholder words (GET /api/report-orders/preview), 'wait' while read
|
|
499
|
+
function ordOpts() {
|
|
500
|
+
return { esc, ic, slideCard, busy: R.busy, issued: R.issued, hidden: R.ordHide, now: R.pyr || R.cyc ? slides() : null,
|
|
501
|
+
pics: (id) => { const x = PICS.get(R.ws + '|' + id); return Array.isArray(x) ? x : null; } };
|
|
502
|
+
}
|
|
503
|
+
const ordRowHtml = () => (R.ord ? OU.rowHtml(R.ord, ordOpts()) : '');
|
|
504
|
+
const ordAssHtml = () => (R.ord && !R.edit ? OU.assHtml(R.ord, ordOpts()) : '');
|
|
505
|
+
function galView() { readPics(); return OU.galHtml(R.ord, R.gal, ordOpts()); }
|
|
506
|
+
/** The pictures of the orders the gallery shows, each read once and kept (they hold placeholder words only). */
|
|
507
|
+
function readPics() {
|
|
508
|
+
const me = R, S = R.ord; if (!S || !S.ok) return;
|
|
509
|
+
const ids = R.gal && R.gal.sel ? [R.gal.sel] : S.orders.map((o) => o.id).concat((S.slides || []).map((x) => x.id));
|
|
510
|
+
for (const id of ids) {
|
|
511
|
+
const k = R.ws + '|' + id; if (id === 'now' || PICS.has(k)) continue;
|
|
512
|
+
PICS.set(k, 'wait');
|
|
513
|
+
textOf('/api/report-orders/preview?' + q({ ws: R.ws, order: id })).then((h) => { const x = splitRender(h); PICS.set(k, x.slides.map((y) => ({ k: 'frame', h: y.h, html: y.html, css: x.style }))); })
|
|
514
|
+
.catch(() => PICS.set(k, [])).then(() => { if (R === me && R.gal) draw(); });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const dropPics = () => { for (const k of [...PICS.keys()]) if (/\|mine-/.test(k)) PICS.delete(k); };
|
|
518
|
+
let ordT = null;
|
|
519
|
+
async function readOrders() {
|
|
520
|
+
const me = R; if (!R || !R.row.path) return;
|
|
521
|
+
const j = await Promise.resolve(pmGet('/api/report-orders?' + q({ ws: R.ws, path: R.row.path }))).catch(() => null);
|
|
522
|
+
if (R !== me) return;
|
|
523
|
+
R.ord = j && j.ok ? j : null;
|
|
524
|
+
if (R.gal && !R.ord) R.gal = null;
|
|
525
|
+
draw();
|
|
526
|
+
// while the report's session writes the draft again, read the state until it lands (the file watch redraws the slides)
|
|
527
|
+
clearTimeout(ordT);
|
|
528
|
+
if (R.ord && R.ord.undo && R.ord.undo.state === 'drafting') ordT = setTimeout(() => { if (R === me) readOrders(); }, 6000);
|
|
529
|
+
}
|
|
530
|
+
async function ordAct(k, v) {
|
|
531
|
+
const me = R, S = R.ord;
|
|
532
|
+
if (k === 'gal') { R.gal = { sel: null }; R.pop = null; return draw(); }
|
|
533
|
+
if (k === 'close') { R.gal = null; return draw(); }
|
|
534
|
+
if (k === 'back') { R.gal = { sel: null }; return draw(); }
|
|
535
|
+
if (k === 'open') { R.gal = { sel: v, i: 0 }; return draw(); }
|
|
536
|
+
if (k === 'at') { if (R.gal) R.gal.i = +v; return draw(); }
|
|
537
|
+
if (k === 'hide') { R.ordHide.add(OU.hideKey(S, v)); try { localStorage.setItem(ORD_HIDE, JSON.stringify([...R.ordHide].slice(-200))); } catch { /* kept for this visit */ } return draw(); }
|
|
538
|
+
if (k === 'ren') { const o = S && [...S.orders, ...(S.slides || [])].find((x) => x.id === v); if (R.gal && o) R.gal.ren = o.name; draw(); const f = bodyEl() && bodyEl().querySelector('#roName'); if (f) f.focus(); return; }
|
|
539
|
+
if (R.busy) return;
|
|
540
|
+
const post = async (sub, body) => { R.busy = true; draw(); const r = await pmPost('/api/report-orders/' + sub, { ws: R.ws, ...body }).catch(() => null); if (R !== me) return null; R.busy = false; return r || { ok: false, error: 'the page could not reach PMOS' }; };
|
|
541
|
+
if (k === 'apply') {
|
|
542
|
+
const r = await post('apply', { path: R.row.path, order: v }); if (!r) return;
|
|
543
|
+
if (!r.ok) return say('The order was not applied: ' + r.error + '.', 'bad');
|
|
544
|
+
R.gal = null;
|
|
545
|
+
await readOrders(); if (R !== me) return;
|
|
546
|
+
return say(r.next ? `From the next cycle this report starts in the ${r.next.name} order. Today's draft stays as agreed.` : `Claude is writing this draft again in the ${r.order.name} order. Undo puts this one back, for one day.`);
|
|
547
|
+
}
|
|
548
|
+
if (k === 'undo') {
|
|
549
|
+
const r = await post('undo', { path: R.row.path }); if (!r) return;
|
|
550
|
+
if (!r.ok) return say('Nothing was undone: ' + r.error + '.', 'bad');
|
|
551
|
+
await reread(); if (R !== me) return; await readOrders(); if (R !== me) return;
|
|
552
|
+
return say('The draft is back as it was before the redraft.');
|
|
553
|
+
}
|
|
554
|
+
if (k === 'save' || k === 'slide') {
|
|
555
|
+
const r = await post(k, k === 'save' ? { path: R.row.path } : { path: R.row.path, n: R.i }); if (!r) return;
|
|
556
|
+
if (!r.ok) return say('Nothing was saved: ' + r.error + '.', 'bad');
|
|
557
|
+
R.ordHide.add(OU.hideKey(S, 'edit')); dropPics(); await readOrders(); if (R !== me) return;
|
|
558
|
+
return say(k === 'save' ? 'Saved to My orders. It is first in the orders, marked Mine. Only its parts and slide layouts are kept.' : `Saved to My slides: ${r.name}, its layout only.`);
|
|
559
|
+
}
|
|
560
|
+
if (k === 'renok') {
|
|
561
|
+
const r = await post('rename', { id: v, name: (R.gal && R.gal.ren) || '' }); if (!r) return;
|
|
562
|
+
if (!r.ok) return say('The name did not change: ' + r.error + '.', 'bad');
|
|
563
|
+
if (R.gal) R.gal.ren = null; dropPics(); return readOrders();
|
|
564
|
+
}
|
|
565
|
+
if (k === 'del') {
|
|
566
|
+
const r = await post('remove', { id: v }); if (!r) return;
|
|
567
|
+
if (!r.ok) return say('It was not deleted: ' + r.error + '.', 'bad');
|
|
568
|
+
R.gal = { sel: null }; dropPics(); await readOrders(); if (R !== me) return;
|
|
569
|
+
return say('Deleted from your orders. No report changed.');
|
|
570
|
+
}
|
|
493
571
|
}
|
|
494
572
|
/* ── Claude's review (batch 22 item 14, canvas RfH2): the checks PMOS runs on the draft, as ✦ fixes the report's own session makes ── */
|
|
495
573
|
function rvHtml() {
|
|
@@ -605,9 +683,10 @@ function wire(b) {
|
|
|
605
683
|
e.preventDefault(); act(t.dataset.r.split('|'), t);
|
|
606
684
|
};
|
|
607
685
|
b.onchange = (e) => { if (e.target.dataset.r === 'stime') R.sch.time = e.target.value || '09:00'; };
|
|
608
|
-
b.oninput = (e) => { if (e.target.dataset.r === 'shpass') R.shr.pass = e.target.value; };
|
|
686
|
+
b.oninput = (e) => { if (e.target.dataset.r === 'shpass') R.shr.pass = e.target.value; if (e.target.dataset.r === 'ord-name' && R.gal) R.gal.ren = e.target.value; };
|
|
609
687
|
b.onkeydown = (e) => {
|
|
610
688
|
if (e.key === 'Escape' && R.pop) { e.preventDefault(); e.stopPropagation(); R.pop = null; draw(); return; }
|
|
689
|
+
if (e.key === 'Escape' && R.gal) { e.preventDefault(); e.stopPropagation(); R.gal = R.gal.sel ? { sel: null } : null; draw(); return; }
|
|
611
690
|
if (!R.edit && R.view === 'deck' && /^Arrow(Left|Right)$/.test(e.key) && !e.target.closest('input')) { const L = slides().length, j = R.i + (e.key === 'ArrowRight' ? 1 : -1); if (j >= 0 && j < L) { e.preventDefault(); R.i = j; draw(); const s = bodyEl().querySelector('.vz-slide'); if (s) s.focus(); } }
|
|
612
691
|
};
|
|
613
692
|
/* a phone swipes the slide (RpPhone "swipe for the next") */
|
|
@@ -634,6 +713,7 @@ export function fit(root) { root.querySelectorAll('.vz-fr').forEach((c) => { if
|
|
|
634
713
|
const say = (text, tone) => { R.msg = { text, tone }; draw(); const me = R, m = R.msg; if (tone !== 'bad') setTimeout(() => { if (R === me && R.msg === m) { R.msg = null; draw(); } }, 9000); };
|
|
635
714
|
async function act([k, v], t) {
|
|
636
715
|
if (k === 'rv-do' || k === 'rv-no' || k === 'rv-all') return rvAct(k, v);
|
|
716
|
+
if (k === 'ord') return ordAct(v, t.dataset.r.split('|').slice(2).join('|'));
|
|
637
717
|
if (k === 'pop') { R.pop = v || null; return draw(); }
|
|
638
718
|
if (k === 'cut') return cutTo(v);
|
|
639
719
|
if (k === 'bf') return briefAct(v, t.dataset.r.split('|').slice(2).join('|'));
|
|
@@ -1030,7 +1110,7 @@ export async function openReport(row, hand = {}) {
|
|
|
1030
1110
|
if (R) present(false);
|
|
1031
1111
|
const me = R = { row, ws: hand.ws || '', id: c.cadence, name: hand.name || row.name || c.cadence || 'Report', audience: hand.audience || '', issued: row.state === 'issued', archived: !!row.archived,
|
|
1032
1112
|
text: '', err: '', W: null, view: 'deck', BF: null, bfRaw: null, bfMsg: '', LV: null, blank: false, i: 0, cyc: null, pyr: null, setup: undefined, pop: null, conf: false, checks: null, issueErr: '', busy: false, ask: '', askErr: '', edit: false, msg: null,
|
|
1033
|
-
sch: schedPick(null), schErr: '', shr: null, rv: null, rvHide: new Set(), rvSent: new Set(), shares: null, more: [], hand, cuts: hand.cuts ? { on: 'team', team: null, sponsors: null, err: '' } : null, cycles: (hand.cycles || []).slice(), onIssue: hand.onIssue || null, onChange: hand.onChange || null, vid: 'report:' + row.path };
|
|
1113
|
+
sch: schedPick(null), schErr: '', shr: null, ord: null, gal: null, ordHide: ordHidden(), rv: null, rvHide: new Set(), rvSent: new Set(), shares: null, more: [], hand, cuts: hand.cuts ? { on: 'team', team: null, sponsors: null, err: '' } : null, cycles: (hand.cycles || []).slice(), onIssue: hand.onIssue || null, onChange: hand.onChange || null, vid: 'report:' + row.path };
|
|
1034
1114
|
const head = me.head = () => ({ id: me.vid, kind: 'Report' + (me.audience ? ' · ' + me.audience : ''), title: me.name, icon: me.view === 'doc' ? 'doc' : 'report', full: true, url: viewUrl(row, me.ws), more: me.more,
|
|
1035
1115
|
// the template behind one icon (batch 22 item 14, canvas RfH1): format, readers, when it goes out, and its history
|
|
1036
1116
|
presses: me.setup ? [{ act: 'chat', label: 'Talk to Claude about this report', icon: RF_ICON.chat }, { act: 'template', label: 'Template: format, readers, look, sources', icon: RF_ICON.tune }] : [],
|
|
@@ -1048,6 +1128,7 @@ export async function openReport(row, hand = {}) {
|
|
|
1048
1128
|
if (R !== me) return me;
|
|
1049
1129
|
readBrief(); // the Brief tab's count shows at once
|
|
1050
1130
|
readReview();
|
|
1131
|
+
readOrders();
|
|
1051
1132
|
if (hand.say) me.msg = { text: hand.say };
|
|
1052
1133
|
if (vCur() && vCur().id === me.vid && (vCur().title !== me.name || me.audience || me.setup)) { me.reopen = true; vOpen(head()); me.reopen = false; } // the name and audience are known now
|
|
1053
1134
|
draw();
|
|
@@ -1067,7 +1148,7 @@ async function watchFile(me) {
|
|
|
1067
1148
|
if (R !== me || !h) continue;
|
|
1068
1149
|
// a session writes the draft in many steps: read it when the file has been still for one more check, so a burst of writes is one redraw
|
|
1069
1150
|
if (seen && h !== seen) { seen = h; pending = true; continue; }
|
|
1070
|
-
if (pending) { pending = false; await reread(); if (R === me && !me.edit && !me.pop) { me.fromFile = true; draw(); me.fromFile = false; readReview(); } }
|
|
1151
|
+
if (pending) { pending = false; await reread(); if (R === me && !me.edit && !me.pop) { me.fromFile = true; draw(); me.fromFile = false; readReview(); readOrders(); } }
|
|
1071
1152
|
seen = h;
|
|
1072
1153
|
}
|
|
1073
1154
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/* pm/report-orders-ui.js — the orders of a report draft in the report Viewer (batch 27 item 16; main canvas TpA, TpD, TpE). Pure html builders;
|
|
2
|
+
* premium-report-read.js holds the state and the presses (data-r="ord|…") and reads GET /api/report-orders (lib/routes-report-orders.js).
|
|
3
|
+
* The app says "order", never "template".
|
|
4
|
+
*
|
|
5
|
+
* rowHtml(S, o) the order line in the summary: the order's name as a chip (zero is silence for the standard order), the grid icon that
|
|
6
|
+
* opens the gallery (a draft), the bookmark (save as my order), Undo while its day lasts, and the next cycle's order
|
|
7
|
+
* assHtml(S, o) the one ✦ under the slide: Claude's pick (✕ · grid · ✓ redraft) | "Redrafting in …" | after the PM edited an order,
|
|
8
|
+
* "You changed this order … Save it as yours?" (✕ · bookmark). Nothing to say → ''.
|
|
9
|
+
* galHtml(S, G, o) the gallery: Now, then Mine, then the seven PMOS orders, each with the pictures of its first four layouts and its slide
|
|
10
|
+
* count; My slides; a pressed card shows its slides, its parts, what it adds or drops, and ✓ (Redraft in this order, or
|
|
11
|
+
* Use from the next cycle on an agreed outline). Rename and Delete on Mine only.
|
|
12
|
+
* S = the GET answer · G = { sel, i, ren } · o = { esc, ic, slideCard, pics(id) → [slide] | null, now: [slide], busy, hidden: Set, issued }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const keyOf = (S, k) => k + ':' + (S && S.slot) + ':' + ((S && S.pick && S.pick.id) || (S && S.now && S.now.id) || '');
|
|
16
|
+
export const hideKey = keyOf;
|
|
17
|
+
const words = (L) => (L.length <= 1 ? L.join('') : L.slice(0, -1).join(', ') + ' and ' + L[L.length - 1]);
|
|
18
|
+
const ALWAYS = new Set(['overview', 'sources']);
|
|
19
|
+
/** "adds Why, Scope and People; drops The week" (pure). */
|
|
20
|
+
export function diffWords(d) {
|
|
21
|
+
const a = (d && d.adds) || [], r = (d && d.drops) || [];
|
|
22
|
+
if (!a.length && !r.length) return 'Same parts as your draft.';
|
|
23
|
+
return 'Compared with your draft: ' + [a.length ? 'adds ' + words(a) : '', r.length ? 'drops ' + words(r) : ''].filter(Boolean).join('; ') + '.';
|
|
24
|
+
}
|
|
25
|
+
const btn = (o, act, icon, label, cls = '') => `<button type="button" class="rp-tl vz-hv ro-ib${cls}" data-r="${o.esc(act)}" aria-label="${o.esc(label)}" title="${o.esc(label)}"${o.busy ? ' disabled' : ''}>${o.ic(icon, 16)}</button>`;
|
|
26
|
+
|
|
27
|
+
/** The order line under the summary. */
|
|
28
|
+
export function rowHtml(S, o) {
|
|
29
|
+
if (!S || !S.ok) return '';
|
|
30
|
+
const e = o.esc, n = S.now || {}, out = [];
|
|
31
|
+
const named = n.kind !== 'standard';
|
|
32
|
+
if (!o.issued) out.push(`<button type="button" class="rp-mc ro-chip" data-r="ord|gal" aria-label="${e(named ? n.name + ': see the orders' : 'See the orders')}" title="See the orders">${o.ic('grid', 14)}${named ? e(n.name) + (n.edited ? ', edited' : '') : ''}</button>`);
|
|
33
|
+
else if (named) out.push(`<span class="rp-mc ro-chip">${o.ic('grid', 14)}${e(n.name)}</span>`);
|
|
34
|
+
if (S.next) out.push(`<span class="rp-mc ro-next">Next cycle: ${e(S.next.name)}</span>`);
|
|
35
|
+
if (S.engine) out.push(btn(o, 'ord|save', 'mark', 'Save as my order'));
|
|
36
|
+
if (S.undo && S.undo.state !== 'drafting' && !o.issued) out.push(btn(o, 'ord|undo', 'undo', 'Undo the redraft'));
|
|
37
|
+
return `<div class="rp-mcs ro-row">${out.join('')}</div>`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const parts = (o, list, adds, cls) => list.filter((p) => !ALWAYS.has(p.layout)).map((p) => `<span class="ro-pt${adds && adds.includes(p.part) ? ' add' : ''}${cls || ''}">${o.esc(p.part)}</span>`).join('');
|
|
41
|
+
/** The one ✦ under the slide. */
|
|
42
|
+
export function assHtml(S, o) {
|
|
43
|
+
if (!S || !S.ok || o.issued) return '';
|
|
44
|
+
const e = o.esc, h = o.hidden || new Set();
|
|
45
|
+
const U = S.undo;
|
|
46
|
+
if (U && U.state !== 'done' && U.order) {
|
|
47
|
+
const late = U.state === 'late';
|
|
48
|
+
return `<div class="ro-ass" role="status"><p><b class="ro-st" aria-hidden="true">✦</b> ${late ? 'The redraft in the ' + e(U.order.name) + ' order has not landed yet.' : 'Redrafting in the ' + e(U.order.name) + ' order.'}</p>`
|
|
49
|
+
+ `<p class="ro-note">${late ? 'Undo puts the draft back as it was.' : 'It takes a few minutes. Every reading stays; the new draft shows here when it lands.'}</p></div>`;
|
|
50
|
+
}
|
|
51
|
+
if (S.pick && !h.has(keyOf(S, 'pick'))) {
|
|
52
|
+
const ord = (S.orders || []).find((x) => x.id === S.pick.id);
|
|
53
|
+
if (!ord) return '';
|
|
54
|
+
return `<div class="ro-ass" role="group" aria-label="Claude's pick"><p><b class="ro-st" aria-hidden="true">✦</b> The ${e(ord.name)} order fits this report's question.</p>`
|
|
55
|
+
+ `<p class="ro-pts">${parts(o, ord.parts, S.pick.adds)}${(S.pick.drops || []).map((x) => `<span class="ro-pt cut">${e(x)}</span>`).join('')}</p>`
|
|
56
|
+
+ `<p class="ro-note">Blue is added${(S.pick.drops || []).length ? ', struck through is dropped' : ''}. Every reading stays.</p>`
|
|
57
|
+
+ `<div class="ro-acts">${btn(o, 'ord|hide|pick', 'x', 'Not now')}${btn(o, 'ord|open|' + ord.id, 'grid', 'See it with the other orders')}${btn(o, 'ord|apply|' + ord.id, 'check', 'Redraft in this order', ' rp-pri')}</div></div>`;
|
|
58
|
+
}
|
|
59
|
+
const n = S.now || {};
|
|
60
|
+
if (S.engine && n.kind === 'order' && n.edited && !h.has(keyOf(S, 'edit'))) {
|
|
61
|
+
const c = n.changes || {}, said = [(c.drops || []).length ? 'dropped ' + words(c.drops.map((x) => '“' + x + '”')) : '', (c.adds || []).length ? 'added ' + words(c.adds.map((x) => '“' + x + '”')) : ''].filter(Boolean).join(', ');
|
|
62
|
+
return `<div class="ro-ass" role="group" aria-label="Save your order"><p><b class="ro-st" aria-hidden="true">✦</b> You changed the ${e(n.name)} order${said ? ': ' + e(said) : ''}. Save it as yours?</p>`
|
|
63
|
+
+ `<p class="ro-pts">${parts(o, n.parts, c.adds)}</p><p class="ro-note">It saves the parts and their slide layouts. Not the numbers or the words.</p>`
|
|
64
|
+
+ `<div class="ro-acts">${btn(o, 'ord|hide|edit', 'x', 'Not now')}${btn(o, 'ord|save', 'mark', 'Save as my order', ' rp-pri')}</div></div>`;
|
|
65
|
+
}
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Up to four pictures of an order's layouts (real slides, drawn small). Until they are read: grey boxes. */
|
|
70
|
+
function pics(o, L, n = 4) {
|
|
71
|
+
if (!L) return `<span class="ro-pics" aria-hidden="true">${'<span class="ro-pic ro-wait"></span>'.repeat(n)}</span>`;
|
|
72
|
+
return `<span class="ro-pics" aria-hidden="true">${L.slice(0, n).map((s) => `<span class="ro-pic">${o.slideCard(s, true)}</span>`).join('')}</span>`;
|
|
73
|
+
}
|
|
74
|
+
const more = (n) => n + (n === 1 ? ' slide' : ' slides') + (n > 4 ? ' · +' + (n - 4) + ' more' : '');
|
|
75
|
+
function cardHtml(S, o, c) {
|
|
76
|
+
const e = o.esc, mark = c.now ? '<span class="ro-tag">Now</span>' : c.mine ? '<span class="ro-tag">Mine</span>' : S.pick && S.pick.id === c.id ? '<span class="ro-tag ro-star" aria-label="Claude\'s pick">✦</span>' : '';
|
|
77
|
+
return `<button type="button" class="ro-card${c.now ? ' now' : ''}" data-r="ord|open|${e(c.id)}"${c.now ? ' aria-current="true"' : ''}>${pics(o, c.now ? o.now : o.pics(c.id))}`
|
|
78
|
+
+ `<span class="ro-cn">${mark}<b>${e(c.name)}</b></span><small>${more(c.n)}</small></button>`;
|
|
79
|
+
}
|
|
80
|
+
/** The gallery (TpD), or one order pressed open. */
|
|
81
|
+
export function galHtml(S, G, o) {
|
|
82
|
+
if (!S || !S.ok) return '<p class="vz-empty" role="status">Reading the orders.</p>';
|
|
83
|
+
const e = o.esc;
|
|
84
|
+
const close = `<button type="button" class="rp-tl vz-hv ro-ib" data-r="ord|close" aria-label="Close the orders" title="Close">${o.ic('x', 16)}</button>`;
|
|
85
|
+
if (G.sel) return detailHtml(S, G, o);
|
|
86
|
+
const n = S.now || {};
|
|
87
|
+
const head = S.now.kind === 'agreed'
|
|
88
|
+
? `<p class="ro-note">Your readers agreed this order${n.since ? ' when the ' + e(n.since) + ' report went out' : ''}. A pick here is used from the next cycle; today's draft stays as agreed.</p>`
|
|
89
|
+
: S.pick ? `<p class="ro-note"><b class="ro-st" aria-hidden="true">✦</b> The ${e((S.orders.find((x) => x.id === S.pick.id) || {}).name || '')} order fits this report's question. Press a card to see it, or pick another.</p>` : '';
|
|
90
|
+
const cards = [{ id: 'now', now: true, name: n.name, n: (o.now || []).length || n.parts.length }].concat(S.orders.filter((x) => x.mine), S.orders.filter((x) => !x.mine));
|
|
91
|
+
const sl = (S.slides || []).length ? `<p class="rp-lbl">My slides</p><div class="ro-slides">${S.slides.map((s) => `<button type="button" class="ro-sl" data-r="ord|open|${e(s.id)}">${pics(o, o.pics(s.id), 1)}<small>${e(s.name)}</small></button>`).join('')}</div>` : '';
|
|
92
|
+
return `<div class="ro-gal" role="region" aria-label="Orders"><div class="ro-gh"><b>Orders</b>${close}</div>${head}`
|
|
93
|
+
+ `<div class="ro-grid">${cards.map((c) => cardHtml(S, o, c)).join('')}</div>${sl}`
|
|
94
|
+
+ `<p class="ro-note">An order is a default order of slides. The writer keeps every reading and may add or drop a part.</p></div>`;
|
|
95
|
+
}
|
|
96
|
+
function detailHtml(S, G, o) {
|
|
97
|
+
const e = o.esc;
|
|
98
|
+
const isNow = G.sel === 'now';
|
|
99
|
+
const slide = (S.slides || []).find((x) => x.id === G.sel);
|
|
100
|
+
const ord = isNow ? { id: 'now', name: S.now.name, n: (o.now || []).length, parts: S.now.parts } : slide ? { id: slide.id, name: slide.name, n: 1, parts: [{ part: slide.part, layout: slide.layout }], mine: true, slide: true } : S.orders.find((x) => x.id === G.sel);
|
|
101
|
+
if (!ord) return '';
|
|
102
|
+
const L = isNow ? o.now : o.pics(ord.id);
|
|
103
|
+
const i = Math.max(0, Math.min(G.i || 0, L ? L.length - 1 : 0));
|
|
104
|
+
const big = L && L[i] ? `<div class="vz-slide ro-big">${o.slideCard(L[i], false)}</div>` : '<div class="vz-slide ro-big"><span class="ro-pic ro-wait"></span></div>';
|
|
105
|
+
const strip = L && L.length > 1 ? `<div class="vz-thumbs rp-thumbs ro-strip" role="group" aria-label="Slides">${L.map((s, j) => `<button type="button" class="vz-th rp-th" data-r="ord|at|${j}"${j === i ? ' aria-current="true"' : ''} aria-label="Slide ${j + 1}: ${e(s.h)}"><span>${o.slideCard(s, true)}</span></button>`).join('')}</div>` : '';
|
|
106
|
+
const agreed = S.now.kind === 'agreed';
|
|
107
|
+
const sub = isNow ? 'Now' : ord.slide ? 'My slide · its layout only' : (ord.mine ? 'Mine · ' : '') + more(ord.n).split(' · ')[0] + (ord.sub ? ' · ' + ord.sub : '');
|
|
108
|
+
const diff = isNow || ord.slide ? '' : `<p class="ro-note">${e(diffWords(ord))}${agreed ? ' From the next cycle: today\'s draft stays as agreed.' : ' Every reading stays.'}</p>`;
|
|
109
|
+
const ren = G.ren ? `<div class="ro-ren"><label class="vz-lab" for="roName">Name</label><input id="roName" data-r="ord-name" value="${e(G.ren)}" maxlength="80"><button type="button" class="rp-tl vz-hv ro-ib rp-pri" data-r="ord|renok|${e(ord.id)}" aria-label="Keep this name" title="Keep this name"${o.busy ? ' disabled' : ''}>${o.ic('check', 16)}</button></div>` : '';
|
|
110
|
+
const acts = [btn(o, 'ord|back', 'prev', 'Back to the orders')];
|
|
111
|
+
if (ord.mine) acts.push(btn(o, 'ord|del|' + ord.id, 'bin', 'Delete'), btn(o, 'ord|ren|' + ord.id, 'pen', 'Rename'));
|
|
112
|
+
if (!isNow && !ord.slide && !o.issued) acts.push(btn(o, 'ord|apply|' + ord.id, 'check', agreed ? 'Use from the next cycle' : 'Redraft in this order', ' rp-pri'));
|
|
113
|
+
return `<div class="ro-gal ro-one" role="region" aria-label="${e(ord.name)}"><div class="ro-gh"><span><b>${e(ord.name)}</b><small>${e(sub)}</small></span></div>${big}${strip}`
|
|
114
|
+
+ `<p class="ro-pts">${ord.parts.map((p, j) => `<span class="ro-pt${!isNow && j === i ? ' on' : ''}">${e(p.part)}</span>`).join('')}</p>${diff}${ren}<div class="ro-acts">${acts.join('')}</div></div>`;
|
|
115
|
+
}
|
|
@@ -33,7 +33,7 @@ Procedure:
|
|
|
33
33
|
5. Choose the outline for these readers and this question. Any sections, any titles, any order.
|
|
34
34
|
- A recurring report keeps `agreed_outline`, the sections of its last issued issue. Add a section only when a finding needs one, drop one only when it would be empty, and say so in `outline_note` (one line).
|
|
35
35
|
- `earlier_outline` is the shape of an issue written before the engine (the old fixed frame). It is an example, not an agreement: keep what served the readers.
|
|
36
|
-
- `template` is set when the PM started the report from a report template (`pmos report make <id> --template <name>`; `pmos report library --json` lists them). Its `outline` is a default order of parts, each with its slide layout: use it as the starting outline, retitle each part as a finding, and add or drop a part when the findings say so. An agreed outline wins over a template.
|
|
36
|
+
- `template` is set when the PM started the report from a report template (`pmos report make <id> --template <name>`; `pmos report library --json` lists them). Its `outline` is a default order of parts, each with its slide layout: use it as the starting outline, retitle each part as a finding, and add or drop a part when the findings say so. An agreed outline wins over a template, except when `template.chosen` is true: the PM picked that order for this cycle, so use it, and read `earlier_outline` as an example only. The template can also be an order the PM saved (its id starts with `mine-`) or one the PM applied from the report's orders; it reads the same way.
|
|
37
37
|
- Each section title is a finding, not a label: "Sign-up stalls before submit", not "Sign-up". Reading only the titles tells the story.
|
|
38
38
|
- Shapes other reports took, as examples and never a menu: a decision (the lean · the options and what each costs · what could reverse it · your call); a watch (today against yesterday and the week · the drop that moved most · why); a stakeholder update (are the dates on track · what each team needs and by when · open decisions · what could stop it); a review (the summary for decision · the outcomes by segment · the challenges and answers · method); did-it-work (what was promised · what we got · the verdict).
|
|
39
39
|
6. Write `<slot>.report.json` (in a scratch file, then `pmos report write <id> --from <file>`):
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Batch 27 item 16: apply an order to a report draft (the ✦ pick, the orders gallery's state and pictures, the redraft in the report's own session, Undo for one day, the next cycle's order on an agreed outline) and my saved orders and slides (per-user ledger). Node only.",
|
|
3
|
+
"modules": [
|
|
4
|
+
"report-orders.test.js",
|
|
5
|
+
"report-orders-page.test.mjs"
|
|
6
|
+
],
|
|
7
|
+
"seconds": {
|
|
8
|
+
"report-orders.test.js": 2,
|
|
9
|
+
"report-orders-page.test.mjs": 1
|
|
10
|
+
}
|
|
11
|
+
}
|