create-pmos 0.493.0 → 0.495.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.
Files changed (82) hide show
  1. package/package.json +99 -27
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/briefing-premium-adddoc-test.js +2 -1
  7. package/workspace/briefing-premium-board-test.js +5 -4
  8. package/workspace/briefing-premium-initiative-test.js +4 -2
  9. package/workspace/changelog.json +98 -0
  10. package/workspace/guide/briefing-premium.md +4 -1
  11. package/workspace/guide/claude-proposes.md +2 -2
  12. package/workspace/guide/data-freshness.md +3 -1
  13. package/workspace/guide/initiatives-screen.md +3 -1
  14. package/workspace/guide/report-slides.md +14 -1
  15. package/workspace/guide/reports.md +2 -0
  16. package/workspace/guide/workflows.md +15 -3
  17. package/workspace/guide/your-impact.md +6 -7
  18. package/workspace/ini-workflow-test.js +2 -1
  19. package/workspace/lib/board-trail.js +30 -3
  20. package/workspace/lib/engine/workflows.js +19 -6
  21. package/workspace/lib/git-shape.js +21 -0
  22. package/workspace/lib/grill-runs.js +21 -6
  23. package/workspace/lib/grill.js +1 -1
  24. package/workspace/lib/impact.js +29 -16
  25. package/workspace/lib/initiative-cli.js +15 -1
  26. package/workspace/lib/open-mission.js +14 -0
  27. package/workspace/lib/page-warm.js +68 -0
  28. package/workspace/lib/report-design.js +6 -1
  29. package/workspace/lib/report-export.js +150 -9
  30. package/workspace/lib/report-pyramid.js +535 -0
  31. package/workspace/lib/report-structure.js +5 -1
  32. package/workspace/lib/report-templates.js +111 -20
  33. package/workspace/lib/routes-boards.js +36 -3
  34. package/workspace/lib/routes-brief-page.js +21 -3
  35. package/workspace/lib/routes-grill.js +10 -4
  36. package/workspace/lib/routes-impact.js +12 -4
  37. package/workspace/lib/routes-initiatives.js +1 -0
  38. package/workspace/lib/routes-programs.js +6 -4
  39. package/workspace/lib/routes-report-design.js +15 -0
  40. package/workspace/lib/routes-report-flow.js +115 -19
  41. package/workspace/lib/routes-report-setups.js +4 -2
  42. package/workspace/lib/routes-report-structure.js +3 -0
  43. package/workspace/lib/routes-workflows.js +6 -1
  44. package/workspace/lib/wf-bridge.js +5 -3
  45. package/workspace/lib/workflow-cli.js +2 -2
  46. package/workspace/lib/workflows-store.js +15 -1
  47. package/workspace/metric-drafts-ui-test.js +2 -1
  48. package/workspace/package-lock.json +2 -2
  49. package/workspace/package.json +1 -1
  50. package/workspace/premium-board-actions-test.js +2 -1
  51. package/workspace/premium-chat-report-tab-test.js +5 -1
  52. package/workspace/premium-vw-backlog-test.js +3 -2
  53. package/workspace/premium-vw-item-test.js +3 -2
  54. package/workspace/premium-vw-jira-test.js +3 -2
  55. package/workspace/program-panel-test.js +2 -1
  56. package/workspace/public/pm/cozy-home.css +1 -0
  57. package/workspace/public/pm/cozy-vw-rp.css +12 -0
  58. package/workspace/public/pm/fetch-share.js +38 -0
  59. package/workspace/public/pm/home-app.js +24 -4
  60. package/workspace/public/pm/home-chat.js +53 -11
  61. package/workspace/public/pm/home-impact.css +23 -13
  62. package/workspace/public/pm/home-impact.js +49 -32
  63. package/workspace/public/pm/home-ini.css +2 -0
  64. package/workspace/public/pm/ini-view.css +55 -0
  65. package/workspace/public/pm/ini-view.js +7 -4
  66. package/workspace/public/pm/premium-board.js +1 -0
  67. package/workspace/public/pm/premium-insights.js +1 -0
  68. package/workspace/public/pm/premium-report-read.js +25 -6
  69. package/workspace/public/pm/premium-report-tab.js +69 -14
  70. package/workspace/public/pm/premium-shell.js +57 -6
  71. package/workspace/public/pm/premium.css +1 -0
  72. package/workspace/public/pm/report-flow.js +93 -10
  73. package/workspace/public/pm/wf-page.css +8 -0
  74. package/workspace/public/pm/wf-page.js +33 -5
  75. package/workspace/server.js +24 -3
  76. package/workspace/skills/report-template.skill +10 -0
  77. package/workspace/split-merge-ui-test.js +3 -2
  78. package/workspace/stage-card-test.js +2 -1
  79. package/workspace/task-press-test.js +2 -1
  80. package/workspace/test-lists/page-open-speed.json +5 -0
  81. package/workspace/test-lists/report-pyramid.json +9 -0
  82. package/workspace/tk-tasks-test.js +2 -1
@@ -14,6 +14,7 @@
14
14
  *
15
15
  * The models are in home-data.js and home-list.js (pure). This file reads the routes, draws, and wires the presses. Every write is the PM's
16
16
  * press (pmPost). Nothing here opens the old dialog or links to the console. */
17
+ import './fetch-share.js'; // first: identical GETs in flight are asked once (page-open-speed)
17
18
  import { mountNav } from './cozy-nav.js';
18
19
  import { mountSetupChecklist } from './setup-checklist.js';
19
20
  import { mountOnboard } from './cozy-onboard.js';
@@ -222,7 +223,8 @@ async function load() {
222
223
  // an initiative of another product opened before its board was read (a link, a quick press): read it again from its own board
223
224
  if (S.ini && !S.IM && otherIni(S.ini)) goIni(S.ini, { page: VW.isOpen() });
224
225
  }
225
- /* ── Home's one line (canvas FcPhone, Home: "only when something changed"): the newest grill this week that left findings, from GET /api/grill.
226
+ /* ── Home's one line (canvas FcPhone, Home: "only when something changed"): the newest grill this week that left findings, from GET /api/grill,
227
+ * and how many other initiatives were grilled with it ("I grilled Inline login and 5 more this week: 21 findings.").
226
228
  * "Checkout v2 will miss its date. I grilled it overnight: 3 findings." ✦ opens them on the initiative. Nothing to say is silence. ── */
