create-pmos 0.523.0 → 0.524.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 +44 -16
- 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/changelog.json +43 -0
- package/workspace/guide/agent-capabilities.md +6 -3
- package/workspace/guide/home.md +1 -1
- package/workspace/guide/horizon-scan.md +2 -2
- package/workspace/guide/idle-lab.md +46 -0
- package/workspace/guide/index.md +2 -0
- package/workspace/guide/moments.md +43 -0
- package/workspace/guide/welcome.md +37 -28
- package/workspace/lib/bg-queue.js +226 -0
- package/workspace/lib/driver-run.js +8 -1
- package/workspace/lib/first-reveal.js +311 -0
- package/workspace/lib/idle-lab.js +275 -0
- package/workspace/lib/moments.js +241 -0
- package/workspace/lib/morning-goals.js +191 -0
- package/workspace/lib/onboard-fundamentals.js +205 -0
- package/workspace/lib/onboarding-progress.js +13 -1
- package/workspace/lib/orchestrator-spawn.js +9 -2
- package/workspace/lib/project-onboard.js +86 -6
- package/workspace/lib/proposals.js +9 -5
- package/workspace/lib/route-modules.js +4 -0
- package/workspace/lib/routes-lab.js +137 -0
- package/workspace/lib/routes-loose.js +1 -0
- package/workspace/lib/routes-moments.js +42 -0
- package/workspace/lib/routes-morning-goals.js +109 -0
- package/workspace/lib/routes-onboard.js +28 -8
- package/workspace/lib/routes-proposals.js +12 -2
- package/workspace/lib/routes-reveal.js +73 -0
- package/workspace/lib/scan-run.js +4 -3
- package/workspace/lib/scan.js +5 -3
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/project-onboard-flow-test.js +70 -57
- package/workspace/public/pm/cozy-ev.css +3 -0
- package/workspace/public/pm/cozy-lab-ins.js +87 -0
- package/workspace/public/pm/cozy-lab.css +86 -0
- package/workspace/public/pm/cozy-lab.js +138 -0
- package/workspace/public/pm/cozy-nav.js +22 -8
- package/workspace/public/pm/cozy-onboard.css +31 -0
- package/workspace/public/pm/cozy-onboard.js +217 -111
- package/workspace/public/pm/cozy-reveal.css +52 -0
- package/workspace/public/pm/cozy-reveal.js +130 -0
- package/workspace/public/pm/home-app.js +38 -3
- package/workspace/public/pm/home-impact.css +12 -0
- package/workspace/public/pm/home-moment.js +41 -0
- package/workspace/public/pm/ini-props.js +15 -6
- package/workspace/public/pm/premium-board.js +2 -0
- package/workspace/public/pm/premium-first.js +1 -1
- package/workspace/public/pm/premium-insights.js +6 -1
- package/workspace/public/pm/quiet-place.js +31 -0
- package/workspace/public/premium-intelligence.html +3 -0
- package/workspace/replaced-tests.json +14 -0
- package/workspace/server.js +46 -5
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/first-reveal.json +9 -0
- package/workspace/test-lists/idle-lab.json +9 -0
- package/workspace/test-lists/morning-goals.json +9 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/* pm/cozy-reveal.js — Home right after setup (onboarding build B2; frames ObA2First and ObA5Aha on the In development canvas; the model is
|
|
2
|
+
* lib/first-reveal.js, GET /api/first-reveal).
|
|
3
|
+
*
|
|
4
|
+
* In the Home chat's own place, in order: the ticks of what setup did (each with its real time), the product portrait (only the cells a
|
|
5
|
+
* source returned, each with its source), the drafted first initiative (✓ keeps it, ✕ drops it; one ✦ at a time), and one quiet line for what
|
|
6
|
+
* the notes did not say. The composer is open: what the PM types goes to Home's normal chat. ✓ creates the initiative, then its chat is
|
|
7
|
+
* asked for the BRD (the existing Triage output), so the PM sees it drafted with its open questions marked.
|
|
8
|
+
*
|
|
9
|
+
* mountReveal(host, { ws, model, send(text), openIni(id, prompt), changed(model) }) → { el, close() }
|
|
10
|
+
* leftHtml(model) the rows Home's list shows at its top while the reveal is new: the draft, the weekly, the PM's own tickets
|
|
11
|
+
*
|
|
12
|
+
* Every text from a connector is escaped. No confetti, no streaks, no "great job": the mood is never happier than the data. */
|
|
13
|
+
import { pmPost } from './http.js';
|
|
14
|
+
import { adler } from './cozy-adler.js';
|
|
15
|
+
|
|
16
|
+
// its own stylesheet, from the module's own folder so it shares the module's build (test.js N2-build); premium.html stays as it is
|
|
17
|
+
if (typeof document !== 'undefined') {
|
|
18
|
+
const href = new URL('./cozy-reveal.css', import.meta.url).href;
|
|
19
|
+
if (![...document.querySelectorAll('link[rel="stylesheet"]')].some((x) => x.href === href)) { const l = document.createElement('link'); l.rel = 'stylesheet'; l.href = href; document.head.append(l); }
|
|
20
|
+
}
|
|
21
|
+
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
22
|
+
const SVG = (d, n = 16, w = 2.2) => `<svg width="${n}" height="${n}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${w}" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${d}</svg>`;
|
|
23
|
+
const I = {
|
|
24
|
+
check: '<path d="M5 12.5l4.5 4.5L19 7.5"/>',
|
|
25
|
+
x: '<path d="M6 6l12 12M18 6L6 18"/>',
|
|
26
|
+
edit: '<path d="M4 20h4L19 9l-4-4L4 16v4z"/><path d="M14 6l4 4"/>',
|
|
27
|
+
up: '<path d="M12 19V5M5 12l7-7 7 7"/>',
|
|
28
|
+
spark: '<path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2.5 2.5M15.5 15.5L18 18M6 18l2.5-2.5M15.5 8.5L18 6"/>',
|
|
29
|
+
};
|
|
30
|
+
const srcLine = (s) => (!s ? '' : Array.isArray(s) ? s.map(srcLine).filter(Boolean).join(' · ') : (s.url ? `<a href="${esc(s.url)}" target="_blank" rel="noopener noreferrer">${esc(s.label)}</a>` : esc(s.label)) + (s.title ? ' · ' + esc(s.title) : ''));
|
|
31
|
+
const hhmm = (iso) => { const t = Date.parse(iso || ''); return Number.isFinite(t) ? new Date(t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''; };
|
|
32
|
+
|
|
33
|
+
function ticksHtml(M) {
|
|
34
|
+
if (!M.ticks || !M.ticks.length) return '';
|
|
35
|
+
return `<ul class="rv-ticks">${M.ticks.map((t, i) => `<li style="--rv-i:${i}">${SVG(I.check, 14, 2.8)}<span>${esc(t.text)}</span><em>${esc(t.at)}</em></li>`).join('')}</ul>`;
|
|
36
|
+
}
|
|
37
|
+
function cellHtml(c) {
|
|
38
|
+
if (c.key === 'goal') {
|
|
39
|
+
const nums = c.now || c.target ? `<b class="rv-big">${esc(c.now || '—')}${c.target ? ` <i>→</i> ${esc(c.target)}` : ''}</b>` : '';
|
|
40
|
+
return `<div class="rv-cell"><p class="rv-lab">${esc(c.label)}</p>${nums}<p class="rv-txt">${esc(c.metric)}</p>${c.pct != null ? `<div class="rv-bar" role="img" aria-label="${esc(c.now)} of ${esc(c.target)}"><i style="width:${c.pct}%"></i></div>` : ''}<p class="rv-src">${srcLine(c.src)}</p></div>`;
|
|
41
|
+
}
|
|
42
|
+
if (c.key === 'time') {
|
|
43
|
+
const marks = c.months.map((m) => `<span class="rv-mon" style="left:${m.at}%">${esc(m.label)}</span>`).join('')
|
|
44
|
+
+ c.weekly.map((w) => `<i class="rv-dot" style="left:${w.at}%" title="${esc(w.day)}"></i>`).join('')
|
|
45
|
+
+ `<i class="rv-today" style="left:${c.today}%" title="Today"></i><span class="rv-chk" style="left:${c.check.at}%" title="Check ${esc(c.check.label)}">${SVG(I.check, 11, 3)}</span>`;
|
|
46
|
+
return `<div class="rv-cell"><p class="rv-lab">${esc(c.label)}</p><div class="rv-track">${marks}</div><p class="rv-txt">${esc(c.line)}${c.line ? ' · ' : ''}check ${esc(c.check.label)}</p>${c.meet ? `<p class="rv-txt">${esc(c.meet)}</p>` : ''}<p class="rv-src">${srcLine(c.src)}</p></div>`;
|
|
47
|
+
}
|
|
48
|
+
if (c.key === 'work') {
|
|
49
|
+
return `<div class="rv-cell"><p class="rv-lab">${esc(c.label)}</p><b class="rv-big">${c.open}<i> open</i>${c.mine != null ? ` · ${c.mine}<i> yours</i>` : ''}</b><p class="rv-txt">${c.key_ ? 'Jira ' + esc(c.key_) : ''}${c.due_week ? ' · ' + c.due_week + ' due this week' : ''}</p><p class="rv-src">${srcLine(c.src)}${c.read_at ? ' · read ' + esc(hhmm(c.read_at)) : ''}</p></div>`;
|
|
50
|
+
}
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
function portraitHtml(M) {
|
|
54
|
+
const P = M.portrait, h = P.header;
|
|
55
|
+
return `<div class="ut-card rv-portrait"><div class="rv-head"><span class="rv-av" aria-hidden="true">${esc(h.initials)}</span><div><b>${esc(h.name)}</b><p>${h.line.map(esc).join(' · ')}</p></div></div>`
|
|
56
|
+
+ (P.cells.length ? `<div class="rv-cells">${P.cells.map(cellHtml).join('')}</div>` : '')
|
|
57
|
+
+ `<div class="rv-foot"><span>Wrong somewhere? Say so, or change a line</span><button type="button" class="ut-ib" data-rv="edit" aria-label="Change a line" title="Change a line">${SVG(I.edit, 16, 1.8)}</button></div></div>`;
|
|
58
|
+
}
|
|
59
|
+
function draftHtml(M, S) {
|
|
60
|
+
const d = M.draft;
|
|
61
|
+
if (M.state === 'kept') return `<div class="ob-good">${SVG(I.check, 14, 2.8)}<b>Kept · ${esc(d.title)}</b><span>In Triage${M.kept && M.kept.horizon ? ' · check ' + esc(M.kept.horizon) : ''}. I’m drafting its BRD in its chat.</span></div>`;
|
|
62
|
+
if (M.state === 'later') return ''; // the PM said ✕ to "Draft the first initiative" in setup: no draft now, nothing written
|
|
63
|
+
if (M.state === 'dropped') return '<p class="ob-sub">Dropped. Tell me the first piece of work in your own words, and I draft it from that.</p>';
|
|
64
|
+
const row = (k, v, src) => `<div class="rv-dl"><dt>${k}</dt><dd>${v}${src ? `<em>${srcLine(src)}</em>` : ''}</dd></div>`;
|
|
65
|
+
const outcome = d.outcome ? row('Expected outcome', esc(d.outcome), d.outcome_src)
|
|
66
|
+
: `<div class="rv-dl"><dt>Expected outcome</dt><dd><input class="rv-in" name="rv-outcome" maxlength="300" value="${esc(S.typed)}" placeholder="What should change once it ships?" aria-label="Expected outcome"></dd></div>`;
|
|
67
|
+
return `<div class="ut-card rv-draft" role="group" aria-label="Drafted initiative"><p class="rv-kind">${SVG(I.spark, 13, 2)}<span>Initiative · ${esc(d.stage)} · ${esc(d.workflow)} · draft</span></p>`
|
|
68
|
+
+ `<b class="rv-title">${esc(d.title)}</b><dl>${outcome}${row('Checked', esc(d.horizon_label), d.horizon_src || { label: d.horizon_why })}${row('Owner', esc(d.owner), d.owner_src)}</dl>`
|
|
69
|
+
+ (d.goal_new ? '<p class="ob-sub">The product has no goal yet. Keeping this makes the expected outcome its first goal.</p>' : '')
|
|
70
|
+
+ `<p class="rv-next">${esc(d.next)}</p>`
|
|
71
|
+
+ `<div class="rv-acts"><button type="button" class="ut-ib solid lg" data-rv="keep" aria-label="Keep this initiative" title="Keep this initiative"${S.busy ? ' disabled' : ''}>${SVG(I.check, 18, 2.6)}</button><button type="button" class="ut-ib lg" data-rv="drop" aria-label="Drop this draft" title="Drop this draft"${S.busy ? ' disabled' : ''}>${SVG(I.x, 18, 2.2)}</button></div>`
|
|
72
|
+
+ (S.note ? `<p class="ob-note" role="status">${esc(S.note)}</p>` : '') + '</div>';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The rows at the top of Home's list while the reveal is new (frame ObA2First, left column). Zero is silence: a row only with its data. */
|
|
76
|
+
export function leftHtml(M) {
|
|
77
|
+
if (!M || M.none) return '';
|
|
78
|
+
const L = M.left || {}, rows = [];
|
|
79
|
+
if (L.weekly) rows.push(`<div class="rv-lrow"><b>Your day · ${esc(L.weekly.title)}</b><span>${esc([L.weekly.next, L.weekly.when].filter(Boolean).join(' · '))} · Prepared from the initiative. Open it the evening before.</span></div>`);
|
|
80
|
+
if (L.draft) rows.push(`<button type="button" class="rv-lrow rv-ldraft" data-rv-left="draft"><b>${SVG(I.spark, 12, 2)} ${esc(L.draft.title)}</b><span>Draft · ${esc(L.draft.stage)} · check ${esc(L.draft.check)}</span></button>`);
|
|
81
|
+
if (L.mywork) rows.push(`<div class="rv-lrow"><b>My work</b><span>${L.mywork.mine} ticket${L.mywork.mine === 1 ? '' : 's'} in ${esc(L.mywork.key)} ${L.mywork.mine === 1 ? 'is' : 'are'} yours${L.mywork.due_week ? ' · ' + L.mywork.due_week + ' due this week' : ''}</span></div>`);
|
|
82
|
+
if (!rows.length) return '';
|
|
83
|
+
return `<div class="rv-left" aria-label="Set up just now">${rows.join('')}<p class="rv-lnote">Board, Intelligence and reports appear as there is something to show.</p></div>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function mountReveal(host, { ws, model, send, openIni, changed } = {}) {
|
|
87
|
+
const sec = document.createElement('section');
|
|
88
|
+
sec.className = 'hm-chat ob-chat rv-chat'; sec.id = 'rvChat'; sec.setAttribute('aria-label', 'Your product, set up');
|
|
89
|
+
sec.innerHTML = '<div class="hm-log ob-log" role="log" aria-live="polite"></div><div class="hm-foot"></div>';
|
|
90
|
+
host.after(sec); host.hidden = true; document.body.classList.add('ob-on');
|
|
91
|
+
const log = sec.querySelector('.hm-log'), foot = sec.querySelector('.hm-foot');
|
|
92
|
+
const S = { M: model, busy: false, note: '', typed: '' };
|
|
93
|
+
const turn = (body, mood, latest) => `<div class="hm-a ob-turn">${latest ? `<span class="hm-av hm-adler" aria-hidden="true">${adler(mood, 36, { label: false, id: 'rv', dress: true })}</span>` : '<span class="hm-av t-C s28" aria-hidden="true">C</span>'}<div class="hm-ab"><p class="hm-who">Claude</p>${body}</div></div>`;
|
|
94
|
+
|
|
95
|
+
function draw() {
|
|
96
|
+
const M = S.M, list = [];
|
|
97
|
+
if (M.ticks && M.ticks.length) list.push({ mood: 'handled', body: ticksHtml(M) });
|
|
98
|
+
list.push({ mood: 'handled', body: `<p>${M.portrait.said ? 'You typed one sentence. ' : ''}Here is <b>${esc(M.name)}</b> as I see it.</p>${portraitHtml(M)}` });
|
|
99
|
+
if (M.state !== 'later') list.push({ mood: M.state === 'open' ? 'needs-you' : 'handled', body: `${M.state === 'open' ? '<p>I drafted the first initiative from it. Nothing is final until you keep it.</p>' : ''}${draftHtml(M, S)}` });
|
|
100
|
+
if (M.quiet) list.push({ mood: 'handled', body: `<p class="ob-sub">${esc(M.quiet)}</p>` });
|
|
101
|
+
log.innerHTML = list.map((t, i) => turn(t.body, t.mood, i === list.length - 1)).join('');
|
|
102
|
+
if (!foot.firstChild) {
|
|
103
|
+
foot.innerHTML = `<form class="hm-box" data-rv-form="say"><textarea rows="1" aria-label="Message Claude" placeholder="Message Claude"></textarea><div class="hm-tb"><span class="sp"></span><span class="hm-key">Enter sends · Shift+Enter for a new line</span><button type="submit" class="hm-send" aria-label="Send">${SVG(I.up, 18, 2.4)}</button></div></form>`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function act(a) {
|
|
107
|
+
if (S.busy) return;
|
|
108
|
+
const inp = log.querySelector('input[name="rv-outcome"]'); if (inp) S.typed = inp.value;
|
|
109
|
+
if (a === 'edit') { const ta = foot.querySelector('textarea'); ta.value = 'In the portrait, change '; ta.focus(); return; }
|
|
110
|
+
S.busy = true; S.note = ''; draw();
|
|
111
|
+
const r = await pmPost('/api/first-reveal/' + a, a === 'keep' ? { ws, outcome: S.typed } : { ws }).catch(() => null);
|
|
112
|
+
S.busy = false;
|
|
113
|
+
if (!r || !r.ok) { S.note = (r && r.error) || 'PMOS did not answer. Try again.'; draw(); return; }
|
|
114
|
+
S.M = { ...S.M, state: a === 'keep' ? 'kept' : 'dropped', kept: a === 'keep' ? r : null, left: { ...S.M.left, draft: null } };
|
|
115
|
+
draw();
|
|
116
|
+
if (changed) changed(S.M);
|
|
117
|
+
if (a === 'keep' && r.id && openIni) openIni(r.id, S.M.brd);
|
|
118
|
+
}
|
|
119
|
+
sec.addEventListener('click', (e) => { const b = e.target.closest('[data-rv]'); if (b && !b.disabled) act(b.dataset.rv); });
|
|
120
|
+
sec.addEventListener('input', (e) => { if (e.target.name === 'rv-outcome') S.typed = e.target.value; });
|
|
121
|
+
sec.addEventListener('submit', (e) => {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
const ta = e.target.querySelector('textarea'); const v = ta && ta.value.trim(); if (!v) return;
|
|
124
|
+
close(); if (send) send(v);
|
|
125
|
+
});
|
|
126
|
+
sec.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && e.target.matches('textarea')) { e.preventDefault(); e.target.closest('form').requestSubmit(); } });
|
|
127
|
+
function close() { sec.remove(); host.hidden = false; document.body.classList.remove('ob-on'); }
|
|
128
|
+
draw();
|
|
129
|
+
return { el: sec, close, show: () => { const c = log.querySelector('.rv-draft'); if (c) c.scrollIntoView({ block: 'center' }); } };
|
|
130
|
+
}
|
|
@@ -18,6 +18,7 @@ import './fetch-share.js'; // first: identical GETs in flight are asked once (
|
|
|
18
18
|
import { mountNav } from './cozy-nav.js';
|
|
19
19
|
import { mountSetupChecklist } from './setup-checklist.js';
|
|
20
20
|
import { mountOnboard } from './cozy-onboard.js';
|
|
21
|
+
import { mountReveal, leftHtml as revealLeft } from './cozy-reveal.js';
|
|
21
22
|
import { mountLivePill } from './cozy-live-pill.js';
|
|
22
23
|
import { mountPanels } from './cozy-panels.js';
|
|
23
24
|
import { ownTab, bring, tabUrl } from './rec-tab-core.js';
|
|
@@ -59,6 +60,7 @@ import * as SO from './sort-order.js'; // the sort button and ONE order with t
|
|
|
59
60
|
import * as PO from './prog-order.js'; // a program's order on Home: the grip, hold-then-drag on a phone, ✦ Claude's order from the dates (canvas RoAPhone)
|
|
60
61
|
import * as IMP from './home-impact.js'; // Your impact: the weekly check in Home's own chat (batch 16 item 7, GmP1Ledger)
|
|
61
62
|
import * as DRIFT from './home-drift.js'; // What you might have missed: the weekly drift ✦ in Home's own chat (weekly-drift-check)
|
|
63
|
+
import * as MOM from './home-moment.js'; // a feature appears when its moment comes: the day's ✦ in Home's own chat (onboarding B3, ObA4Gradual)
|
|
62
64
|
import * as LPV from './loose-props.js'; // Claude's clean-up of "Not in an initiative": the ✦ on its header and the Viewer (batch 13 item 3)
|
|
63
65
|
import * as IM from './impact.js';
|
|
64
66
|
import { mountPull } from './cozy-pull.js'; // pull to refresh on Your day, Adler peeking up (batch 17 item 4-B, AdP2) // Your impact (batch 16 item 7): Adler's sprout on the greeting, the week in the Viewer, the Friday chapter
|
|
@@ -216,6 +218,7 @@ async function load() {
|
|
|
216
218
|
NEED.refresh(); // an open ask: its agent finished, or it left Home while it was open
|
|
217
219
|
loadImpact();
|
|
218
220
|
loadDrift();
|
|
221
|
+
loadMoment();
|
|
219
222
|
readImpact();
|
|
220
223
|
loadGrillNews();
|
|
221
224
|
// the boards of the other products Home reads, each its own closed group: read after the list is drawn, so a slow one never holds it up
|
|
@@ -349,7 +352,7 @@ function homeListHtml() {
|
|
|
349
352
|
S.lo = { sel, ses: S.ses, restOpen: (k) => S.rest.has(k), pin: S.pin, phone: false }; // the folded rail's card draws Home's rows with these
|
|
350
353
|
return TD.listHtml(M, todayModel(), {
|
|
351
354
|
sel, selHead: S.head, ses: S.ses, pOpen, here: isPhone() ? null : TD.groupOfOpen(M, sel) || S.head, restOpen: (k) => S.rest.has(k), cap: S.cap, pin: S.pin, avHtml, ready: S.ready, err: S.D.err.length ? S.D.err[0] : '',
|
|
352
|
-
pick: S.D.pick, tones: pickTones(), review: R, mic: canDictate(), phone: isPhone(), day: S.day, news: S.news && S.news.ok ? S.news.count : 0, top: S.ready ? RECROW.html() + MEN.rowHtml(S.D, { sel: S.sel }) + (S.impact ? IMP.rowHtml(S.impact.c, { esc, sel: S.sel === 'impact' }) : '') + (S.drift ? DRIFT.rowHtml(S.drift.c, { esc, sel: S.sel === 'drift' }) : '') : S.menIn ? '<div class="ls-stale" inert>' + LS.inert(MEN.rowHtml(S.D, {})) + '</div>' : LS.menShape(), // Mentions on its own clock, seen but not pressed until Home is fresh (SpG)
|
|
355
|
+
pick: S.D.pick, tones: pickTones(), review: R, mic: canDictate(), phone: isPhone(), day: S.day, news: S.news && S.news.ok ? S.news.count : 0, top: S.ready ? revealLeft(S.reveal) + RECROW.html() + MEN.rowHtml(S.D, { sel: S.sel }) + (S.impact ? IMP.rowHtml(S.impact.c, { esc, sel: S.sel === 'impact' }) : '') + (S.drift ? DRIFT.rowHtml(S.drift.c, { esc, sel: S.sel === 'drift' }) : '') : S.menIn ? '<div class="ls-stale" inert>' + LS.inert(MEN.rowHtml(S.D, {})) + '</div>' : LS.menShape(), // Mentions on its own clock, seen but not pressed until Home is fresh (SpG)
|
|
353
356
|
last: S.ready ? '' : LS.last(LAST_KEY), keep: S.ready && S.filter === 'today' && !S.ini ? (b) => LS.keepLast(LAST_KEY, b) : null,
|
|
354
357
|
imFace: IM.faceHtml(S.im, { joy: S.imJoy }), grill: S.ready ? grillNews() : null,
|
|
355
358
|
});
|
|
@@ -775,7 +778,30 @@ function turnEnded() {
|
|
|
775
778
|
* finished project's next steps on ?onboarded=1, and opens by itself on a first run: setup not finished and no registered folder holds an
|
|
776
779
|
* initiative. A named workspace does not stop it: the installer opens its own empty ~/PMOS folder by name (bug 2026-11-02, Windows). */
|
|
777
780
|
const startOnboard = (done) => { document.body.classList.add('ob-on'); mountOnboard($('#hmChat'), { ws: WS, done }); };
|
|
778
|
-
|
|
781
|
+
/* onboarding B2 (cozy-reveal.js): right after setup, Home's chat shows what setup did and read, the product portrait and the drafted first
|
|
782
|
+
* initiative, and the list shows the draft, the weekly and the PM's own tickets at its top. It opens on ?onboarded=1, and again on a later
|
|
783
|
+
* visit while the draft waits for ✓ or ✕. With nothing to reveal it falls back to the old next steps. */
|
|
784
|
+
let RV = null;
|
|
785
|
+
function startReveal(M) {
|
|
786
|
+
S.reveal = M; drawList();
|
|
787
|
+
RV = mountReveal($('#hmChat'), {
|
|
788
|
+
ws: WS, model: M,
|
|
789
|
+
send: (t) => { RV = null; chat.send(t); },
|
|
790
|
+
openIni: (id, prompt) => { RV && RV.close(); RV = null; load().then(() => goIni(id)).then(() => { if (prompt) chat.send(prompt); }); },
|
|
791
|
+
changed: (m) => { S.reveal = m; drawList(); },
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
$('#hmList').addEventListener('click', (e) => { if (e.target.closest('[data-rv-left="draft"]')) { if (RV) RV.show(); else if (S.reveal) startReveal(S.reveal); } });
|
|
795
|
+
if (P.get('onboard') === '1') startOnboard(false);
|
|
796
|
+
else if (P.get('onboarded') === '1' || WS) {
|
|
797
|
+
pmGet('/api/first-reveal?ws=' + encodeURIComponent(WS)).then((r) => {
|
|
798
|
+
// setup's own ✦ "Draft the first initiative" (onboarding B1) sends &first=draft on ✓ and &first=none on ✕: with none, no draft is shown
|
|
799
|
+
if (r && r.ok && !r.none && P.get('onboarded') === '1' && P.get('first') === 'none' && r.state === 'open') startReveal({ ...r, state: 'later', left: { ...r.left, draft: null } });
|
|
800
|
+
else if (r && r.ok && !r.none && (P.get('onboarded') === '1' || r.state === 'open')) startReveal(r);
|
|
801
|
+
else if (P.get('onboarded') === '1') startOnboard(true);
|
|
802
|
+
else pmGet('/api/onboard/state').then((x) => { if (x && x.ok && !x.has_projects) startOnboard(false); }).catch(() => {});
|
|
803
|
+
}).catch(() => { if (P.get('onboarded') === '1') startOnboard(true); });
|
|
804
|
+
}
|
|
779
805
|
else pmGet('/api/onboard/state').then((r) => { if (r && r.ok && !r.has_projects) startOnboard(false); }).catch(() => {});
|
|
780
806
|
|
|
781
807
|
/* The chat's header line on a desktop (HvACDesk): the open row's mark and title, and where it sits: its initiative with the stage track, or
|
|
@@ -1076,7 +1102,7 @@ function programTarget(id, task) {
|
|
|
1076
1102
|
function homeTarget() {
|
|
1077
1103
|
return { key: 'chat:main', ws: WS || undefined, title: 'Home', scope: { text: 'All work', icon: I.home }, rich: false, placeholder: 'Message Claude', role: CLAUDE,
|
|
1078
1104
|
empty: 'Ask anything about your work. Claude reads your repositories and works in a real Claude Code session.', also: alsoOf(null),
|
|
1079
|
-
intro: S.sel === 'drift' && S.drift ? S.drift.intro : S.impact ? S.impact.intro : S.drift ? S.drift.intro : undefined };
|
|
1105
|
+
intro: S.sel === 'drift' && S.drift ? S.drift.intro : S.impact ? S.impact.intro : S.drift ? S.drift.intro : S.moment ? S.moment.intro : undefined };
|
|
1080
1106
|
}
|
|
1081
1107
|
/* Your impact (batch 16 item 7; batch 23 item 4, IcA): while last week's check is open, Home's own chat opens on Claude's thank-you card.
|
|
1082
1108
|
* Off in Settings, or nothing to check: no card. Read after the list is drawn, so it never holds Home up. */
|
|
@@ -1097,6 +1123,15 @@ async function loadDrift() {
|
|
|
1097
1123
|
if ((was || S.drift) && cur && cur.key === 'chat:main') { chat.open(homeTarget()); }
|
|
1098
1124
|
if (was || S.drift) drawList();
|
|
1099
1125
|
}
|
|
1126
|
+
/* The day's moment (onboarding B3, ObA4Gradual): one ✦ when a place's moment comes (three initiatives, a goal kept, a week of movement). It
|
|
1127
|
+
* waits behind Your impact and the drift check, so Home's chat holds one ✦ at a time. ✕ drops it for good; ✓ opens the place. */
|
|
1128
|
+
async function loadMoment() {
|
|
1129
|
+
const m = await MOM.readMoment(pmGet).catch(() => null);
|
|
1130
|
+
const was = !!S.moment;
|
|
1131
|
+
S.moment = m ? { m, intro: MOM.momentIntro(m, { esc, pmPost, guide: (pg) => import('./premium-vw-open.js').then((V) => V.openGuide(pg)), done: () => { S.moment = null; const c = chat.current && chat.current(); if (c && c.key === 'chat:main') chat.open(homeTarget()); } }) } : null;
|
|
1132
|
+
const cur = chat.current && chat.current();
|
|
1133
|
+
if ((was || S.moment) && cur && cur.key === 'chat:main') { chat.open(homeTarget()); }
|
|
1134
|
+
}
|
|
1100
1135
|
// the switch in Settings › General (pm/cozy-settings.js) turns the check off and on at once
|
|
1101
1136
|
window.addEventListener('pmos:impact', () => { loadImpact(); readImpact(); }); // the switch: the weekly check row and Adler's sprout follow at once
|
|
1102
1137
|
function openTarget(t) {
|
|
@@ -24,3 +24,15 @@
|
|
|
24
24
|
.im-msg { margin: 4px 0 0; font-size: var(--fs-meta); color: var(--cz-ink3); line-height: var(--lh-meta); }
|
|
25
25
|
.im-msg:empty { display: none; }
|
|
26
26
|
@media (prefers-reduced-motion: no-preference) { .im-q { transition: background-color .2s ease; } }
|
|
27
|
+
/* the day's moment (pm/home-moment.js, onboarding B3 ObA4Gradual): one ✦, the Guide page inline, ✓ opens the place, ✕ never again */
|
|
28
|
+
.mo-card { clear: both; margin-top: 10px; max-width: 600px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 12px 12px 12px 16px; border-radius: 18px; background: var(--cz-bluebg); }
|
|
29
|
+
.mo-tx { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
30
|
+
.mo-w { font-size: var(--fs-body); color: var(--cz-ink2); line-height: var(--lh-body); overflow-wrap: anywhere; }
|
|
31
|
+
.mo-guide { align-self: flex-start; display: inline-flex; align-items: center; gap: 5px; margin-top: 2px; padding: 0; border: 0; background: none; color: var(--cz-blueink); font: inherit; font-size: var(--fs-body); font-weight: var(--fw-strong); cursor: pointer; }
|
|
32
|
+
.mo-guide:hover { text-decoration: underline; }
|
|
33
|
+
.mo-ok, .mo-x { flex: none; width: 36px; height: 36px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; padding: 0; cursor: pointer; }
|
|
34
|
+
.mo-ok { border: 0; background: var(--cz-ink); color: var(--cz-card); }
|
|
35
|
+
.mo-x { border: 1px solid var(--cz-hair2, var(--cz-hair)); background: var(--cz-card); color: var(--cz-ink2); }
|
|
36
|
+
.mo-ok:disabled, .mo-x:disabled { opacity: .5; cursor: default; }
|
|
37
|
+
.mo-card .im-msg { flex-basis: 100%; margin: 0; }
|
|
38
|
+
.mo-card .im-msg:empty { display: none; }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* pm/home-moment.js — a feature appears when its moment comes: the day's ✦ in Home's own chat (onboarding build B3, design ObA4Gradual).
|
|
2
|
+
* The rules are lib/moments.js; the routes lib/routes-moments.js.
|
|
3
|
+
*
|
|
4
|
+
* readMoment(pmGet) → the day's moment, or null (none due, a workspace that was in use before, or answered today)
|
|
5
|
+
* momentIntro(m, { esc, pmPost, guide, done }) → the intro of Home's chat: { role, mood, text, card: { html, press } }
|
|
6
|
+
* The card: Claude's line, one line of what was done or where it leads, the Guide page inline ("How the Board works"), then two icons:
|
|
7
|
+
* ✓ opens the place (it is kept), ✕ drops it, and it never comes back. The first draw tells the server it was shown (that day's moment).
|
|
8
|
+
* guide(page) opens the guide page in the Viewer; done() is called after ✕, so the host can redraw Home's chat without the card. */
|
|
9
|
+
|
|
10
|
+
const SV = (d, w = 18) => `<svg aria-hidden="true" width="${w}" height="${w}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
|
|
11
|
+
const P = { ok: 'M5 12.5l4.5 4.5L19 7.5', no: 'M6 6l12 12M18 6L6 18', guide: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M9.5 9a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6M12 17h.01' };
|
|
12
|
+
|
|
13
|
+
export async function readMoment(pmGet) {
|
|
14
|
+
const r = await pmGet('/api/moments');
|
|
15
|
+
return r && r.ok && r.moment && r.moment.id ? r.moment : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function momentIntro(m, { esc, pmPost, guide, done }) {
|
|
19
|
+
const st = { told: !!m.shown, msg: '' };
|
|
20
|
+
const html = () => {
|
|
21
|
+
if (!st.told) { st.told = true; Promise.resolve(pmPost('/api/moments/shown', { ws: m.ws, id: m.id })).catch(() => {}); }
|
|
22
|
+
return `<div class="mo-card" role="group" aria-label="${esc(m.text)}">`
|
|
23
|
+
+ `<div class="mo-tx">${m.sub ? `<span class="mo-w">${esc(m.sub)}</span>` : ''}${m.product ? `<span class="mo-w">${esc(m.product)}</span>` : ''}`
|
|
24
|
+
+ `<button type="button" class="mo-guide" data-mo-guide>${SV(P.guide, 14)}${esc(m.guideLabel)}</button></div>`
|
|
25
|
+
+ `<button type="button" class="mo-ok" data-mo-keep aria-label="${esc(m.go)}" title="${esc(m.go)}">${SV(P.ok)}</button>`
|
|
26
|
+
+ `<button type="button" class="mo-x" data-mo-drop aria-label="Not now, and not again" title="Not now, and not again">${SV(P.no, 16)}</button>`
|
|
27
|
+
+ `<p class="im-msg" role="status">${esc(st.msg)}</p></div>`;
|
|
28
|
+
};
|
|
29
|
+
const press = async (b) => {
|
|
30
|
+
const d = b.dataset;
|
|
31
|
+
if ('moGuide' in d) { guide(m.guide); return false; }
|
|
32
|
+
if (!('moKeep' in d) && !('moDrop' in d)) return false;
|
|
33
|
+
b.disabled = true;
|
|
34
|
+
const r = await pmPost('/api/moments/' + ('moKeep' in d ? 'keep' : 'drop'), { ws: m.ws, id: m.id });
|
|
35
|
+
if (!r || !r.ok) { b.disabled = false; st.msg = (r && r.error) || 'Not saved.'; return true; }
|
|
36
|
+
if ('moKeep' in d) { location.href = m.href; return false; }
|
|
37
|
+
done();
|
|
38
|
+
return false;
|
|
39
|
+
};
|
|
40
|
+
return { role: { key: 'claude', name: 'Claude', initial: 'C' }, mood: 'handled', text: '✦ ' + m.text, card: { html, press } };
|
|
41
|
+
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* Simulated tag on Voices and What-if; ✓ adds the next step as a task, ✎ lets the PM word it first, ✕ drops it. 3 show,
|
|
12
12
|
* "+N smaller findings" unfolds the rest. Over them, quietly: the angles Claude chose, when it ran, and what it cost.
|
|
13
13
|
* On a phone the findings are cards side by side, one per screen.
|
|
14
|
+
* The idle lab (canvas I2 IlB): Claude prototyped it, Claude improved it, Claude scanned for it, drawn by
|
|
15
|
+
* pm/cozy-lab.js cardHtml; ⤢ opens the compare view (I3), ✕ asks why first, ✓ keeps it as a document.
|
|
14
16
|
* lookHtml(v) the Tasks' dashed placeholder while Claude looks and nothing is kept yet (PiA1)
|
|
15
17
|
* foundDocs(v) the documents a ✓ kept from elsewhere, for the Documents fold: [{ title, meta, ref }]
|
|
16
18
|
*
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
import { pmGet, pmPost } from './http.js';
|
|
19
21
|
import { taskTitle } from './ini-links.js';
|
|
20
22
|
import * as GW from './grill-words.js'; // Grill findings: the angle, its mark, the next step, the Simulated tag (canvas GrillResult)
|
|
23
|
+
import * as LC from './cozy-lab.js'; // the idle lab (canvas I2 IlB, I3 IlCompare): its three ✦ cards and the compare view they open
|
|
21
24
|
|
|
22
25
|
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
23
26
|
const SVG = (d, w = 16, sw = 1.8) => `<svg aria-hidden="true" width="${w}" height="${w}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
|
|
@@ -36,7 +39,7 @@ const md = (iso) => { const m = DAY.exec(String(iso || '')); return m ? MON[+m[2
|
|
|
36
39
|
const span = (a, b) => (a && b && a !== b ? md(a) + ' – ' + md(b) : md(a || b));
|
|
37
40
|
const plural = (n, a, b) => n + ' ' + (n === 1 ? a : b);
|
|
38
41
|
const clip = (s, n) => { s = String(s || ''); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
|
|
39
|
-
const GROUP = { description: 'Description', outcome: 'Expected outcome', found: 'Found elsewhere', dates: 'Dates', status: 'Looks done', names: 'Names', links: 'Links', grill: 'Claude grilled it' };
|
|
42
|
+
const GROUP = { description: 'Description', outcome: 'Expected outcome', prototyped: 'Claude prototyped it', improved: 'Claude improved it', scanned: 'Claude scanned for it', found: 'Found elsewhere', dates: 'Dates', status: 'Looks done', names: 'Names', links: 'Links', grill: 'Claude grilled it' };
|
|
40
43
|
|
|
41
44
|
/* ── the chip and the live look ── */
|
|
42
45
|
export function chipOf(p) {
|
|
@@ -87,7 +90,7 @@ export function foundDocs(v) {
|
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
/* ── the Viewer ── */
|
|
90
|
-
const S = { id: null, props: null, open: new Set(), choices: {}, H: null, title: '', timer: null, edit: null, all: false, run: null };
|
|
93
|
+
const S = { id: null, props: null, open: new Set(), choices: {}, H: null, title: '', timer: null, edit: null, all: false, run: null, why: null };
|
|
91
94
|
const titleOf = (p) => {
|
|
92
95
|
if (p.title) return p.title;
|
|
93
96
|
const n = p.items.length;
|
|
@@ -143,6 +146,7 @@ function grillSec(g) {
|
|
|
143
146
|
}
|
|
144
147
|
function propHtml(p) {
|
|
145
148
|
if (p.group === 'grill') return grillHtml(p);
|
|
149
|
+
if (LC.isLab(p)) return LC.cardHtml(p, { why: S.why === p.id });
|
|
146
150
|
if (p.group === 'links') {
|
|
147
151
|
const l = p.link || {}, a = taskTitle(p.ini || S.id, l.from), b = taskTitle(p.ini || S.id, l.to);
|
|
148
152
|
const t = l.kind === 'link' ? clip(b, 40) + ' waits on ' + clip(a, 40) : l.kind === 'pull' ? 'Pull ' + plural((l.tasks || []).length, 'task', 'tasks') + ' in' : 'Move ' + plural((l.tasks || []).length, 'task', 'tasks') + ' to keep your links';
|
|
@@ -208,11 +212,12 @@ async function keep(ids, all) {
|
|
|
208
212
|
});
|
|
209
213
|
if (!S.props || (!S.props.n && !(S.props.look && !S.props.look.ended))) window.czViewer.back();
|
|
210
214
|
}
|
|
211
|
-
async function drop(sig) {
|
|
212
|
-
const res = await write(S.H, '/api/proposals/drop', { initiative: S.id, sig });
|
|
215
|
+
async function drop(sig, why) {
|
|
216
|
+
const res = await write(S.H, '/api/proposals/drop', { initiative: S.id, sig, ...(why ? { why } : {}) });
|
|
213
217
|
if (!res || res.ok === false) { say((res && res.error) || 'Nothing was changed.'); return; }
|
|
214
218
|
await fresh(); after();
|
|
215
219
|
}
|
|
220
|
+
const labProp = (id) => (S.props && S.props.groups || []).flatMap((g) => g.props).find((x) => x.id === id) || null;
|
|
216
221
|
async function onPress(e) {
|
|
217
222
|
const b = e.target.closest('button'); if (!b) return;
|
|
218
223
|
const d = b.dataset;
|
|
@@ -221,6 +226,10 @@ async function onPress(e) {
|
|
|
221
226
|
if (d.ppEdit) { const i = b.closest('.pp-p') && b.closest('.pp-p').querySelector('[data-pp-ned]'); if (i && i.value.trim()) S.choices[d.ppEdit] = i.value.trim(); S.edit = S.edit === d.ppEdit ? null : d.ppEdit; draw(); const n = document.getElementById('ppNed'); if (n) n.focus(); return; }
|
|
222
227
|
if ('ppGall' in d) { S.all = true; b.disabled = true; await fresh(); return; }
|
|
223
228
|
if (d.ppKeep) { const i = document.querySelector(`[data-pp-ned="${CSS.escape(d.ppKeep)}"]`); if (i && i.value.trim()) S.choices[d.ppKeep] = i.value.trim(); b.disabled = true; S.edit = null; await keep([d.ppKeep], false); return; }
|
|
229
|
+
// the idle lab's ✕ asks why first (Claude learns from it); the reason is optional, and ✕ again drops it
|
|
230
|
+
if (d.labWhy) { S.why = S.why === d.labWhy ? null : d.labWhy; draw(); const i = document.querySelector(`[data-lab-reason="${CSS.escape(d.labWhy)}"]`); if (i) i.focus(); return; }
|
|
231
|
+
if (d.labDrop) { const i = document.querySelector(`[data-lab-reason="${CSS.escape(d.labDrop)}"]`), p = labProp(d.labDrop); if (!p) return; b.disabled = true; S.why = null; await drop(p.sig, i ? i.value.trim() : ''); return; }
|
|
232
|
+
if (d.labOpen) { const p = labProp(d.labOpen); if (p) LC.openCompare({ where: S.H && S.H.where ? S.H.where() : {}, id: p.id, key: d.labKey || '', H: { toast: say, reload: async () => { await fresh(); after(); } } }); return; }
|
|
224
233
|
if (d.ppDrop) { b.disabled = true; await drop(d.ppDrop); return; }
|
|
225
234
|
if (d.ppDropit) { b.disabled = true; await drop(d.ppDropit); return; }
|
|
226
235
|
if ('ppDroplk' in d) {
|
|
@@ -245,12 +254,12 @@ async function onPress(e) {
|
|
|
245
254
|
/** The ✦'s press: the drill-in. v: the initiative's view (id, title, props). */
|
|
246
255
|
export function open(v, H) {
|
|
247
256
|
const VW = window.czViewer; if (!VW || !v || !v.id) return;
|
|
248
|
-
S.id = v.id; S.props = v.props; S.H = H; S.open = new Set(); S.choices = {}; S.title = v.title; S.edit = null; S.all = false; S.run = null;
|
|
257
|
+
S.id = v.id; S.props = v.props; S.H = H; S.open = new Set(); S.choices = {}; S.title = v.title; S.edit = null; S.all = false; S.run = null; S.why = null;
|
|
249
258
|
if (v.props && (v.props.groups || []).some((g) => g.group === 'grill')) readRun(H, v.id).then((r) => { if (S.id === v.id && r) { S.run = r; draw(); } });
|
|
250
259
|
VW.drill();
|
|
251
260
|
VW.open({ id: 'pp:' + v.id, kind: 'Claude proposes', title: 'Claude proposes' + (v.props && v.props.n ? ' ' + v.props.n : ''), sub: v.title, icon: I.spark,
|
|
252
261
|
presses: presses(v.props),
|
|
253
262
|
onPress: (act) => { if (act === 'all') keep([], true); },
|
|
254
|
-
render: (b) => { b.innerHTML = bodyHtml(S.props); if (!b.__pp) { b.__pp = true; } b.onclick = onPress; b.onkeydown = (e) => { if (e.key !== 'Enter' || !e.target.matches('[data-pp-ned]')) return; e.preventDefault(); const k = e.target.closest('.pp-p').querySelector('[data-pp-keep]'); if (k) k.click(); }; draw(); if (!S.props || S.props.look) fresh(); },
|
|
263
|
+
render: (b) => { b.innerHTML = bodyHtml(S.props); if (!b.__pp) { b.__pp = true; } b.onclick = onPress; b.onkeydown = (e) => { if (e.key === 'Enter' && e.target.matches('[data-lab-reason]')) { e.preventDefault(); const x = e.target.closest('.pp-p').querySelector('[data-lab-drop]'); if (x) x.click(); return; } if (e.key !== 'Enter' || !e.target.matches('[data-pp-ned]')) return; e.preventDefault(); const k = e.target.closest('.pp-p').querySelector('[data-pp-keep]'); if (k) k.click(); }; draw(); if (!S.props || S.props.look) fresh(); },
|
|
255
264
|
onClose: () => { clearTimeout(S.timer); if (S.id === v.id) S.id = null; } });
|
|
256
265
|
}
|
|
@@ -23,6 +23,7 @@ import { followDoc } from './doc-follow.js';
|
|
|
23
23
|
import { $, $$, esc, store, chatAbout, setPageGaps } from './premium-shell.js';
|
|
24
24
|
import * as PK from './cozy-pick.js'; // the one product picker Home, Board and Insights share (batch 12 item 5)
|
|
25
25
|
import { mountNav } from './cozy-nav.js';
|
|
26
|
+
import { quietPlace } from './quiet-place.js'; // onboarding B3: before three initiatives or a date, the Board shows one line
|
|
26
27
|
import { mountLivePill } from './cozy-live-pill.js';
|
|
27
28
|
import { stepsHelpHtml } from './bd-stages.js';
|
|
28
29
|
import { msMode } from './bd-pins.js';
|
|
@@ -90,6 +91,7 @@ let byProduct = store.get(GROUPING_KEY) === 'product'; // the grouping: progra
|
|
|
90
91
|
let DM = {}; // the documents' history (GET /api/board-docs): { [path]: { changed, by, log } }
|
|
91
92
|
let peek = null; // what the Viewer shows for the Board: { kind: 'ini', key } an initiative or its ticket, { kind: 'acts' } an acts panel
|
|
92
93
|
mountNav('board');
|
|
94
|
+
quietPlace('board', document.getElementById('stage'));
|
|
93
95
|
mountLivePill();
|
|
94
96
|
/* The page is the first panel (panels on Board and Intelligence, canvas BiB1Split): a Viewer opens beside it, behind the divider. A row's
|
|
95
97
|
* name drags beside, or ⌘/Ctrl-click opens it beside (a By stage card's drag moves the stage, so it is no source). fit(): the width the
|
|
@@ -14,7 +14,7 @@ function toolState() {
|
|
|
14
14
|
}
|
|
15
15
|
export function mountFirst() {
|
|
16
16
|
const q = new URLSearchParams(location.search), id = q.get('first'), ws = q.get('ws') || '';
|
|
17
|
-
if (!id) return null;
|
|
17
|
+
if (!id || q.get('onboarded') === '1') return null; // onboarding's own first=draft|none is the reveal's, not an import id
|
|
18
18
|
const sec = document.createElement('section');
|
|
19
19
|
sec.className = 'hm-first hx hm-tc'; sec.id = 'wlFirst'; sec.setAttribute('aria-labelledby', 'hWl');
|
|
20
20
|
sec.innerHTML = '<h2 id="hWl" class="ut-h2">Your first Briefing</h2><p class="ut-small ut-mut">Reading what the scan found.</p>';
|
|
@@ -35,6 +35,7 @@ import * as GW from './grill-words.js'; // Grill and Forecast screens (canvas
|
|
|
35
35
|
import * as PK from './cozy-pick.js'; // the one product picker Home, Board and Insights share (batch 12 item 5)
|
|
36
36
|
import { openReport } from './premium-report-read.js';
|
|
37
37
|
import { mountNav } from './cozy-nav.js';
|
|
38
|
+
import { quietPlace, quietLine } from './quiet-place.js'; // onboarding B3: before a goal is kept Intelligence shows one line; reports before a week of movement
|
|
38
39
|
import { mountLivePill } from './cozy-live-pill.js';
|
|
39
40
|
import { mountPanels } from './cozy-panels.js';
|
|
40
41
|
import * as IM from './impact.js'; // Your impact's ✦ line (batch 16 item 7): a key result your work moved
|
|
@@ -45,6 +46,7 @@ import * as TRV from './trends-review.js'; // Delivery trends' words (batch 22
|
|
|
45
46
|
import * as OK from './okr-list.js'; // Claude runs the OKRs (batch 16 item 6, 6-B): the goal list, its drafts, the ✦ line, ⋯ › Metrics, the notify choice
|
|
46
47
|
import * as IP2 from './ins-pane.js'; // Intelligence as the first panel (panels on Board and Intelligence, P-B): the narrow form, Moved by, the open row
|
|
47
48
|
import * as GC from './goals-clear.js';
|
|
49
|
+
import * as LAB from './cozy-lab-ins.js'; // Intelligence › Lab (idle lab B+C, canvas I1 IlBC): what fits no initiative, the week's background record, next time
|
|
48
50
|
import * as FCV from './goal-forecast.js'; // a goal's forecast in its Viewer (FcViewer, FcPhone): the state line, the range band, the baseline ✓ // goals made clear (batch 18 item 11, 11-B): the drafts table, the check panel, first readings, failed and held
|
|
49
51
|
|
|
50
52
|
let M = null; // the model
|
|
@@ -62,6 +64,7 @@ let loadedAt = 0;
|
|
|
62
64
|
let selecting = false; let picked = []; // New dashboard: the goals picked in the table, in order
|
|
63
65
|
wireRail('rep');
|
|
64
66
|
mountNav('insights');
|
|
67
|
+
quietPlace('insights', document.getElementById('stage'));
|
|
65
68
|
mountLivePill();
|
|
66
69
|
// the page is the first panel (canvas BiI1Split): a goal row drags beside or opens beside on ⌘/Ctrl-click; the table needs about 880 px whole
|
|
67
70
|
mountPanels({ page: document.getElementById('stage'), key: 'intel', sources: IP2.SOURCES, fit: IP2.fit });
|
|
@@ -2079,7 +2082,7 @@ function drawList() {
|
|
|
2079
2082
|
const nNeed = waits.length + waitAsks.length;
|
|
2080
2083
|
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>` : '';
|
|
2081
2084
|
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>`
|
|
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>' : '';
|
|
2085
|
+
: !empty && !needs ? (quietLine('reports') ? `<div class="ins-empty"><b>${esc(quietLine('reports'))}</b></div>` : '<div class="ins-empty"><b>No report is set up and no report file is on record.</b> Reports are made in Home.</div>') : '';
|
|
2083
2086
|
const dd = savedDash().length ? null : draftedDash();
|
|
2084
2087
|
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
2088
|
$('#repBody').innerHTML = `${needs}${oneNote()}${libFolds(LIB.L) || '<div id="readyLib"></div>'}<div id="listRows" aria-label="Dashboards and reports">${fbox}${empty}${table}${dashBlock}</div>`;
|
|
@@ -2340,8 +2343,10 @@ async function load() {
|
|
|
2340
2343
|
if (first) { const t = new URLSearchParams(location.search).get('tab'); if (t === 'outcomes' || t === 'library') setTab(t); }
|
|
2341
2344
|
if (first) draftsLoad();
|
|
2342
2345
|
if (!PF) fcGrillLoad().then(() => { if (M && FCG) drawStrip(); }).catch(() => {});
|
|
2346
|
+
if (!PF) LAB.load(); else { const el = $('#pLab'); if (el) el.hidden = true; }
|
|
2343
2347
|
if (!PF) { await noteOpen(); RI.loadProposals(M.board.ws, M.board.id).then(() => { if (M) drawStrip(); }); IM.readWeek().then(() => { if (M && IM.intelItem()) drawStrip(); }).catch(() => {}); }
|
|
2344
2348
|
}
|
|
2349
|
+
LAB.mount({ el: $('#pLab'), where: () => (M ? { ws: M.board.ws, board: M.board.id } : {}), iniHref, toast: (m, undo) => toast(m, undo ? { label: 'Undo', fn: undo } : undefined) });
|
|
2345
2350
|
/* a report from Intelligence (batch 15 item 4): the view on screen is its first scope; a saved dashboard is its own */
|
|
2346
2351
|
RI.mount({ model: () => (PF ? null : M), scope: () => { const D = curDash(); return D && D.saved ? 'dashboard:' + D.id : 'view'; }, viewer, close: () => closeMenus(), reload: () => load(),
|
|
2347
2352
|
openFile: (setup, p) => { const sl = (/(\d{4}-\d{2}-\d{2})\.md$/.exec(p) || [])[1]; openReport({ cadence: setup, slot: sl, path: p, state: 'draft' }, { ws: M.board.ws, onIssue: () => load(), onChange: () => load() }); },
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* pm/quiet-place.js — a place whose moment has not come shows one honest line instead of empty tables (onboarding build B3, ObA4Gradual;
|
|
2
|
+
* rules in lib/moments.js). Board: "The Board starts at three initiatives, or a date." Intelligence: "Intelligence starts when you keep a goal."
|
|
3
|
+
* Reports: "The first report comes after a week of movement."
|
|
4
|
+
*
|
|
5
|
+
* quietPlace(place, stage) follows the menu's read of GET /api/moments (the event 'cz:moments' from pm/cozy-nav.js): while the place is
|
|
6
|
+
* quiet, the page's panel shows only its line; the moment it opens, the page is back as it was
|
|
7
|
+
* quietLine(place) the line while that place is quiet, else '' (Intelligence's reports panel uses it for its empty state)
|
|
8
|
+
* A published (read-only) copy is never quiet. Quiet only applies when every workspace on this computer is new and none has reached that
|
|
9
|
+
* place, so a PMOS already in use never loses a page. */
|
|
10
|
+
let last = null;
|
|
11
|
+
if (typeof document !== 'undefined') document.addEventListener('cz:moments', (e) => { last = e.detail || null; });
|
|
12
|
+
|
|
13
|
+
export const quietLine = (place) => {
|
|
14
|
+
if (typeof window !== 'undefined' && window.PMOS_READONLY) return '';
|
|
15
|
+
const p = last && last.places && last.places[place];
|
|
16
|
+
return p && p.quiet ? String(p.line || '') : '';
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function quietPlace(place, stage, doc = document) {
|
|
20
|
+
if (!stage || (typeof window !== 'undefined' && window.PMOS_READONLY)) return;
|
|
21
|
+
const draw = () => {
|
|
22
|
+
const line = quietLine(place);
|
|
23
|
+
let el = stage.querySelector(':scope > .qp-line');
|
|
24
|
+
if (!line) { stage.classList.remove('qp-on'); if (el) el.remove(); return; }
|
|
25
|
+
if (!el) { el = doc.createElement('p'); el.className = 'qp-line'; el.setAttribute('role', 'status'); stage.prepend(el); }
|
|
26
|
+
el.textContent = line;
|
|
27
|
+
stage.classList.add('qp-on');
|
|
28
|
+
};
|
|
29
|
+
doc.addEventListener('cz:moments', () => setTimeout(draw, 0));
|
|
30
|
+
draw();
|
|
31
|
+
}
|
|
@@ -64,6 +64,9 @@
|
|
|
64
64
|
<section class="ins-pane" id="pOutcomes" aria-label="Did it work?" hidden>
|
|
65
65
|
<div class="ins-tbl" id="outTbl" role="table" aria-label="Did it work?"></div>
|
|
66
66
|
</section>
|
|
67
|
+
<!-- Lab (idle lab B+C, canvas I1 IlBC): the ✦ that fit no initiative, the week's record of every background job, the keep rate, next time.
|
|
68
|
+
Hidden until there is something (pm/cozy-lab-ins.js). -->
|
|
69
|
+
<section class="ins-pane lab-sec" id="pLab" aria-labelledby="hLab" hidden></section>
|
|
67
70
|
</main>
|
|
68
71
|
|
|
69
72
|
<!-- Reports and dashboards (⋯; the old Library): Found in your work, requests from Home, the reports, then the dashboards -->
|
|
@@ -3625,6 +3625,20 @@
|
|
|
3625
3625
|
"T21 progress is ledger-only and leaves proposed items out",
|
|
3626
3626
|
"T25 batches and flags are this initiative's: another initiative's batch and kept-not-ui stay out; a torn line (no initiative) is shown"
|
|
3627
3627
|
]
|
|
3628
|
+
},
|
|
3629
|
+
{
|
|
3630
|
+
"id": "E81",
|
|
3631
|
+
"group": "E",
|
|
3632
|
+
"file": "workspace/proactive-prepare.test.js",
|
|
3633
|
+
"section": "runner wiring",
|
|
3634
|
+
"change": "replaced",
|
|
3635
|
+
"slice": "bg-queue",
|
|
3636
|
+
"reason": "Idle lab frame I4 (PM wawan, 2026-11-04, 'build B+C'): the 60-second interval now runs one queue (lib/bg-queue.js) and every job, the prepare runner included, is a registered queue job; the literal call chain C5 matched is gone.",
|
|
3637
|
+
"replacement": "C5 (rewritten) asserts the runner is the queue's 'prepare' job, the interval runs bgQueue.tick(), and no setInterval names prepareTick; bg-queue.test.js Q1/Q10 assert the queue's order and that registered jobs run each tick.",
|
|
3638
|
+
"pm_approval": null,
|
|
3639
|
+
"names": [
|
|
3640
|
+
"C5 the runner is called from the existing 60-second interval and nowhere else is a second timer made"
|
|
3641
|
+
]
|
|
3628
3642
|
}
|
|
3629
3643
|
]
|
|
3630
3644
|
}
|