227
229
  async function loadGrillNews() {
228
230
  const b0 = (S.D.board && S.D.board.board) || {};
@@ -236,6 +238,7 @@ function grillNews() {
236
238
  const row = H.boardRows(S.D).find((r) => r.id === n.ini), title = (row && row.title) || n.ini;
237
239
  const t = n.turn && GW.turnWord(n.turn.to), when = GW.whenWord(n.at), f = n.n + (n.n === 1 ? ' finding' : ' findings');
238
240
  const text = t ? (n.turn.scope === 'goal' ? 'A goal ' + title + ' moves ' + t + '. I grilled it ' + when + ': ' + f + '.' : title + ' ' + t + (t === 'will miss' ? ' its date' : '') + '. I grilled it ' + when + ': ' + f + '.')
241
+ : n.more > 0 ? 'I grilled ' + title + ' and ' + n.more + ' more this week: ' + n.total + ' findings.'
239
242
  : 'I grilled ' + title + ' ' + when + (GW.whyWord(n) ? ', ' + GW.whyWord(n) : '') + ': ' + f + '.';
240
243
  return { id: n.ini, title, text, n: n.n };
241
244
  }
@@ -840,7 +843,9 @@ function sessionTarget(s) {
840
843
  // a program's or a product's own chat says so in its scope, never "Loose" (batch 22 item 17)
841
844
  const pm = /^chat:program:([^:]+)/.exec(key || ''), pr = pm ? (S.D.programs || []).find((p) => p && p.id === pm[1]) : null;
842
845
  const prod = !ini && !pr && /^product(:|$)/.test(key || '') ? H.productName(S.D) : '';
843
- const home = pr ? { text: pr.title, icon: TD.P.layers } : prod ? { text: prod, icon: I.home } : null;
846
+ // a New workflow chat (Settings › Workflows) says so, never "Loose" (batch 24 item 4); its draft stays on the Workflows list
847
+ const wfNew = !ini && !pr && !prod && /^pop:workflow:/.test(key || '');
848
+ const home = pr ? { text: pr.title, icon: TD.P.layers } : prod ? { text: prod, icon: I.home } : wfNew ? { text: 'New workflow', icon: I.chat } : null;
844
849
  const t = { title: H.nameOf(s), ses: s.id, sub: [role.name, ini ? iniTitle(ini) : home ? home.text : 'Loose'].join(' · '), ini, role, scope: ini ? scopeOf(ini) : home || { text: 'Loose', icon: I.chat, loose: true }, rich: rich(), to: rich() ? role : null, also: alsoOf(ini) };
845
850
  if (key && /^(decision|blocked|waited|src|ev|work|board|init|doc|ins|chat|pop):/.test(key)) { t.key = key; t.sid = undefined; } else t.sid = s.id;
846
851
  if (!t.key) t.sid = s.id;
@@ -1052,12 +1057,12 @@ function homeTarget() {
1052
1057
  empty: 'Ask anything about your work. Claude reads your repositories and works in a real Claude Code session.', also: alsoOf(null),
1053
1058
  intro: S.impact ? S.impact.intro : undefined };
1054
1059
  }
1055
- /* Your impact (batch 16 item 7): when a finished week has items not checked yet, Home's own chat opens on Claude's check (GmP1Ledger).
1060
+ /* 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.
1056
1061
  * Off in Settings, or nothing to check: no card. Read after the list is drawn, so it never holds Home up. */
1057
1062
  async function loadImpact() {
1058
1063
  const c = await IMP.readCheck(pmGet).catch(() => null);
1059
1064
  const was = !!S.impact;
1060
- S.impact = c ? { c, intro: IMP.checkIntro(c, { esc, pmPost, done: () => { S.impact = null; drawList(); } }) } : null;
1065
+ S.impact = c ? { c, intro: IMP.checkIntro(c, { esc, pmPost }) } : null;
1061
1066
  const cur = chat.current && chat.current();
1062
1067
  if ((was || S.impact) && cur && cur.key === 'chat:main') { chat.open(homeTarget()); }
1063
1068
  if (was || S.impact) drawList();
@@ -1583,6 +1588,19 @@ function menCtx() {
1583
1588
  drawList(); chat.focus();
1584
1589
  } };
1585
1590
  }
1591
+ /* A conversation about one item that names its own context (a report's chat icon, report-flow.js chatOf): Home's chat opens on it, the Viewer
1592
+ * stays beside it (on a phone the chat takes the page). Another page hands it over through sessionStorage with ?about=<its key>. */
1593
+ function chatAbout(t) {
1594
+ S.phoneChat = isPhone(); if (isPhone()) VW.close();
1595
+ openTarget({ ...homeTarget(), key: t.key, title: t.title, scope: { text: 'Report · ' + t.title, icon: I.chat }, intro: t.intro ? { role: CLAUDE, text: t.intro } : undefined,
1596
+ placeholder: t.placeholder || 'Message Claude', context: t.context });
1597
+ drawList(); chat.focus();
1598
+ }
1599
+ addEventListener('pmos-chat-about', (e) => { const t = e.detail; if (!t || !/^pop:[A-Za-z0-9][A-Za-z0-9._:-]{0,100}$/.test(String(t.key)) || !t.context) return; e.preventDefault(); chatAbout(t); });
1600
+ function aboutHanded(key) {
1601
+ let t = null; try { t = JSON.parse(sessionStorage.getItem('pmos-chat-about') || 'null'); sessionStorage.removeItem('pmos-chat-about'); } catch { return null; }
1602
+ return t && t.key === key && t.context ? t : null;
1603
+ }
1586
1604
  function recLive() {
1587
1605
  if (!REC) return null;
1588
1606
  const h = REC.holding && REC.holding();
@@ -1949,6 +1967,7 @@ load().then(() => {
1949
1967
  const say = (P.get('say') || P.get('q') || '').slice(0, 500);
1950
1968
  const asked = !ask ? '' : /^board:/.test(ask) ? ask : /^(insights|verdict)$/.test(ask) ? 'ins:' + ask : 'init:' + ask;
1951
1969
  const about = KEY.test(P.get('about') || '') ? P.get('about') : KEY.test(asked) ? asked : null;
1970
+ const handed = about && /^pop:/.test(about) ? aboutHanded(about) : null;
1952
1971
  if (isPhone() && ((open && !blockOpen) || sid || about)) { S.phoneChat = true; drawList(); } // the conversation the address names, not the list
1953
1972
  if (open && !blockOpen) {
1954
1973
  // ?item=pt:<program>:<key> (the program panel's link to a program task) names the row by its id; Home lists no task, so the chat opens
@@ -1960,6 +1979,7 @@ load().then(() => {
1960
1979
  else if (about && /^init:/.test(about) && iniRowOf(P.get('vid') || about.slice(5))) { const id = iniRowOf(P.get('vid')) ? P.get('vid') : about.slice(5); goIni(id).then(() => { if (P.get('vk') && S.ini === id && !isPhone()) iniOverview(); }); }
1961
1980
  // the Board's program panel Discuss in chat names board:program:<id>: the program's own conversation, the one its tasks open (bug 2026-10-24)
1962
1981
  else if (about && /^board:program:[A-Za-z0-9][A-Za-z0-9._-]{0,100}$/.test(about)) openTarget(programTarget(about.slice(14)));
1982
+ else if (handed) chatAbout(handed); // a report's chat icon on another page
1963
1983
  else if (about) { openTarget({ ...homeTarget(), key: about, title: (P.get('label') || '').slice(0, 80) || about }); discussed(about, P.get('vid'), P.get('label')); }
1964
1984
  else if (P.get('view') === 'metrics') openMetrics(P.get('ws') || ''); // ?view=metrics: an old link lands on Intelligence › Delivery trends
1965
1985
  else if (S.ini) goIni(S.ini);
@@ -15,7 +15,7 @@
15
15
  * started, POST /ask and /press) or a session with no chat key (GET /api/sessions/conversation?id=, POST /api/session/say and /keys).
16
16
  * A loose session the host can place (host.suggestLink(target, firstText) → { id, title }) shows one chip: Link it (host.linkTo) or × (host.dismissLink).
17
17
  * Every write is the PM's press. mountChat(el, host) returns { open(target), refresh(), focus(), send(text) }.
18
- * target: { key?, sid?, title, scope: { text, icon, loose }, role: { name, initial }, intro?: { role, text, at, pills: [{ label, primary, go }] },
18
+ * target: { key?, sid?, title, scope: { text, icon, loose }, role: { name, initial }, intro?: { role, text, at, mood? (Adler as the avatar while the log is empty), pills: [{ label, primary, go }] },
19
19
  * ini?, rich, also?: { text, go } }
20
20
  * host: { esc, toast, pmGet, pmPost, roles(), seen(key), markSeen(key), onScope(btn), onLink(btn), onTo(btn), workItem(text, target),
21
21
  * viewerClosed(), reopenViewer(), head(target), touch(sid) (the PM opened, typed in or answered that session: the Live pill) } */
@@ -58,7 +58,7 @@ export const adlerAvHtml = (mood) => `<span class="hm-av hm-adler" aria-hidden="
58
58
 
59
59
  export function mountChat(el, host) {
60
60
  const esc = host.esc;
61
- const C = { t: null, th: null, conv: null, timer: null, show: 12, plOpen: false, plAll: false, open: new Set(), seq: 0, sending: false, files: [], seenAt: null, lastKey: null, picked: {}, auto: { key: null, ctx: new Set(), rep: null, goal: null }, slashAt: 0 };
61
+ const C = { t: null, th: null, conv: null, timer: null, show: 12, plOpen: false, plAll: false, open: new Set(), seq: 0, sending: false, files: [], seenAt: null, lastKey: null, picked: {}, pend: [], auto: { key: null, ctx: new Set(), rep: null, goal: null }, slashAt: 0 };
62
62
  el.innerHTML = `<button type="button" class="hm-reopen" id="hmReopen" aria-label="Open the Viewer" title="Open the Viewer" hidden>${PANE}</button>`
63
63
  + '<div class="hm-chd" id="hmCh" hidden></div><div class="hm-sug" id="hmSug" hidden></div><div class="hm-plw" id="hmPl" hidden></div><div class="hm-log" id="hmLog" role="log" aria-live="polite"></div><div class="hm-foot" id="hmFoot"></div>';
64
64
  const log = el.querySelector('#hmLog'), foot = el.querySelector('#hmFoot'), reopen = el.querySelector('#hmReopen'), head = el.querySelector('#hmCh'), sug = el.querySelector('#hmSug'), pl = el.querySelector('#hmPl');
@@ -138,11 +138,12 @@ export function mountChat(el, host) {
138
138
  if (a.kind === 'reach' && R.stop && R.stop.reach && it.mi === R.lastMsg) return '';
139
139
  return kindCardHtml(a, { live, role: R.role.name });
140
140
  }
141
- function introHtml(o) {
141
+ /** alone: nothing else in the log yet, so an intro with a mood shows Adler as Claude's avatar (one Adler: a reply takes him over). */
142
+ function introHtml(o, alone = true) {
142
143
  if (!o) return '';
143
144
  const r = o.role || { name: 'Claude', initial: 'C' };
144
145
  const card = o.card && typeof o.card.html === 'function' ? o.card.html() : '';
145
- return `<div class="hm-a">${avHtml(r)}<div class="hm-ab"><p class="hm-who"><b>${esc(r.name)}</b> <span class="hm-say">${esc(o.text)}</span></p>${card ? `<div class="hm-icard">${card}</div>` : ''}</div></div>`
146
+ return `<div class="hm-a">${o.mood && alone && isClaude(r) ? adlerAvHtml(o.mood) : avHtml(r)}<div class="hm-ab"><p class="hm-who"><b>${esc(r.name)}</b> <span class="hm-say">${esc(o.text)}</span></p>${card ? `<div class="hm-icard">${card}</div>` : ''}</div></div>`
146
147
  + (o.pills && o.pills.length ? `<div class="hm-pills">${o.pills.map((p, i) => `<button type="button" class="${p.primary ? 'pri' : ''}" data-hm-pill="${i}"${p.disabled ? ' disabled' : ''}>${esc(p.label)}</button>`).join('')}</div>` : '');
147
148
  }
148
149
  /** Claude's question card (AskUserQuestion), answerable until the PM writes again. */
@@ -174,6 +175,32 @@ export function mountChat(el, host) {
174
175
  return q.filter((x) => !read(x)).map((x) => `<div class="hm-q"><div class="hm-qb">${userHtml(x.text)}</div><span class="hm-qn">${esc(role().name)} reads it after this tool call, in seconds<button type="button" class="hm-qx" data-hm-qdel="${esc(x.at)}" aria-label="Take it back" title="Take it back">${SVG(XX, 13)}</button></span></div>`).join('');
175
176
  }
176
177
  const sid = () => (C.t && C.t.sid) || (C.th && C.th.session) || null;
178
+ /* A sent message shows at once (batch 24 item 2: "enter the prompt is so long to appear on the chat"). Its bubble draws the moment the PM
179
+ * presses Enter, says "Sending…" until PMOS takes it, then "You · HH:MM". It leaves when the transcript holds the PM's turn (the same words,
180
+ * or any PM turn from that moment on), and after 3 minutes at most, so a bubble never stays as a ghost. A message that was not sent leaves at
181
+ * once and its words go back in the box. */
182
+ const PEND_MAX = 180000;
183
+ function settlePend() {
184
+ if (!C.pend.length) return;
185
+ const key = (t) => splitFiles(shownUser(t)).text.replace(/\s+/g, ' ').trim();
186
+ const users = ((C.conv && C.conv.messages) || []).filter((m) => m.role === 'user');
187
+ const lastUser = users.reduce((n, m) => Math.max(n, Date.parse(m.at) || 0), 0), now = Date.now();
188
+ // a message for a working session is queued by the host and drawn as "reads it after this tool call": the bubble gives way to it
189
+ const queued = new Set((host.queued ? host.queued(sid()) : []).map((x) => x.text));
190
+ C.pend = C.pend.filter((p) => {
191
+ if (!p.sent) return true;
192
+ if (queued.has(p.text)) return false;
193
+ const t0 = Date.parse(p.at);
194
+ if (now - t0 > PEND_MAX) return false;
195
+ if (lastUser && lastUser >= t0 - 2000) return false;
196
+ return !users.slice(p.n).some((m) => key(m.text) === key(p.text));
197
+ });
198
+ }
199
+ function pendHtml() {
200
+ settlePend();
201
+ return C.pend.map((p) => `<div class="hm-u hm-pend${p.sent ? '' : ' sending'}"><div class="hm-bub">${filesHtml(p.files)}${p.text ? userHtml(p.text) : ''}</div>`
202
+ + `<span class="hm-you" role="status">${p.sent ? 'You · ' + esc(hm(p.at)) : 'Sending…'}</span></div>`).join('');
203
+ }
177
204
  // A loose chat that reads like one initiative gets one quiet chip at its top: "Looks like <initiative> · Link it ×" (XvALinkSuggest).
178
205
  function drawSug() {
179
206
  const t = C.t, g = t && t.scope && t.scope.loose && t.sid && host.suggestLink ? host.suggestLink(t, firstUser()) : null;
@@ -204,7 +231,7 @@ export function mountChat(el, host) {
204
231
  const goal = C.th && C.th.goal;
205
232
  const parts = items.map((it, i) => msgHtml(it, i, R));
206
233
  placeDivider(parts, items, goal);
207
- let html = (t.intro && !(t.intro.onlyEmpty && items.length) ? introHtml(t.intro) : '') + parts.join('') + askHtml(R) + (R.stop && R.lastAgent < 0 ? dividerHtml(R.stop) : '') + queuedHtml();
234
+ let html = (t.intro && !(t.intro.onlyEmpty && items.length) ? introHtml(t.intro, !items.length) : '') + parts.join('') + askHtml(R) + (R.stop && R.lastAgent < 0 ? dividerHtml(R.stop) : '') + pendHtml() + queuedHtml();
208
235
  html += GU.choiceHtml(); // /goal while a goal works here: In a new chat, or Here instead
209
236
  if (C.th && C.th.working) html += `<p class="hm-note" role="status">The agent is answering. You can write again when it has.</p>`;
210
237
  if (C.conv && C.conv.state === 'ended') html += '<p class="hm-note">This session has ended. Your conversation is kept here, and your next message picks it up.</p>';
@@ -417,20 +444,31 @@ export function mountChat(el, host) {
417
444
  async function send(text) {
418
445
  const t = C.t; text = String(text || '').trim();
419
446
  if (!t || (!text && !C.files.length) || C.sending) return false;
447
+ if (t.key && C.files.some((f) => f.state === 'saving')) { host.toast('A file is still being saved. Send again in a moment.'); return false; }
420
448
  C.sending = true;
421
449
  let r;
422
450
  const was = running();
451
+ // the bubble first, then the server (batch 24 item 2): the files leave the box with the words, and come back if the send fails
452
+ const sentFiles = C.files;
453
+ const p = { text, files: sentFiles.map((f) => f.path || f.saved).filter(Boolean), at: new Date().toISOString(), sent: false, n: ((C.conv && C.conv.messages) || []).filter((m) => m.role === 'user').length };
454
+ C.pend.push(p); C.files = []; C.jump = true; draw();
423
455
  if (t.key) {
424
- if (C.files.some((f) => f.state === 'saving')) { C.sending = false; host.toast('A file is still being saved. Send again in a moment.'); return false; }
425
- const files = C.files.map((f) => f.saved);
456
+ const files = sentFiles.map((f) => f.saved);
426
457
  r = await host.pmPost('/api/briefing-premium/ask' + (t.ws ? '?ws=' + encodeURIComponent(t.ws) : ''), files.length ? { key: t.key, text, files, ...(t.context ? { context: t.context } : {}) } : { key: t.key, text, ...(t.context ? { context: t.context } : {}) }).catch(() => null);
427
458
  } else r = await host.pmPost('/api/session/say', { id: t.sid, text }).catch(() => null);
428
459
  C.sending = false;
429
- if (!r || r.ok !== true) { host.toast((r && r.error) || 'The message was not sent.'); return false; }
460
+ if (!r || r.ok !== true) {
461
+ C.pend = C.pend.filter((x) => x !== p);
462
+ if (C.t === t && !C.files.length) C.files = sentFiles;
463
+ if (C.t === t) draw();
464
+ host.toast((r && r.error) || 'The message was not sent.'); return false;
465
+ }
430
466
  const s = sid() || r.session;
431
467
  if (host.touch) host.touch(s);
432
468
  if (was && s && host.queue) host.queue(s, text);
433
- C.files.forEach((f) => { if (f.url) URL.revokeObjectURL(f.url); }); C.files = []; C.jump = true;
469
+ p.sent = true;
470
+ sentFiles.forEach((f) => { if (f.url) URL.revokeObjectURL(f.url); }); C.jump = true;
471
+ if (C.t === t) draw();
434
472
  if (t.onSent) t.onSent(r);
435
473
  read();
436
474
  return true;
@@ -526,7 +564,11 @@ export function mountChat(el, host) {
526
564
  drawFoot({ role: role() });
527
565
  }
528
566
  };
529
- el.addEventListener('submit', (e) => { e.preventDefault(); const ta = foot.querySelector('textarea'); const v = ta.value; if (GU.submit(v)) { ta.value = ''; grow(ta); return; } send(v).then((ok) => { if (ok) { ta.value = ''; grow(ta); } }); });
567
+ el.addEventListener('submit', (e) => { e.preventDefault(); const ta = foot.querySelector('textarea'); const v = ta.value; if (GU.submit(v)) { ta.value = ''; grow(ta); return; }
568
+ // the box empties at once and the bubble shows (batch 24 item 2); a send that fails puts the words back, unless the PM typed again
569
+ if (!v.trim() && !C.files.length) return;
570
+ ta.value = ''; grow(ta);
571
+ send(v).then((ok) => { const box = foot.querySelector('textarea'); if (!ok && box && !box.value) { box.value = v; grow(box); } }); });
530
572
  const shutAdd = (focus) => { if (!C.addOpen) return; C.addOpen = false; drawFoot({ role: role() }); if (focus) { const a = foot.querySelector('#hmAdd'); if (a) a.focus(); } };
531
573
  document.addEventListener('click', (e) => { if (C.addOpen && e.target.isConnected && !e.target.closest('.hm-addw')) shutAdd(false); }, true);
532
574
  el.addEventListener('keydown', (e) => {
@@ -622,7 +664,7 @@ export function mountChat(el, host) {
622
664
  if (notesM) notesM.then((M) => setTimeout(() => M.syncNotes(), 0)); // the Notes open in the Viewer belonged to the conversation before
623
665
  const same = C.t && t && C.t.key === t.key && C.t.sid === t.sid;
624
666
  C.t = t;
625
- if (!same) { C.addOpen = false; C.th = null; C.conv = null; C.show = 12; C.open = new Set(); C.files = []; C.marked = null; C.touched = null; C.picked = {}; C.plOpen = false; C.plAll = false; C.seenAt = t.key ? host.seen(t.key) : null; put(log, introHtml(t.intro)); put(pl, ''); pl.hidden = true; C.jump = true; peek(null); }
667
+ if (!same) { C.addOpen = false; C.th = null; C.conv = null; C.show = 12; C.open = new Set(); C.files = []; C.marked = null; C.touched = null; C.picked = {}; C.pend = []; C.plOpen = false; C.plAll = false; C.seenAt = t.key ? host.seen(t.key) : null; put(log, introHtml(t.intro)); put(pl, ''); pl.hidden = true; C.jump = true; peek(null); }
626
668
  draw(); read();
627
669
  },
628
670
  refresh() { if (C.t) draw(); },
@@ -1,16 +1,26 @@
1
- /* pm/home-impact.css — the weekly check of Your impact in Home's chat (feedback batch 16 item 7, canvas GmP1Ledger). pm/home-impact.js. */
2
- .im-card { clear: both; margin-top: 8px; padding: 4px 14px 8px; } /* under Claude's line, never beside it (the line floats on a desk) */
3
- .im-list { list-style: none; margin: 0; padding: 0; }
4
- .im-row { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-top: 1px solid var(--cz-hair); }
5
- .im-row:first-child { border-top: 0; }
6
- .im-row.im-off .im-t { color: var(--cz-ink3); text-decoration: line-through; }
7
- .im-k { width: 36px; height: 36px; border-radius: 10px; display: inline-flex; align-items: center; justify-content: center; flex: none; background: var(--cz-well); color: var(--cz-ink2); }
8
- .im-k.im-thanks { background: var(--cz-bluebg); color: var(--cz-blueink); }
1
+ /* pm/home-impact.css — the weekly check of Your impact in Home's chat (batch 16 item 7; batch 23 item 4, canvas IcA). pm/home-impact.js. */
2
+ .im-card { clear: both; margin-top: 10px; max-width: 600px; } /* under Claude's line, never beside it (the line floats on a desk) */
3
+ .im-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
4
+ .im-q { position: relative; display: flex; align-items: flex-start; gap: 10px; padding: 12px 8px 12px 16px; border-radius: 18px; background: var(--cz-bluebg); }
5
+ .im-k { flex: none; display: inline-flex; margin-top: 2px; color: var(--cz-blueink); }
9
6
  .im-tx { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
10
- .im-t { font-size: var(--fs-4); overflow-wrap: anywhere; }
11
- .im-src { display: inline-flex; align-items: center; gap: 4px; text-decoration: none; }
7
+ .im-t { font-size: var(--fs-4); font-weight: 700; line-height: 1.35; color: var(--cz-ink); overflow-wrap: anywhere; }
8
+ .im-w { font-size: var(--fs-3); color: var(--cz-ink2); overflow-wrap: anywhere; }
9
+ .im-src { display: inline-flex; align-items: center; gap: 4px; margin-top: 2px; font-size: var(--fs-2); color: var(--cz-ink3); text-decoration: none; }
12
10
  .im-src:hover { text-decoration: underline; }
13
11
  .im-src svg, .im-lock svg { flex: none; }
14
- .im-foot { display: flex; align-items: center; gap: 8px; padding-top: 8px; }
15
- .im-lock { flex: 1; display: inline-flex; align-items: center; gap: 6px; }
16
- @media (prefers-reduced-motion: no-preference) { .im-row { transition: opacity .2s ease; } }
12
+ .im-mw { position: relative; flex: none; }
13
+ .im-more { width: 28px; height: 28px; color: var(--cz-ink2); }
14
+ .ut-ib.im-more svg { stroke-width: 3.2; }
15
+ .im-pop { position: absolute; right: 0; top: 32px; z-index: 5; padding: 4px; border-radius: 14px; background: var(--cz-card); box-shadow: 0 14px 34px -14px rgba(42, 46, 53, .4), 0 0 0 1px var(--cz-hair); }
16
+ .im-pop button { display: flex; align-items: center; gap: 8px; height: 36px; padding: 0 12px; border: 0; border-radius: 10px; background: none; color: var(--cz-ink); font: inherit; font-size: var(--fs-3); font-weight: 600; white-space: nowrap; cursor: pointer; }
17
+ .im-pop button:hover, .im-pop button:focus-visible { background: var(--cz-well); }
18
+ .im-pop svg { width: 15px; height: 15px; }
19
+ .im-q.im-gone { align-items: center; padding: 8px 8px 8px 16px; background: var(--cz-well); color: var(--cz-ink3); font-size: var(--fs-3); }
20
+ .im-gt { flex: 1; min-width: 0; overflow-wrap: anywhere; }
21
+ .im-undo { flex: none; height: 32px; padding: 0 12px; border: 0; border-radius: 999px; background: none; color: var(--cz-blueink); font: inherit; font-size: var(--fs-3); font-weight: 700; cursor: pointer; }
22
+ .im-undo:hover { background: var(--cz-card); }
23
+ .im-lock { display: flex; align-items: center; gap: 6px; margin: 10px 0 0; font-size: var(--fs-2); color: var(--cz-ink3); }
24
+ .im-msg { margin: 4px 0 0; font-size: var(--fs-2); color: var(--cz-ink3); }
25
+ .im-msg:empty { display: none; }
26
+ @media (prefers-reduced-motion: no-preference) { .im-q { transition: background-color .2s ease; } }
@@ -1,12 +1,14 @@
1
- /* pm/home-impact.js — the weekly check of "Your impact" in Home's chat (feedback batch 16 item 7, Phase 1; canvas In development GmP1Ledger).
2
- * After a week ends, Claude lists that week's items and asks whether each is real: ✕ or ✓ on each row, ✓✓ for every row not checked yet.
3
- * Only ✓ items count. Each row links to its source. No number is shown. The card is Home's own chat intro while the check is due; it goes
4
- * when every row is checked.
1
+ /* pm/home-impact.js — the weekly check of "Your impact" in Home's chat (feedback batch 16 item 7; rebuilt batch 23 item 4, canvas IcA).
2
+ * After a week ends, Claude shows that week's items as thank-you bubbles: the quote in bold, who and what, the source link. They count by
3
+ * default: nothing needs a ✓. One quiet ⋯ per row holds "Not right, drop it"; a dropped row says so with Undo. The lock line is a quiet
4
+ * footer. No number is shown. The card is Home's own chat intro for the whole week after (lib/impact.js: when it ends, rows nobody dropped
5
+ * are real); the first time it is drawn, the page says so (looked), and the row under Mentions goes.
5
6
  *
6
7
  * readCheck(pmGet) → the GET /api/impact/check body when a check is due, else null
7
- * checkIntro(c, { esc, pmPost, done }) → the intro of Home's chat: { role, text, card: { html, press } }
8
- * rowHtml(c, { esc, sel }) → the one row under Mentions while the check is due: a tap opens Home's chat on it (on a phone too). No count.
9
- * Writes: POST /api/impact/verdict { id, real }, /verdict-all { week }, /undo { id } (page presses). Nothing leaves PMOS. */
8
+ * partsOf(item) → { quote, what }: the bold line (the words in quotes, else the item's sentence) and the who/what line
9
+ * checkIntro(c, { esc, pmPost }) → the intro of Home's chat: { role, mood, text, card: { html, press } }
10
+ * rowHtml(c, { esc, sel }) → the one row under Mentions until the card is seen: a tap opens Home's chat on it (on a phone too). No count.
11
+ * Writes: POST /api/impact/verdict { id, real: false }, /undo { id }, /looked { week }. Nothing leaves PMOS. */
10
12
 
11
13
  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>`;
12
14
  const P = {
@@ -14,7 +16,7 @@ const P = {
14
16
  unblock: 'M7 11V8a5 5 0 0 1 9.6-2M5 11h14v9H5z',
15
17
  used: 'M12 3l8 9-8 9-8-9z',
16
18
  kr: 'M4 17l6-6 4 4 6-7M15 8h5v5',
17
- no: 'M6 6l12 12M18 6L6 18', yes: 'M5 12l5 5 9-10', all: 'M18 6L7 17l-5-5M22 10l-7.5 7.5L13 16',
19
+ no: 'M6 6l12 12M18 6L6 18', yes: 'M5 12l5 5 9-10', dots: 'M5 12h.01M12 12h.01M19 12h.01',
18
20
  lock: 'M7 11V8a5 5 0 0 1 10 0v3M5 11h14v9H5z', open: 'M14 4h6v6M20 4l-9 9M18 14v6H4V6h6',
19
21
  };
20
22
  const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
@@ -26,47 +28,62 @@ export async function readCheck(pmGet) {
26
28
  }
27
29
 
28
30
  export function rowHtml(c, { esc, sel }) {
29
- if (!c) return '';
31
+ if (!c || c.looked) return '';
30
32
  return `<div class="hv-card hr-card hr-men-card"><div class="hm-r hv-r hr-men${sel ? ' sel' : ''}" data-id="impact">`
31
33
  + `<button type="button" class="hm-rm" data-hr-impact${sel ? ' aria-current="true"' : ''} aria-label="Your impact: ${esc(c.label.toLowerCase())} is ready to check">`
32
34
  + `<span class="hv-mk hr-at" aria-hidden="true">${SV(P.yes, 16)}</span><span class="hm-rt"><b>Your impact</b><small class="hr-sub">${esc(c.label)} is ready to check</small></span>`
33
35
  + '<span class="hv-xc" aria-hidden="true"></span></button></div></div>';
34
36
  }
35
37
 
36
- export function checkIntro(c, { esc, pmPost, done }) {
37
- const st = { c, msg: '' };
38
- const left = () => st.c.items.filter((x) => x.real === null);
38
+ const QUOTE = /[“"]([^”"]{2,})[”"]/;
39
+ /** The bold line is the thanks in the person's own words when the item quotes them; the line under it says who and what. */
40
+ export function partsOf(x) {
41
+ const t = String((x && x.text) || ''), who = String((x && x.who) || ''), m = QUOTE.exec(t);
42
+ if (!m) return { quote: t, what: who };
43
+ const rest = t.replace(m[0], ' ').replace(/^[\s:—–-]+|[\s:—–-]+$/g, '').replace(/\s+/g, ' ').trim();
44
+ return { quote: `“${m[1].trim()}”`, what: rest && rest !== who ? rest : who };
45
+ }
46
+
47
+ export function checkIntro(c, { esc, pmPost }) {
48
+ const st = { c, msg: '', menu: null, told: !!c.looked };
39
49
  const row = (x) => {
40
- const mark = x.real === null
41
- ? `<button type="button" class="ut-ib lg" data-im-v="no" data-im-id="${esc(x.id)}" aria-label="Not real, drop it" title="Not real, drop it">${SV(P.no)}</button>`
42
- + `<button type="button" class="ut-ib pri" data-im-v="yes" data-im-id="${esc(x.id)}" aria-label="Real, keep it" title="Real, keep it">${SV(P.yes)}</button>`
43
- : `<button type="button" class="ut-ib lg on" data-im-undo="${esc(x.id)}" aria-pressed="true" aria-label="${x.real ? 'Kept as real. Undo' : 'Dropped. Undo'}" title="${x.real ? 'Kept as real. Undo' : 'Dropped. Undo'}">${SV(x.real ? P.yes : P.no)}</button>`;
44
- return `<li class="im-row${x.real === false ? ' im-off' : ''}"><span class="im-k im-${esc(x.kind)}" aria-hidden="true">${SV(P[x.kind] || P.used)}</span>`
45
- + `<span class="im-tx"><span class="im-t">${esc(x.text)}</span><a class="ut-small ut-mut im-src" href="${esc(x.source.url)}" target="_blank" rel="noopener noreferrer">${esc(x.source.label)} · ${esc(dayName(x.at))} ${SV(P.open, 12)}</a></span>${mark}</li>`;
50
+ const { quote, what } = partsOf(x);
51
+ const src = `<a class="im-src" href="${esc(x.source.url)}" target="_blank" rel="noopener noreferrer">${esc(x.source.label)} · ${esc(dayName(x.at))} ${SV(P.open, 11)}</a>`;
52
+ if (x.real === false) {
53
+ return `<li class="im-q im-gone"><span class="im-gt">Dropped, not counted: ${esc(quote)}</span>`
54
+ + `<button type="button" class="im-undo" data-im-undo="${esc(x.id)}">Undo</button></li>`;
55
+ }
56
+ const open = st.menu === x.id;
57
+ return `<li class="im-q"><span class="im-k im-${esc(x.kind)}" aria-hidden="true">${SV(P[x.kind] || P.used)}</span>`
58
+ + `<span class="im-tx"><span class="im-t">${esc(quote)}</span>${what ? `<span class="im-w">${esc(what)}</span>` : ''}${src}</span>`
59
+ + `<span class="im-mw"><button type="button" class="ut-ib im-more" data-im-more="${esc(x.id)}" aria-haspopup="menu" aria-expanded="${open}" aria-label="More" title="More">${SV(P.dots, 16)}</button>`
60
+ + (open ? `<span class="im-pop" role="menu"><button type="button" role="menuitem" data-im-drop="${esc(x.id)}">${SV(P.no, 15)} Not right, drop it</button></span>` : '')
61
+ + '</span></li>';
46
62
  };
47
63
  const html = () => {
48
- const open = left();
49
- return `<div class="ut-card im-card" role="group" aria-label="Your impact, ${esc(st.c.label.toLowerCase())}">`
64
+ if (!st.told) { st.told = true; st.c.looked = true; Promise.resolve(pmPost('/api/impact/looked', { week: st.c.week })).catch(() => {}); }
65
+ return `<div class="im-card" role="group" aria-label="Your impact, ${esc(st.c.label.toLowerCase())}">`
50
66
  + `<ul class="im-list">${st.c.items.map(row).join('')}</ul>`
51
- + `<div class="im-foot"><span class="ut-small ut-mut im-lock">${SV(P.lock, 14)} Only you see this. Nothing leaves PMOS.</span>`
52
- + (open.length > 1 ? `<button type="button" class="ut-ib pri" data-im-all aria-label="All real, keep them all" title="All real, keep them all">${SV(P.all)}</button>` : '')
53
- + `</div><p class="ut-small ut-mut" role="status">${esc(st.msg)}</p></div>`;
67
+ + `<p class="im-lock">${SV(P.lock, 13)} Only you see this. Nothing leaves PMOS.</p>`
68
+ + `<p class="im-msg" role="status">${esc(st.msg)}</p></div>`;
54
69
  };
55
70
  const press = async (b) => {
56
71
  const d = b.dataset;
72
+ if (d.imMore) { st.menu = st.menu === d.imMore ? null : d.imMore; return true; }
57
73
  let r = null;
58
- if (d.imV) r = await pmPost('/api/impact/verdict', { id: d.imId, real: d.imV === 'yes' });
74
+ if (d.imDrop) r = await pmPost('/api/impact/verdict', { id: d.imDrop, real: false });
59
75
  else if (d.imUndo) r = await pmPost('/api/impact/undo', { id: d.imUndo });
60
- else if ('imAll' in d) r = await pmPost('/api/impact/verdict-all', { week: st.c.week });
61
76
  else return false;
77
+ st.menu = null;
62
78
  if (!r || !r.ok) { st.msg = (r && r.error) || 'Not saved.'; return true; }
63
- if (d.imV) { const x = st.c.items.find((y) => y.id === d.imId); if (x) x.real = d.imV === 'yes'; }
64
- else if (d.imUndo) { const x = st.c.items.find((y) => y.id === d.imUndo); if (x) x.real = null; }
65
- else for (const x of st.c.items) if (x.real === null) x.real = true;
66
- st.msg = left().length ? '' : 'Thank you. This week is checked.';
67
- if (!left().length && done) done();
79
+ const x = st.c.items.find((y) => y.id === (d.imDrop || d.imUndo));
80
+ if (x) x.real = d.imDrop ? false : null;
81
+ st.msg = '';
68
82
  return true;
69
83
  };
70
- const short = c.gate === 'short' ? ' Too many of these were not real, so I keep your sprout and your log hidden for now.' : '';
71
- return { role: { key: 'claude', name: 'Claude', initial: 'C' }, text: `Here’s what I’d count as your impact ${c.label === 'Last week' ? 'last week' : c.label.replace(/^Week of/, 'the week of')}. Is each one real? I read these from your sources. I don’t post anything anywhere.${short}`, card: { html, press } };
84
+ const thanks = c.items.some((x) => x.kind === 'thanks');
85
+ const when = c.label === 'Last week' ? 'last week' : c.label.replace(/^Week of/, 'the week of');
86
+ const short = c.gate === 'short' ? ' Too many of these were not right, so I keep your sprout and your log hidden for now.' : '';
87
+ return { role: { key: 'claude', name: 'Claude', initial: 'C' }, mood: 'handled',
88
+ text: `${thanks ? 'People thanked you' : 'Your work moved things'} ${when}. I read these from your sources and counted them in your impact.${short}`, card: { html, press } };
72
89
  }
@@ -126,6 +126,8 @@ button.hi-pill { cursor: pointer; }
126
126
  /* ── the initiative's page in the Viewer (canvas HbCDesk, HbCNew, HbCPhone): the stage card, three tasks, the documents that need a look ── */
127
127
  .hi-page { display: flex; flex-direction: column; gap: 14px; }
128
128
  .hi-page .hm-pcard { margin: 0; }
129
+ /* a phone (batch 23 item 5, canvas PmA): the Viewer's own 16 px is the page's one inset */
130
+ @media (max-width: 760px) { .hi-v.hi-page { padding-left: 0; padding-right: 0; } }
129
131
  .hi-pg { display: flex; flex-direction: column; gap: 2px; }
130
132
  .hi-pgh { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 0 4px; }
131
133
  .hi-pgh b { font-size: var(--fs-3); font-weight: 800; color: var(--cz-ink); }
@@ -1060,6 +1060,61 @@ button.iv-mt.iv-wf { padding: 0 !important; margin: 0 !important; }
1060
1060
  .shut-mc .iv-tel { min-height: 0; }
1061
1061
  .ab-sum .nm { display: none; }
1062
1062
  }
1063
+ /* ── a phone (batch 23 item 5, canvas PmA, agreed 2026-11-02): one 16 px inset and no beige wrapper; a shorter mission; the way to finish and About
1064
+ * as one white card of 48 px rows (the next stage with its round →, which opens the Move sheet; the people and the program; About); 36 px pills in
1065
+ * the Tasks header; task rows 12 px in, and a stage's late and open counts on its dates line. A desktop is unchanged. ── */
1066
+ .iv-gx-t .tx.ph, .iv-ws .n-in { display: none; }
1067
+ @media (max-width: 760px) {
1068
+ .iv-mc { background: transparent; padding: 0; border-radius: 0; gap: 0; }
1069
+ .iv-mc-row { display: contents; }
1070
+ .iv-mc > .iv-mc-top { margin-bottom: 12px; border: 1px solid var(--cz-hair); }
1071
+ .iv-mc .iv-tel { min-height: 128px; border-radius: 22px; }
1072
+ .iv-mc:not(.shut-mc) .iv-tel { margin-bottom: 12px; }
1073
+ .iv-mc .iv-tel-ct { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 0 12px; padding: 12px 56px 12px 16px; }
1074
+ .iv-mc .iv-tel-t { display: contents; }
1075
+ .iv-mc .iv-tel-ct .iv-shn { flex-basis: 100%; min-height: 36px; }
1076
+ .iv-tel .iv-dtc { min-height: 24px; font-size: var(--fs-2); }
1077
+ .iv-tel .iv-setd { min-height: 36px; }
1078
+ .iv-ro { gap: 16px; padding-top: 0; }
1079
+ .iv-ro > div { flex-direction: row; align-items: baseline; gap: 4px; }
1080
+ .iv-ro small { font-size: var(--fs-2); font-weight: 700; letter-spacing: 0; }
1081
+ .iv-ro b { font-size: var(--fs-3); }
1082
+ .iv-ro b i { font-size: var(--fs-2); }
1083
+ /* the readouts on the dates line say where today is; the art's own Today label would sit on them */
1084
+ .iv-tel .mi-tx.now { display: none; }
1085
+ /* the one card: the way to finish on top, About under it */
1086
+ .iv-gx, .iv-ab { border: 1px solid var(--cz-hair); }
1087
+ .iv-gx { border-radius: 22px 22px 0 0; border-bottom: 0; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "t a" "l l" "f f"; }
1088
+ .iv-ab { border-radius: 0 0 22px 22px; }
1089
+ .iv-gx-t { padding: 4px 0 4px 10px !important; min-height: 56px; gap: 6px; }
1090
+ .iv-gx-t .chev { width: 16px; }
1091
+ .iv-gx[data-ph] .iv-gx-t .tx.dk, .iv-gx[data-ph] .iv-gx-c { display: none; }
1092
+ .iv-gx-t .tx.ph { display: flex; }
1093
+ .iv-gx-t .tx b { font-size: var(--fs-3); overflow: hidden; text-overflow: ellipsis; }
1094
+ .iv-gx-c { grid-area: a; }
1095
+ .iv-gx-a { padding: 0 8px 0 4px; }
1096
+ .iv-gx[data-ph] .iv-gx-a .iv-scgo { flex: none; width: 40px; height: 40px; padding: 0 !important; justify-content: center !important; }
1097
+ .iv-gx[data-ph] .iv-gx-a .iv-scgn { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
1098
+ .iv-gx-ft { padding: 0 8px; min-height: 48px; display: flex; align-items: center; }
1099
+ .iv-gx .iv-meta { flex: 1; }
1100
+ .iv-gx .iv-prog { margin-left: auto !important; }
1101
+ .iv-ab-h { min-height: 48px; padding: 0 14px !important; }
1102
+ /* Tasks: 36 px pills (a 44 px press under each), rows 12 px in, the counts on the dates line */
1103
+ .hi-v .iv-th button { min-height: 36px; height: 36px; position: relative; }
1104
+ .hi-v .iv-th button::after { content: ""; position: absolute; left: 0; right: 0; top: -4px; bottom: -4px; }
1105
+ .iv-ws { padding-left: 12px !important; }
1106
+ .iv-ws.in { padding-left: 12px !important; }
1107
+ .iv-ws > .n { display: none; }
1108
+ .iv-ws .n-in { display: inline; }
1109
+ .iv-ws .n-in.w { color: var(--cz-risk); font-weight: 700; }
1110
+ .iv-ws.pl > .iv-cv { margin-left: 40px; }
1111
+ .iv-wbs > .iv-ws + .iv-ws::before, .iv-wbs > .iv-wl + .iv-ws::before, .iv-wl > .iv-ws + .iv-ws::before, .iv-wl > .iv-wl + .iv-ws::before, .iv-wbs > * + :is(.iv-wsw, .ig-dz)::before, .iv-wbs > :is(.iv-wsw, .ig-dz) + .iv-ws::before { left: 12px; }
1112
+ .iv-wl { padding: 0 12px 6px 12px; }
1113
+ .iv-wl > .iv-ws.in { margin-left: -12px; width: calc(100% + 24px); }
1114
+ .iv-tr.iv-ed { margin: 0 -12px; padding: 10px 12px 12px !important; width: calc(100% + 24px); }
1115
+ .iv-addin { margin: 0 -12px; padding: 10px 12px 4px; }
1116
+ .iv-addd { margin: 0 -12px; padding: 4px 12px 12px; }
1117
+ }
1063
1118
  html.ad-still .iv-mc-top.adl, html.ad-still .iv-mc-top.adl .iv-ask .ad-svg { animation: none !important; }
1064
1119
  @media (prefers-reduced-motion: reduce) { .iv-mc-top.adl, .iv-mc-top.adl .iv-ask .ad-svg { animation: none !important; } }
1065
1120
 
@@ -75,11 +75,11 @@ const SHOWN = new Map(); // id → the view last drawn, for the Kanban's stage
75
75
  const MCS = new Map(); // id → the PM's own changes to { mc, gx, ab } (true = open), kept in the browser
76
76
  const MCK = 'pmos.iv.mc.';
77
77
  const store = () => { try { return typeof localStorage !== 'undefined' ? localStorage : null; } catch { return null; } };
78
- /** The three folds of one initiative: the PM's own, else the defaults (the way to finish is open on a wide screen, About is folded; on a phone both are folded; About opens by itself while a success measure waits on a ✓). */
78
+ /** The three folds of one initiative: the PM's own, else the defaults (the telemetry card starts hidden in its banner; the way to finish is open on a wide screen, About is folded; on a phone both are folded; About opens by itself while a success measure waits on a ✓). */
79
79
  export function mcState(id, o = {}) {
80
80
  let mine = MCS.get(id);
81
81
  if (!mine) { mine = {}; try { const s = store(), j = s && JSON.parse(s.getItem(MCK + id) || 'null'); if (j && typeof j === 'object') for (const k of ['mc', 'gx', 'ab']) if (typeof j[k] === 'boolean') mine[k] = j[k]; } catch { mine = {}; } MCS.set(id, mine); }
82
- const dflt = { mc: true, gx: !phone(), ab: !!o.proposal };
82
+ const dflt = { mc: false, gx: !phone(), ab: !!o.proposal }; // the telemetry starts hidden in its banner (Wawan, batch 24 item 1)
83
83
  return { mc: 'mc' in mine ? mine.mc : dflt.mc, gx: 'gx' in mine ? mine.gx : dflt.gx, ab: 'ab' in mine ? mine.ab : dflt.ab };
84
84
  }
85
85
  export function mcToggle(id, key, o) {
@@ -165,7 +165,10 @@ function gatesHtml(v, o, pri, open) {
165
165
  const rows = G.open.length ? `<div class="iv-gx-l" role="list">${G.open.slice(0, 3).map(row).join('')}${G.open.length > 3 ? `<p class="iv-gx-dn">${G.open.length - 3} more in Tasks</p>` : ''}${G.done ? `<p class="iv-gx-dn">${plural(G.done, 'milestone', 'milestones')} done</p>` : ''}</div>`
166
166
  : G.done ? `<div class="iv-gx-l"><p class="iv-gx-dn">${plural(G.done, 'milestone', 'milestones')} done</p></div>` : '';
167
167
  const title = st.line && st.line.finished ? 'This stage is done' : 'To finish this stage';
168
- return `<section class="iv-gx" aria-label="${esc(title)}"><button type="button" class="iv-gx-t" data-iv-mc="gx" aria-expanded="${!!open}"><span class="chev" aria-hidden="true">${SVG(CHEV, 16, 2)}</span><span class="tx"><b>${title}</b>${sub ? `<span class="s">${esc(sub)}</span>` : ''}</span></button>`
168
+ // a phone (batch 23 item 5, canvas PmA) reads the row as the next stage and the count, with the round → beside it; the text above is the desktop's
169
+ const cnt = st.nTotal ? st.doneOf || st.nDone + ' of ' + st.nTotal + ' done' : '';
170
+ const ph = st.next ? `<span class="tx ph"><b>${esc('Next: ' + (st.next.ms ? '◆ ' : '') + st.next.name)}</b>${cnt ? `<span class="s">${esc(cnt)}</span>` : ''}</span>` : '';
171
+ return `<section class="iv-gx"${ph ? ' data-ph' : ''} aria-label="${esc(title)}"><button type="button" class="iv-gx-t" data-iv-mc="gx" aria-expanded="${!!open}"><span class="chev" aria-hidden="true">${SVG(CHEV, 16, 2)}</span><span class="tx dk"><b>${title}</b>${sub ? `<span class="s">${esc(sub)}</span>` : ''}</span>${ph}</button>`
169
172
  + `${st.nTotal ? `<span class="iv-gx-c">${pipsOf(st)}<small>${esc(st.doneOf || st.nDone + ' of ' + st.nTotal + ' done')}</small></span>` : ''}<div class="iv-gx-a">${nextHtml(st, o, pri)}</div>${open ? rows : ''}<div class="iv-gx-ft">${metaHtml(s, o)}</div></section>`;
170
173
  }
171
174
  function ctrlHtml(v, o = {}) {
@@ -305,7 +308,7 @@ function sectionHtml(v, sec, o, inner) {
305
308
  const name = sec.label || sec.name;
306
309
  const plus = st && op && has ? ED.plusHtml(sec, v.id || v.key || '', o) : '';
307
310
  const row = `<button type="button" class="iv-ws${sec.kind === 'past' || sec.kind === 'later' ? ' mut' : ''}${inner ? ' in' : ''}${plus ? ' pl' : ''}"${st ? ` data-iv-sec="${sk}"` : ''} ${has ? `data-iv-fold="${esc(key)}" aria-expanded="${op}"` : 'disabled'}>${markOf(sec)}`
308
- + `<span class="w"><b>${esc(name)}</b>${sec.sub ? `<small class="${sec.subWarn ? 'w' : ''}">${esc(sec.sub)}</small>` : ''}</span>${sec.epicKey ? `<span class="ig-key">${esc(sec.epicKey)}</span>` : ''}${sec.count ? `<span class="n${sec.warn ? ' w' : ''}">${esc(sec.count)}</span>` : ''}${has ? chev(op) : ''}</button>`;
311
+ + `<span class="w"><b>${esc(name)}</b>${sec.sub || sec.count ? `<small class="${sec.subWarn ? 'w' : ''}">${esc(sec.sub || '')}${sec.count ? `<span class="n-in${sec.warn ? ' w' : ''}">${sec.sub ? ' · ' : ''}${esc(sec.count)}</span>` : ''}</small>` : ''}</span>${sec.epicKey ? `<span class="ig-key">${esc(sec.epicKey)}</span>` : ''}${sec.count ? `<span class="n${sec.warn ? ' w' : ''}">${esc(sec.count)}</span>` : ''}${has ? chev(op) : ''}</button>`;
309
312
  // a group's own presses sit beside its fold: ⋯ on the PM's own group, the send icon on an epic (batch 11 item 20)
310
313
  const tools = G.toolsHtml(sec) + plus, head = tools ? `<div class="iv-wsw">${row}${tools}</div>` : row;
311
314
  // the PM's own groups and Not in a group take a dragged task
@@ -17,6 +17,7 @@
17
17
  * Workflows (D99, workflows-per-initiative: WfABoard WfABoardOne WfATrack WfAElse WfAPhone; pm/bd-wf.js): every initiative is on one workflow. The
18
18
  * chip row picks one (its own stages become the columns and the track's header) or All (the three categories, each row its own stages).
19
19
  * Where no record holds a value for an initiative, the element says so: nothing is invented. */
20
+ import './fetch-share.js'; // first: identical GETs in flight are asked once (page-open-speed)
20
21
  import { pmPost } from './http.js';
21
22
  import { followDoc } from './doc-follow.js';
22
23
  import { $, $$, esc, store, chatAbout, setPageGaps } from './premium-shell.js';
@@ -28,6 +28,7 @@
28
28
  * IsPhone the goal detail full screen with Readings, Moved by and Dashboards; a report setup as a sheet.
29
29
  * window.czViewer is feature-detected, with this page's side panel as the fallback. A report file opens in the report reader
30
30
  * (pm/premium-report-read.js), the report panel in the Viewer card. The page has no #dlg. */
31
+ import './fetch-share.js'; // first: identical GETs in flight are asked once (page-open-speed)
31
32
  import { $, $$, esc, toast, wireRail, setPageGaps } from './premium-shell.js';
32
33
  import { pmPost, pmGet } from './http.js';
33
34
  import * as GW from './grill-words.js'; // Grill and Forecast screens (canvas FcIntel): the goal that will miss, and what Claude's grill found