create-pmos 0.134.0 → 0.136.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 (211) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  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/payload/state/log.md +1 -1
  7. package/workspace/bin/notes-replay.js +235 -0
  8. package/workspace/c3-briefing-test.js +130 -0
  9. package/workspace/c3-foundation-test.js +159 -0
  10. package/workspace/c3-ib-items-test.js +105 -0
  11. package/workspace/c3-ib-more-test.js +120 -0
  12. package/workspace/c3-ib-triage-backlog-test.js +119 -0
  13. package/workspace/c3-ini-harness.js +66 -0
  14. package/workspace/c3-initiatives-test.js +140 -0
  15. package/workspace/c3-one-home-test.js +14 -1
  16. package/workspace/c3-reports-test.js +57 -18
  17. package/workspace/c3-settings-test.js +139 -0
  18. package/workspace/c3-work-test.js +42 -10
  19. package/workspace/fixtures/one-home-baseline.json +1 -9
  20. package/workspace/guide/briefing-cards.md +15 -3
  21. package/workspace/guide/briefing.md +12 -8
  22. package/workspace/guide/calendar-popup.md +2 -2
  23. package/workspace/guide/console-calendar.md +39 -10
  24. package/workspace/guide/index.md +1 -0
  25. package/workspace/guide/initiative-boards.md +65 -0
  26. package/workspace/guide/jira.md +3 -1
  27. package/workspace/guide/layout.md +2 -0
  28. package/workspace/guide/my-work.md +5 -3
  29. package/workspace/guide/notes.md +16 -1
  30. package/workspace/guide/reports-dashboards.md +29 -5
  31. package/workspace/guide/settings.md +68 -0
  32. package/workspace/guide/welcome.md +10 -10
  33. package/workspace/lib/calendar-cli.js +3 -2
  34. package/workspace/lib/calendar-layers.js +21 -10
  35. package/workspace/lib/calendar-tidy.js +230 -0
  36. package/workspace/lib/engine/boards.js +542 -0
  37. package/workspace/lib/engine/breakdown-ops.js +180 -0
  38. package/workspace/lib/engine/calendar-import.js +84 -0
  39. package/workspace/lib/engine/calendar-model.js +185 -0
  40. package/workspace/lib/engine/card-state.js +92 -0
  41. package/workspace/lib/engine/derived/derive.js +92 -0
  42. package/workspace/lib/engine/derived/plan.js +3 -0
  43. package/workspace/lib/engine/derived/trust.js +3 -0
  44. package/workspace/lib/engine/doc-health.js +142 -0
  45. package/workspace/lib/engine/doc-sheet.js +75 -0
  46. package/workspace/lib/engine/intake.js +78 -0
  47. package/workspace/lib/engine/outbox.js +86 -0
  48. package/workspace/lib/engine/plan-forecast.js +342 -0
  49. package/workspace/lib/engine/promises.js +29 -0
  50. package/workspace/lib/engine/records/index.js +114 -0
  51. package/workspace/lib/engine/releases.js +96 -0
  52. package/workspace/lib/engine/replies.js +99 -0
  53. package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
  54. package/workspace/lib/engine/report-families.js +175 -0
  55. package/workspace/lib/engine/roadmap-ops.js +150 -0
  56. package/workspace/lib/engine/sessions-list.js +27 -0
  57. package/workspace/lib/engine/signal-rules.js +95 -0
  58. package/workspace/lib/engine/signals/cards.js +57 -0
  59. package/workspace/lib/engine/signals/detectors.js +370 -0
  60. package/workspace/lib/engine/signals/facts-cli.js +8 -0
  61. package/workspace/lib/engine/signals/facts.js +197 -0
  62. package/workspace/lib/engine/signals/index.js +33 -0
  63. package/workspace/lib/engine/signals/kinds.js +110 -0
  64. package/workspace/lib/engine/signals/layout-paths.js +44 -0
  65. package/workspace/lib/engine/signals/options.js +24 -0
  66. package/workspace/lib/engine/signals/score.js +72 -0
  67. package/workspace/lib/engine/signals/select.js +47 -0
  68. package/workspace/lib/engine/step-planner.js +65 -0
  69. package/workspace/lib/engine/trust/claim-rules.json +93 -0
  70. package/workspace/lib/engine/trust/import-calendar.js +21 -0
  71. package/workspace/lib/engine/trust/import-guard.js +36 -0
  72. package/workspace/lib/engine/trust/ledger.js +53 -0
  73. package/workspace/lib/engine/trust/outbox.js +45 -0
  74. package/workspace/lib/engine/trust/plan.js +5 -0
  75. package/workspace/lib/engine/trust/trust.js +21 -0
  76. package/workspace/lib/engine/truth-table.js +57 -0
  77. package/workspace/lib/engine/workspace-lib.js +24 -0
  78. package/workspace/lib/factory-join.js +1 -1
  79. package/workspace/lib/facts-async.js +4 -4
  80. package/workspace/lib/headless.js +95 -0
  81. package/workspace/lib/import-overlay.js +2 -1
  82. package/workspace/lib/jira-status.js +30 -3
  83. package/workspace/lib/notes-capture.js +9 -1
  84. package/workspace/lib/notes-final.js +38 -13
  85. package/workspace/lib/notes-replay.js +125 -0
  86. package/workspace/lib/notes-screen.js +180 -0
  87. package/workspace/lib/notes-whisper.js +10 -3
  88. package/workspace/lib/outbox-store.js +10 -6
  89. package/workspace/lib/pmos-verbs.js +3 -1
  90. package/workspace/lib/proactive-cli.js +7 -0
  91. package/workspace/lib/proactive.js +15 -2
  92. package/workspace/lib/records-dir.js +29 -0
  93. package/workspace/lib/repo-script.js +36 -0
  94. package/workspace/lib/report-dashboards.js +5 -2
  95. package/workspace/lib/report-frame.js +122 -0
  96. package/workspace/lib/report-schedules.js +113 -0
  97. package/workspace/lib/route-modules.js +9 -2
  98. package/workspace/lib/routes-boards.js +734 -0
  99. package/workspace/lib/routes-brief-page.js +13 -4
  100. package/workspace/lib/routes-briefing.js +22 -11
  101. package/workspace/lib/routes-c3-init.js +9 -8
  102. package/workspace/lib/routes-c3-life.js +2 -1
  103. package/workspace/lib/routes-c3-reports.js +99 -15
  104. package/workspace/lib/routes-c3-strategy.js +7 -5
  105. package/workspace/lib/routes-c3-timeline.js +3 -2
  106. package/workspace/lib/routes-calendar-import.js +3 -2
  107. package/workspace/lib/routes-calendar-views.js +9 -4
  108. package/workspace/lib/routes-doc-health.js +2 -1
  109. package/workspace/lib/routes-doc-sheet.js +2 -1
  110. package/workspace/lib/routes-forecast.js +2 -1
  111. package/workspace/lib/routes-hooks.js +3 -2
  112. package/workspace/lib/routes-initiatives.js +2 -1
  113. package/workspace/lib/routes-intake.js +2 -1
  114. package/workspace/lib/routes-notes.js +55 -9
  115. package/workspace/lib/routes-outbox.js +23 -3
  116. package/workspace/lib/routes-pm-items.js +19 -6
  117. package/workspace/lib/routes-records.js +10 -7
  118. package/workspace/lib/routes-releases.js +2 -1
  119. package/workspace/lib/routes-report-facts.js +4 -3
  120. package/workspace/lib/routes-report-families.js +2 -1
  121. package/workspace/lib/routes-roadmap-ops.js +17 -14
  122. package/workspace/lib/routes-runs.js +6 -5
  123. package/workspace/lib/routes-settings.js +51 -0
  124. package/workspace/lib/routes-signals.js +3 -2
  125. package/workspace/lib/routes-stage-tools.js +220 -0
  126. package/workspace/lib/run-answers.js +2 -1
  127. package/workspace/lib/settings-store.js +226 -0
  128. package/workspace/lib/stage-tools.js +208 -0
  129. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  130. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  131. package/workspace/lib/work-evidence.js +73 -0
  132. package/workspace/package-lock.json +2 -2
  133. package/workspace/package.json +1 -1
  134. package/workspace/public/pm/briefing-deck.js +63 -17
  135. package/workspace/public/pm/briefing-model.js +36 -7
  136. package/workspace/public/pm/c3-ambient.js +100 -0
  137. package/workspace/public/pm/c3-brief-needs.js +26 -6
  138. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  139. package/workspace/public/pm/c3-brief-today.js +56 -21
  140. package/workspace/public/pm/c3-brief.js +2 -1
  141. package/workspace/public/pm/c3-calendar.js +161 -42
  142. package/workspace/public/pm/c3-calpop.js +5 -5
  143. package/workspace/public/pm/c3-demo-boards.js +649 -0
  144. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  145. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  146. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  147. package/workspace/public/pm/c3-demo-data.js +17 -27
  148. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  149. package/workspace/public/pm/c3-demo.js +2 -1
  150. package/workspace/public/pm/c3-dock.js +1 -1
  151. package/workspace/public/pm/c3-ib-docs.js +80 -0
  152. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  153. package/workspace/public/pm/c3-ib-items.js +201 -0
  154. package/workspace/public/pm/c3-ib-releases.js +51 -0
  155. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  156. package/workspace/public/pm/c3-ib-triage.js +178 -0
  157. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  158. package/workspace/public/pm/c3-ini-records.js +197 -0
  159. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  160. package/workspace/public/pm/c3-ini-ui.js +229 -0
  161. package/workspace/public/pm/c3-ini-work.js +139 -0
  162. package/workspace/public/pm/c3-initiatives.css +218 -0
  163. package/workspace/public/pm/c3-initiatives.js +741 -0
  164. package/workspace/public/pm/c3-plan.js +2 -2
  165. package/workspace/public/pm/c3-reports.css +47 -47
  166. package/workspace/public/pm/c3-reports.js +223 -185
  167. package/workspace/public/pm/c3-settings.css +74 -0
  168. package/workspace/public/pm/c3-settings.js +703 -0
  169. package/workspace/public/pm/c3-shell.js +102 -36
  170. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  171. package/workspace/public/pm/c3-terminal.js +2 -2
  172. package/workspace/public/pm/calendar-views.js +1 -1
  173. package/workspace/public/pm/components.css +319 -213
  174. package/workspace/public/pm/console3.css +99 -23
  175. package/workspace/public/pm/dest-notes.js +26 -4
  176. package/workspace/public/pm/notes-screen-capture.js +165 -0
  177. package/workspace/public/pm/notes-screen-view.js +98 -0
  178. package/workspace/public/pm/panel-trees.js +5 -2
  179. package/workspace/public/pm/pm-work-view.js +233 -146
  180. package/workspace/public/pm/shell-bell.js +3 -2
  181. package/workspace/public/pm/shell-recording.js +29 -8
  182. package/workspace/public/pm/tokens.css +2 -2
  183. package/workspace/public/pm/view-notes.js +46 -9
  184. package/workspace/public/pm/welcome.js +197 -89
  185. package/workspace/public/welcome.html +6 -4
  186. package/workspace/server.js +105 -6
  187. package/workspace/skills/day-check.skill +1 -1
  188. package/workspace/skills/jira-sync.skill +6 -4
  189. package/workspace/skills/morning-brief.skill +1 -1
  190. package/workspace/test-files.json +12 -3
  191. package/workspace/test-lists/c3-ib-items.json +9 -0
  192. package/workspace/test-lists/c3-ib-more.json +9 -0
  193. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  194. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  195. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  196. package/workspace/test-lists/console3-briefing.json +9 -0
  197. package/workspace/test-lists/console3-foundation.json +9 -0
  198. package/workspace/test-lists/console3-reports.json +3 -3
  199. package/workspace/test-lists/meeting-notes.json +10 -4
  200. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  201. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  202. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  203. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  204. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  205. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  206. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  207. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  208. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  209. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  210. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  211. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -2,9 +2,11 @@
2
2
  * Flow reference: the restructure mockup's 33-reports.js (dashboards with scope buttons, edit mode, library, share sheet). Not a pixel spec.
3
3
  *
4
4
  * mountReports(host, ctx) -> { dispose() }
5
- * ctx = { ws, pmGet?, drafts?(hostEl), goInit?(id, title) }
5
+ * ctx = { ws, pmGet?, deck?(hostEl), more?(hostEl), drafts?(hostEl), goInit?(id, title) }
6
6
  * ws the workspace id; every read names it
7
7
  * pmGet the shell's GET helper (default: http.js pmGet)
8
+ * deck mounts the page's headline deck into hostEl, above the tab bar (the shell mounts its Plan deck for the Reports scope).
9
+ * more mounts what the Briefing used to carry below Needs you (Handled, What I could stop, Data you can lean on) into hostEl, on the Drafts tab.
8
10
  * drafts mounts the old Reports content (the four families, the claims check, the outbox) into hostEl. Called once, the
9
11
  * first time the Drafts tab opens. Without it the Drafts tab says the drafts are not available on this page.
10
12
  * goInit opens an initiative from a row inside a report (a bet, a commitment). Default: the Initiative dashboard of it.
@@ -17,22 +19,27 @@
17
19
  * Edit: up and down arrows, Remove (with Undo), Add from the report library, Reset to default. Every change POSTs
18
20
  * /api/report-dashboards/layout { key, reports } (reports: null resets). A failed POST is toasted; the page keeps the change.
19
21
  * Share: a sheet (c3-sheet.js) with the report frame (Headline, Sections, Needs your call, Sources) for Leadership or Team.
20
- * "Save as draft" queues ONE draft through POST /api/outbox/draft. Nothing is sent. The schedule control is off and says why.
22
+ * "Make a draft now" queues ONE draft through POST /api/outbox/draft. Nothing is sent. Schedule: Not scheduled, weekly or monthly,
23
+ * for the sheet's audience, saved by POST /api/report-dashboards/schedule { key, plan, audience, ws }. A run makes a ready draft
24
+ * and a notice on the bell (lib/report-schedules.js); nothing is sent.
21
25
  * Library one row per report (data-home): the title, the question, the source and where it is used. The row opens the card in place.
22
- * Drafts ctx.drafts(host).
23
- * DATA: GET /api/report-dashboards?ws= -> { library, layouts, defaults, boards, initiatives }. In demo mode c3-demo-rep3.js answers both reads
24
- * and the layout POST. Nothing here invents a number: a report with no rows in the scope draws its builder's own empty card. */
26
+ * Drafts the dashboard drafts the Share sheet made (GET /api/outbox, subject "<dashboard> for <audience>"; Preview, Discard with Undo through
27
+ * POST /api/outbox/discard; "Made by schedule" or "Made by you"), Scheduled (each schedule and its next draft), ctx.more(host), then "Reports to send": ctx.drafts(host), the old
28
+ * report families, their claims check and the outbox queue.
29
+ * DATA: GET /api/report-dashboards?ws= -> { library, layouts, defaults, boards, initiatives, schedules }. In demo mode c3-demo-rep3.js answers both reads
30
+ * and the layout and schedule POSTs. Nothing here invents a number: a report with no rows in the scope draws its builder's own empty card. */
25
31
  import { el } from './dom.js';
26
- import { button } from './primitives.js';
32
+ import { button, ensureComponentsCss } from './primitives.js';
27
33
  import { pmGet as httpGet, pmPost } from './http.js';
28
34
  import { ensureC3Css, c3ic, c3Phone } from './pm-work-view.js';
29
35
  import { PANEL_BUILDERS, todayStr } from './c3-cal-strategy.js';
30
36
  import { makeXp, attachExpand } from './c3-expand.js';
31
37
  import { openSheet } from './c3-sheet.js';
32
38
  import { toast } from './c3-toast.js';
39
+ import '/lib/report-frame.js'; // sets self.ReportFrame (lib/report-frame.js, shared with the scheduled run)
33
40
 
34
41
  const SCOPES = [['portfolio', 'Portfolio'], ['board', 'Board'], ['initiative', 'Initiative']];
35
- const TABS = [['dash', 'Dashboards', 'report'], ['lib', 'Library', 'doc'], ['drafts', 'Drafts', 'out']];
42
+ const TABS = [['dash', 'Dashboards', 'report'], ['lib', 'Library', 'doc'], ['drafts', 'Drafts', 'mail']];
36
43
  const ICON = { north: 'target', outcomes: 'target', signals: 'alert', delivery: 'clock', maturity: 'flag', bets: 'flag', synergy: 'init' };
37
44
  const TTL = 60000;
38
45
 
@@ -64,13 +71,12 @@ function readData(get, ws, scope, id) {
64
71
  }
65
72
  const cachedData = (scope, id) => { const c = cache.get(dataKey(scope, id)); return c && Date.now() - c.at < TTL ? c.model : null; };
66
73
 
67
- /* The model the builders read, from the data answer, the way c3-strategy-panels.js build() makes it. */
68
- function modelOf(raw) {
69
- const M = { ...raw, outcomes: raw.outcomes || [], signals: raw.signals || [] };
70
- if (!M.signalsAll) M.signalsAll = M.signals.map((s) => ({ ...s }));
71
- return M;
72
- }
73
- const builderOf = (rep) => (rep && (rep.builder || rep.id)) || '';
74
+ /* The model, the report text and the frame are lib/report-frame.js, the one file the scheduled run also reads (served as /lib/report-frame.js). */
75
+ const RF = self.ReportFrame;
76
+ const { modelOf, builderOf, plural } = RF;
77
+ export const readingOf = RF.readingOf;
78
+ export const frameOf = RF.frameOf;
79
+ export const draftBody = RF.draftBody;
74
80
  function drawReport(rep, raw, bctx) {
75
81
  const M = modelOf(raw); const live = !M.demo;
76
82
  const c = { ...bctx, today: bctx.today || M.today || todayStr(Date.now()), data: { range: M.range || null, gantt: [] } };
@@ -91,120 +97,59 @@ function drawReport(rep, raw, bctx) {
91
97
  const failCard = (rep, why) => el('section', { class: 'card', 'data-rp3-fail': rep ? rep.id : '' }, el('h3', null, c3ic(ICON[builderOf(rep)] || 'report'), rep ? rep.title : 'Report'),
92
98
  el('div', { class: 'fold' }, c3ic('alert', 'sm'), el('span', null, 'This report could not be drawn: ' + why + '.')));
93
99
 
94
- /* ── the text of a report, for the share frame: one sentence, the numbers behind it, and what needs a call ─────────────── */
95
- const fmt = (v) => (v == null ? '' : Number.isInteger(v) ? String(v) : String(Math.round(v * 100) / 100));
96
- const plural = (n, one, many) => n + ' ' + (n === 1 ? one : many);
97
- const ST_TXT = { done: 'done', ok: 'on time', risk: 'at risk', late: 'delayed', over: 'overdue', park: 'parked', prop: 'proposed', none: 'no status' };
98
- const SEV_TXT = { pivot: 'at the pivot line', drift: 'drifting', watch: 'on watch', up: 'upside' };
99
- export function readingOf(rep, raw) {
100
- const M = modelOf(raw || {}); const b = builderOf(rep);
101
- const out = { line: '', nums: [], needs: [] };
102
- if (b === 'north') {
103
- const N = M.north; const P = N && N.points;
104
- if (!P || P.length < 2) { out.line = 'No north-star metric is recorded.'; return out; }
105
- const p = P.find((x) => x.at === N.now) || P[P.length - 1]; const u = N.unit || '';
106
- if (p.actual == null || !p.plan) { out.line = (N.label || 'The north star') + ' has no reading yet.'; return out; }
107
- const v = Math.round((p.actual / p.plan - 1) * 100);
108
- out.line = (N.label || 'The north star') + (v === 0 ? ' is on plan.' : ' is ' + Math.abs(v) + '% ' + (v < 0 ? 'behind' : 'ahead of') + ' plan.');
109
- out.nums.push('Now ' + fmt(p.actual) + u + ', plan ' + fmt(p.plan) + u + '.');
110
- return out;
111
- }
112
- if (b === 'outcomes') {
113
- const L = M.outcomes;
114
- if (!L.length) { out.line = 'No key result is scored.'; return out; }
115
- const on = L.filter((k) => k.status === 'ok').length;
116
- out.line = on + ' of ' + L.length + ' outcomes ' + (on === 1 ? 'is' : 'are') + ' on course.';
117
- for (const k of L) {
118
- const u = k.unit || '';
119
- out.nums.push(k.status === 'unscored' ? k.name + ': not scored.' : k.name + ': now ' + fmt(k.now) + u + ', should be ' + fmt(k.expected) + u + ', target ' + fmt(k.target) + u + '.');
120
- if (k.status === 'off') out.needs.push(k.name + ' is off course.');
121
- }
122
- return out;
123
- }
124
- if (b === 'signals') {
125
- const L = M.signals;
126
- if (!L.length) { out.line = 'No signal is open.'; return out; }
127
- const by = {}; for (const s of L) by[s.sev] = (by[s.sev] || 0) + 1;
128
- out.line = plural(L.length, 'signal is', 'signals are') + ' open: ' + Object.keys(by).map((k) => by[k] + ' ' + (SEV_TXT[k] || k)).join(', ') + '.';
129
- for (const s of L) { out.nums.push(s.t + '. ' + (s.ev || '')); if (s.sev === 'pivot' || s.sev === 'drift') out.needs.push(s.t + '.'); }
130
- return out;
131
- }
132
- if (b === 'delivery') {
133
- const R = (M.delivery && M.delivery.rows) || [];
134
- if (!R.length) { out.line = 'No commitment has a promised date this quarter.'; return out; }
135
- const n = (f) => R.filter(f).length;
136
- out.line = n((r) => r.st === 'ok' || r.st === 'done') + ' of ' + R.length + ' commitments are on time or done, ' + n((r) => r.st === 'risk') + ' at risk, ' + n((r) => r.st === 'late' || r.st === 'over') + ' delayed or overdue.';
137
- for (const r of R) { out.nums.push(r.n + ': ' + (ST_TXT[r.st] || r.st) + '.'); if (r.st === 'late' || r.st === 'over') out.needs.push(r.n + ' is ' + ST_TXT[r.st] + '.'); }
138
- return out;
139
- }
140
- if (b === 'maturity') {
141
- const T = M.maturity;
142
- if (!T || !T.pillars || !T.pillars.length) { out.line = 'No pillar maturity is recorded.'; return out; }
143
- out.line = T.pillars.filter((p) => p.proj >= p.tgt).length + ' of ' + T.pillars.length + ' pillars are on course for ' + T.target_label + '.';
144
- for (const p of T.pillars) out.nums.push(p.n + ': stage ' + p.now + ', target ' + p.tgt + ', projected ' + p.proj + '.');
145
- return out;
146
- }
147
- if (b === 'bets') {
148
- const T = M.timeline;
149
- if (!T || !T.rows || !T.rows.length) { out.line = 'No bet is recorded.'; return out; }
150
- const cp = (T.checkpoints || [])[0];
151
- out.line = plural(T.rows.length, 'bet is', 'bets are') + ' on the plan. ' + (cp ? 'The next checkpoint is ' + cp.label + '.' : 'No checkpoint is recorded.');
152
- for (const r of T.rows) out.nums.push(r.title + ': ' + (ST_TXT[r.st] || 'no status') + '.');
153
- return out;
154
- }
155
- if (b === 'synergy') {
156
- const S = M.synergy;
157
- if (!S || !S.rows || !S.rows.length) { out.line = 'No bet is recorded.'; return out; }
158
- if (!S.columns || !S.columns.length) { out.line = 'No key result exists to measure the bets against.'; return out; }
159
- const active = S.rows.filter((r) => !r.parked);
160
- const gaps = S.columns.filter((_, j) => !active.some((r) => r.weights[j] > 0));
161
- out.line = plural(active.length, 'active bet covers', 'active bets cover') + ' ' + (S.columns.length - gaps.length) + ' of ' + S.columns.length + ' ' + (S.unit || 'key results') + '.';
162
- for (const r of S.rows) out.nums.push(r.title + (r.parked ? ': parked.' : ': active.'));
163
- for (const g of gaps) out.needs.push('No active bet covers ' + g.n + '.');
164
- return out;
165
- }
166
- out.line = 'This report has no text form.';
167
- return out;
168
- }
169
-
170
- /* The report frame of a dashboard, as data (the sheet and the draft body are both drawn from it). */
171
- export function frameOf({ title, reports, raw, audience, today }) {
172
- const lead = audience !== 'Team';
173
- const sections = reports.map((r) => { const t = readingOf(r, raw); return { id: r.id, title: r.title, line: t.line, nums: lead ? [] : t.nums, needs: t.needs }; });
174
- const needs = [...new Set(sections.flatMap((s) => s.needs))];
175
- const headline = !sections.length ? 'This dashboard has no reports yet.'
176
- : needs.length ? needs[0] + (needs.length > 1 ? ' ' + plural(needs.length - 1, 'more thing needs', 'more things need') + ' your call.' : '') : sections[0].line;
177
- const sources = [...new Set(reports.map((r) => r.source).filter(Boolean))];
178
- return { title, audience: lead ? 'Leadership' : 'Team', today, headline, sections, needs, sources };
179
- }
180
- export function draftBody(F) {
181
- const L = [F.title + ' for ' + F.audience + ', ' + F.today, 'Headline: ' + F.headline, '', 'Sections'];
182
- for (const s of F.sections) { L.push(s.title, '- ' + s.line + ' [reported]'); for (const n of s.nums) L.push('- ' + n + ' [reported]'); }
183
- L.push('', 'Needs your call');
184
- if (F.needs.length) for (const n of F.needs) L.push('- ' + n + ' [reported]'); else L.push('Nothing needs your call in this scope.');
185
- L.push('', 'Sources: ' + (F.sources.length ? F.sources.join('; ') : 'none') + '. Read on ' + F.today + '.');
186
- return L.join('\n');
187
- }
188
100
 
189
101
  /* ── the page ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
190
102
  /* every control comes from primitives.js button(); the children are the console3 icon and text, which keep the suites' hooks */
191
103
  const mk = (variant, attrs, kids) => { const { onclick, disabled, class: cls, ...rest } = attrs || {}; const b = button({ label: '', variant, size: 'sm', onClick: onclick, disabled, attrs: { ...rest, class: cls } }); b.replaceChildren(...kids.filter((k) => k != null)); return b; };
192
104
  const btn = (attrs, ...kids) => mk('outline', { ...attrs, class: 'rp3-btn' }, kids);
193
- const iconBtn = (label, icon, attrs, cls) => mk('ghost', { title: label, 'aria-label': label, ...attrs, class: 'rp3-btn rp3-ib' + (cls ? ' ' + cls : '') }, [c3ic(icon, 'sm')]);
194
105
  const pill = (attrs, ...kids) => mk('ghost', attrs, kids);
106
+ const btnV = (variant, attrs, ...kids) => mk(variant, { ...attrs, class: 'rp3-btn' }, kids);
107
+ /* The mockup's UI.sect: a section card with a head row (icon, title, count) and, on the right, controls. `attrs` go on the section. */
108
+ const sect = (title, icon, count, body, right, attrs) => el('section', { ...(attrs || {}), class: 'card sect' + (attrs && attrs.class ? ' ' + attrs.class : '') },
109
+ el('div', { class: 'sh2' }, el('h2', null, icon ? c3ic(icon, 'sm') : null, title, count != null ? el('span', { class: 'bdg g' }, String(count)) : null), right || null), ...[].concat(body));
110
+ /* A builder card draws its own head (icon, title, question line). The tile's head row names the report, so only the question line stays. */
111
+ function trimHead(card) {
112
+ const h = card && card.querySelector && card.querySelector(':scope > h3');
113
+ if (!h) return card;
114
+ const line = h.querySelector('.dim');
115
+ if (line) h.replaceChildren(line); else h.remove();
116
+ return card;
117
+ }
118
+ const DAY = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
119
+ const dateOf = (iso) => { const d = new Date(iso); return isNaN(d) ? '' : DAY[d.getUTCDay()] + ' ' + d.getUTCDate() + ' ' + MON[d.getUTCMonth()]; };
120
+ /* A dashboard draft is an outbox entry the Share sheet made: its subject is "<dashboard> for <audience>". */
121
+ const DRAFT_RE = /^(.*) for (Leadership|Team)$/;
122
+ /* A schedule per dashboard (lib/report-schedules.js PLANS): it makes a ready draft and a notice on the bell. Nothing is sent. */
123
+ const SCHEDULES = [['none', 'Not scheduled'], ['weekly', 'Weekly, Mondays 08:00'], ['monthly', 'Monthly, 1st at 08:00']];
124
+ const SCHED_TXT = Object.fromEntries(SCHEDULES);
125
+ /* The next slot, host-local, as lib/schedule-logic.js nextSlot reads PLANS: Mondays 08:00, or the 1st at 08:00. */
126
+ export function nextDraftOf(plan, now = new Date()) {
127
+ const d = new Date(now); d.setHours(8, 0, 0, 0);
128
+ if (plan === 'weekly') { for (let i = 0; i < 8; i++) { if (d.getDay() === 1 && d > now) return d; d.setDate(d.getDate() + 1); } return null; }
129
+ if (plan === 'monthly') { d.setDate(1); if (d <= now) d.setMonth(d.getMonth() + 1); return d; }
130
+ return null;
131
+ }
132
+ const localDate = (d) => (d ? DAY[d.getDay()] + ' ' + d.getDate() + ' ' + MON[d.getMonth()] + ', 08:00' : '');
133
+
134
+ /* The live page, so the shell can open one of its tabs (a bell notice opens Drafts). A request before the page mounts waits for it. */
135
+ let livePage = null; let wantView = null;
136
+ export function showReportsView(view) { if (livePage) livePage.showTab(view); else wantView = view; }
195
137
 
196
138
  export function mountReports(host, ctx) {
197
- ensureC3Css(); ensureReportsCss();
139
+ ensureC3Css(); ensureComponentsCss(); ensureReportsCss();
198
140
  const C = ctx || {};
199
141
  const get = C.pmGet || httpGet;
200
- const S = { tab: 'dash', scope: 'portfolio', pick: { board: '', initiative: '' }, meta: null, metaErr: null, layouts: {}, edit: false, drafts: false, alive: true, libOpen: new Set() };
201
- const root = el('div', { class: 'rp3', 'data-rp3': '' });
142
+ const S = { schedules: {}, tab: 'dash', scope: 'portfolio', pick: { board: '', initiative: '' }, meta: null, metaErr: null, layouts: {}, edit: false, drafts: false, alive: true, libOpen: new Set(), outbox: null, outboxErr: null };
143
+ const root = el('div', { class: 'rp3 calwrap', 'data-rp3': '' });
202
144
  const stopPhone = host.closest && host.closest('.c3') ? () => {} : c3Phone(root);
203
145
  if (!(host.closest && host.closest('.c3'))) root.classList.add('c3');
204
146
  const tabBar = el('div', { class: 'seg rp3-tabs', role: 'tablist', 'aria-label': 'Reports' });
147
+ const scopeHost = el('div', { class: 'chips2 rp3-scope', role: 'group', 'aria-label': 'Scope' });
205
148
  const panes = { dash: el('div', { class: 'rp3-pane', role: 'tabpanel', 'data-rp3-pane': 'dash' }), lib: el('div', { class: 'rp3-pane', role: 'tabpanel', 'data-rp3-pane': 'lib', hidden: true }), drafts: el('div', { class: 'rp3-pane', role: 'tabpanel', 'data-rp3-pane': 'drafts', hidden: true }) };
206
- root.append(el('p', { class: 'rp3-intro' }, 'Dashboards made of reports. Each report answers one question from the repository\'s records.'), tabBar, panes.dash, panes.lib, panes.drafts);
149
+ const deckHost = el('div', { 'data-rp3-deck': '' });
150
+ root.append(deckHost, el('div', { class: 'calbar' }, tabBar, scopeHost), panes.dash, panes.lib, panes.drafts);
207
151
  host.replaceChildren(root);
152
+ if (typeof C.deck === 'function') C.deck(deckHost); else deckHost.hidden = true;
208
153
 
209
154
  const lib = () => (S.meta && S.meta.library) || [];
210
155
  const repOf = (id) => lib().find((r) => r.id === id) || null;
@@ -218,6 +163,8 @@ export function mountReports(host, ctx) {
218
163
  const defaultOf = (scope) => known(S.meta && S.meta.defaults && S.meta.defaults[scope]);
219
164
  const hasOwn = (scope, id) => Object.prototype.hasOwnProperty.call(S.layouts, keyOf(scope, id));
220
165
  const layoutOf = (scope, id) => (hasOwn(scope, id) ? known(S.layouts[keyOf(scope, id)]) : defaultOf(scope));
166
+ const schedOf = (scope, id) => S.schedules[keyOf(scope, id)] || null;
167
+ const titleOfKey = (key) => { if (key.indexOf(':') < 0) return 'Portfolio'; const i = key.indexOf(':'); const sc = key.slice(0, i); return (sc === 'board' ? 'Board: ' : 'Initiative: ') + nameOf(sc, key.slice(i + 1)); };
221
168
  const goInit = (id, title) => {
222
169
  if (C.goInit) { C.goInit(id, title); return; }
223
170
  if (!listOf('initiative').some((x) => x.id === id)) { toast('No dashboard is kept for ' + (title || id) + '.'); return; }
@@ -226,60 +173,63 @@ export function mountReports(host, ctx) {
226
173
 
227
174
  /* tabs */
228
175
  function drawTabs() {
229
- tabBar.replaceChildren(...TABS.map(([id, label, icon]) => pill({ type: 'button', role: 'tab', 'aria-selected': String(S.tab === id), tabindex: S.tab === id ? '0' : '-1', 'data-rp3-tab': id, onclick: () => showTab(id) }, c3ic(icon, 'sm'), label,
230
- id === 'lib' && S.meta ? el('span', { class: 'tag' }, String(lib().length)) : null)));
176
+ tabBar.replaceChildren(...TABS.map(([id, label, icon]) => pill({ type: 'button', role: 'tab', 'aria-selected': String(S.tab === id), 'aria-pressed': String(S.tab === id), tabindex: S.tab === id ? '0' : '-1', 'data-rp3-tab': id, onclick: () => showTab(id) }, c3ic(icon, 'sm'), label,
177
+ id === 'lib' && S.meta ? el('span', { class: 'bdg g' }, String(lib().length)) : id === 'drafts' && S.outbox && dashDrafts().length ? el('span', { class: 'bdg g', 'data-rp3-draftcount': '' }, String(dashDrafts().length)) : null)));
178
+ scopeHost.hidden = S.tab !== 'dash';
231
179
  }
232
180
  function showTab(id) {
233
181
  S.tab = id; drawTabs();
234
182
  for (const k of Object.keys(panes)) panes[k].hidden = k !== id;
235
183
  if (id === 'lib') drawLib();
236
- if (id === 'drafts' && !S.drafts) {
237
- S.drafts = true;
238
- if (typeof C.drafts === 'function') { const h = el('div', { class: 'rp3-drafts', 'data-rp3-drafts': '' }); panes.drafts.replaceChildren(h); C.drafts(h); }
239
- else panes.drafts.replaceChildren(el('div', { class: 'fold' }, c3ic('info', 'sm'), 'The report drafts are not available on this page.'));
240
- }
184
+ if (id === 'drafts') { drawDrafts(); loadOutbox(); }
241
185
  }
242
186
 
243
187
  /* ── Dashboards ── */
244
- function scopeBar() {
245
- const seg = el('div', { class: 'seg rp3-scope', role: 'group', 'aria-label': 'Scope' }, SCOPES.map(([id, label]) => pill({ 'aria-pressed': String(S.scope === id), 'data-rp3-scope': id,
246
- onclick: () => { if (S.scope === id) return; S.scope = id; S.edit = false; drawDash(); } }, label)));
247
- const out = [seg];
248
- if (S.scope !== 'portfolio') {
249
- const L = listOf(S.scope);
250
- const lab = S.scope === 'board' ? 'Board' : 'Initiative';
251
- const sel = el('select', { class: 'rp3-select', 'aria-label': 'Choose the ' + lab.toLowerCase(), 'data-rp3-pick': S.scope, disabled: !L.length,
252
- onchange: (e) => { S.pick[S.scope] = e.target.value; S.edit = false; drawDash(); } },
253
- L.length ? L.map((x) => el('option', { value: x.id, selected: x.id === S.pick[S.scope] }, S.scope === 'initiative' && x.board ? (x.title || x.id) + ' (' + nameOf('board', x.board) + ')' : (x.title || x.id))) : el('option', { value: '' }, 'None recorded'));
254
- out.push(el('label', { class: 'rp3-pick' }, el('span', { class: 'dim' }, lab), sel));
255
- }
256
- return el('div', { class: 'rp3-bar' }, out);
188
+ /* The scope pills sit on the tab row (the mockup's calbar); the pill names the picked board or initiative. */
189
+ function drawScope() {
190
+ scopeHost.replaceChildren(...SCOPES.map(([id, label]) => {
191
+ const picked = id !== 'portfolio' && S.pick[id] ? ' (' + nameOf(id, S.pick[id]) + ')' : '';
192
+ return pill({ type: 'button', class: 'pill', 'aria-pressed': String(S.scope === id), 'data-rp3-scope': id,
193
+ onclick: () => { if (S.scope === id) return; S.scope = id; S.edit = false; drawDash(); } }, label + picked);
194
+ }));
195
+ }
196
+ /* The chooser of the board or the initiative stays a real select (the mockup has one fixed name): it sits in the dashboard head. */
197
+ function chooser() {
198
+ const L = listOf(S.scope);
199
+ const lab = S.scope === 'board' ? 'Board' : 'Initiative';
200
+ const sel = el('select', { class: 'rp3-select', 'aria-label': 'Choose the ' + lab.toLowerCase(), 'data-rp3-pick': S.scope, disabled: !L.length,
201
+ onchange: (e) => { S.pick[S.scope] = e.target.value; S.edit = false; drawDash(); } },
202
+ L.length ? L.map((x) => el('option', { value: x.id, selected: x.id === S.pick[S.scope] }, S.scope === 'initiative' && x.board ? (x.title || x.id) + ' (' + nameOf('board', x.board) + ')' : (x.title || x.id))) : el('option', { value: '' }, 'None recorded'));
203
+ return el('label', { class: 'rp3-pick' }, el('span', { class: 'dim' }, lab), sel);
257
204
  }
258
205
 
259
206
  function widget(rep, i, n, model) {
260
207
  const body = !model ? el('p', { class: 'dim mono rp3-status', role: 'status', 'data-rp3-loading': rep.id }, 'Reading the repository')
261
208
  : !model.ok ? el('p', { class: 'dim mono rp3-status', role: 'status', 'data-rp3-error': rep.id }, 'This report could not be read: ' + model.error)
262
209
  : drawReport(rep, model, { goInit });
263
- const w = el('article', { class: 'rp3-w', 'data-ref-row': rep.id, 'data-rp3-w': rep.id });
264
- if (S.edit) {
265
- w.append(el('div', { class: 'rp3-wh' }, el('b', { class: 'rp3-wt' }, rep.title), el('span', { class: 'rp3-acts' },
266
- iconBtn('Move ' + rep.title + ' up', 'prev', { 'data-rp3-up': rep.id, disabled: i === 0, onclick: () => move(rep.id, -1) }, 'rp3-rot'),
267
- iconBtn('Move ' + rep.title + ' down', 'next', { 'data-rp3-down': rep.id, disabled: i === n - 1, onclick: () => move(rep.id, 1) }, 'rp3-rot'),
268
- btn({ 'data-rp3-rm': rep.id, 'aria-label': 'Remove ' + rep.title, onclick: () => remove(rep.id) }, c3ic('x', 'sm'), 'Remove'))));
269
- }
270
- w.append(body);
210
+ /* the mockup's tile: a section card, head row with the title and one ghost arrow (or the edit arrows), the report, then its source */
271
211
  const about = el('div', { class: 'rp3-about', 'data-rp3-about': rep.id }, c3ic('next', 'sm chev'), el('span', null, 'About this report'));
272
212
  const xp = makeXp(el('div', { class: 'rp3-case' },
273
213
  el('p', null, el('b', null, rep.title + '. '), rep.question || ''),
274
214
  el('p', { class: 'dim' }, 'Source: ' + (rep.source || 'not named') + '. Used on: ' + usedOn(rep.id) + '.'),
275
215
  btn({ 'data-rp3-golib': rep.id, onclick: () => openInLibrary(rep.id) }, c3ic('doc', 'sm'), 'Open in the library')));
276
216
  attachExpand(about, xp, { key: 'rp3w-' + rep.id });
277
- w.append(about, xp);
217
+ const ib = (label, icon, attrs) => mk('ghost', { title: label, 'aria-label': label, class: 'ib sm', ...attrs }, [c3ic(icon, 'sm')]);
218
+ const right = S.edit
219
+ ? el('span', { class: 'chips2' },
220
+ ib('Move ' + rep.title + ' up', 'prev', { class: 'ib sm rp3-rot', 'data-rp3-up': rep.id, disabled: i === 0, onclick: () => move(rep.id, -1) }),
221
+ ib('Move ' + rep.title + ' down', 'next', { class: 'ib sm rp3-rot', 'data-rp3-down': rep.id, disabled: i === n - 1, onclick: () => move(rep.id, 1) }),
222
+ ib('Remove ' + rep.title + ' from this dashboard', 'x', { 'data-rp3-rm': rep.id, onclick: () => remove(rep.id) }))
223
+ : el('span', { class: 'chips2' }, ib('Open ' + rep.title, 'next', { 'data-rp3-open': rep.id, title: 'Open the report', onclick: () => about.click() }));
224
+ const w = sect(rep.title, null, null, [trimHead(body), el('div', { class: 'chips' }, el('span', { class: 'chip2', title: 'Where this report reads from' }, rep.source || 'no source named')), about, xp], right,
225
+ { 'data-ref-row': rep.id, 'data-rp3-w': rep.id });
226
+ w.classList.add('rp3-w');
278
227
  return w;
279
228
  }
280
229
 
281
230
  function drawDash() {
282
231
  if (!S.alive) return;
232
+ drawScope();
283
233
  const P = panes.dash;
284
234
  if (S.metaErr) { P.replaceChildren(el('div', { class: 'fold', role: 'status', 'data-rp3-error': 'meta' }, c3ic('alert', 'sm'), 'The dashboards could not be read: ' + S.metaErr)); return; }
285
235
  if (!S.meta) { P.replaceChildren(el('p', { class: 'dim mono rp3-status', role: 'status', 'data-rp3-loading': 'meta' }, 'Reading the repository')); return; }
@@ -289,34 +239,39 @@ export function mountReports(host, ctx) {
289
239
  const ids = noScope ? [] : layoutOf(S.scope, id);
290
240
  const own = !noScope && hasOwn(S.scope, id);
291
241
  root.classList.toggle('editing', S.edit);
292
- const head = el('div', { class: 'rp3-head' },
293
- el('div', { class: 'rp3-ttl' }, el('h2', { 'data-rp3-title': '' }, noScope ? (S.scope === 'board' ? 'Board' : 'Initiative') : dashTitle()),
294
- el('span', { class: 'dim', 'data-rp3-count': String(ids.length) }, noScope ? '' : plural(ids.length, 'report', 'reports') + (own ? ', your layout' : ', the default layout'))),
295
- noScope ? null : el('span', { class: 'rp3-acts' },
296
- btn({ 'data-rp3-share': '', onclick: () => share(ids) }, c3ic('out', 'sm'), 'Share'),
297
- S.edit ? btn({ 'data-rp3-reset': '', disabled: !own, title: own ? 'Return this scope to its default layout' : 'This scope shows its default layout', onclick: () => resetLayout() }, c3ic('undo', 'sm'), 'Reset to default') : null,
298
- btn({ 'data-rp3-edit': '', 'aria-pressed': String(S.edit), onclick: () => { S.edit = !S.edit; drawDash(); } }, c3ic('edit', 'sm'), S.edit ? 'Done editing' : 'Edit dashboard')));
299
- const kids = [scopeBar(), head];
242
+ drawScope(); // again: the pick above may have changed the pill's name
243
+ /* the mockup's .lh: report icon, bold title, one dim line (scope, count, schedule), then Share and Edit dashboard */
244
+ const head = el('div', { class: 'lh rp3-head' }, el('span', { class: 'ki' }, c3ic('report')),
245
+ el('div', { class: 'rp3-min0' }, el('b', { 'data-rp3-title': '' }, noScope ? (S.scope === 'board' ? 'Board' : 'Initiative') : dashTitle()),
246
+ el('div', { class: 'dim' }, noScope ? '' : [el('span', { 'data-rp3-count': String(ids.length) }, plural(ids.length, 'report', 'reports') + (own ? ', your layout' : ', the default layout')),
247
+ el('span', { 'data-rp3-schedline': '' }, ' · ' + (schedOf(S.scope, id) ? 'Scheduled: ' + SCHED_TXT[schedOf(S.scope, id).plan] + ', draft only' : 'Not scheduled'))]),
248
+ S.scope !== 'portfolio' ? chooser() : null),
249
+ noScope ? null : el('span', { class: 'chips2' },
250
+ btnV('outline', { 'data-rp3-share': '', onclick: () => share(ids) }, c3ic('out', 'sm'), 'Share'),
251
+ S.edit ? btnV('ghost', { 'data-rp3-reset': '', disabled: !own, title: own ? 'Return this scope to its default layout' : 'This scope shows its default layout', onclick: () => resetLayout() }, c3ic('undo', 'sm'), 'Reset to default') : null,
252
+ btnV(S.edit ? 'primary' : 'outline', { 'data-rp3-edit': '', 'aria-pressed': String(S.edit), onclick: () => { S.edit = !S.edit; drawDash(); } }, c3ic(S.edit ? 'check' : 'edit', 'sm'), S.edit ? 'Done editing' : 'Edit dashboard')));
253
+ const kids = [head];
300
254
  if (noScope) {
301
255
  kids.push(el('div', { class: 'fold', 'data-rp3-empty': S.scope }, c3ic('moon', 'sm'), S.scope === 'board'
302
256
  ? 'No board is recorded. A board is the domain of an initiative\'s anchor. It appears here when an initiative names one.'
303
257
  : 'No initiative is recorded. An initiative appears here when the repository holds its record.'));
304
- P.replaceChildren(...kids); return;
258
+ P.replaceChildren(el('div', { class: 'calbody brief' }, ...kids)); return;
305
259
  }
306
- if (S.edit) kids.push(el('p', { class: 'dim rp3-note' }, 'Use the arrows to move a report. Remove takes it off this dashboard only: it stays in the library. Your layout is kept for this scope in the runtime state, not in the repository.'));
260
+ if (S.edit) kids.push(el('div', { class: 'fold a rp3-note' }, c3ic('edit', 'sm'), 'Editing: reorder with the arrows, remove what this scope does not need, add from the library below. Reports are never deleted from the library.'));
307
261
  const model = cachedData(S.scope, id);
308
- const grid = el('div', { class: 'rp3-grid', 'data-rp3-grid': keyOf(S.scope, id) });
262
+ const grid = el('div', { class: 'tiles rp3-grid' + (S.edit ? ' editing' : ''), 'data-rp3-grid': keyOf(S.scope, id) });
309
263
  const reps = ids.map(repOf);
310
264
  reps.forEach((r, i) => grid.append(widget(r, i, reps.length, model)));
311
265
  kids.push(reps.length ? grid : el('div', { class: 'fold', 'data-rp3-empty': 'layout' }, c3ic('moon', 'sm'), 'This dashboard has no reports. Press Edit dashboard and add one from the library.'));
312
266
  if (S.edit) {
313
267
  const free = lib().filter((r) => !ids.includes(r.id) && fits(r, S.scope));
314
- kids.push(el('section', { class: 'sec rp3-add', 'data-rp3-addlist': '' }, el('div', { class: 'sh' }, el('h3', null, 'Add from the report library'), el('span', { class: 'r' }, free.length + ' available')),
315
- free.length ? el('ul', { class: 'list' }, free.map((r) => el('li', { class: 'rp3-addrow', 'data-rp3-free': r.id }, el('div', { class: 'nb' }, el('b', null, r.title), el('div', { class: 'dim' }, r.question || '')),
316
- btn({ 'data-rp3-add': r.id, 'aria-label': 'Add ' + r.title, onclick: () => add(r.id) }, c3ic('plus', 'sm'), 'Add'))))
317
- : el('div', { class: 'fold' }, c3ic('check', 'sm'), 'Every report that fits this scope is on the dashboard.')));
268
+ kids.push(sect('Add from the report library', 'report', free.length, free.length
269
+ ? el('ul', { class: 'list', role: 'list' }, free.map((r) => el('li', { class: 'nrow', 'data-rp3-free': r.id }, el('span', { class: 'ki' }, c3ic('report')),
270
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, r.title), el('div', { class: 'dim rp3-sub' }, r.question || '')),
271
+ el('div', { class: 'acts' }, btnV('outline', { 'data-rp3-add': r.id, 'aria-label': 'Add ' + r.title, onclick: () => add(r.id) }, c3ic('plus', 'sm'), 'Add')))))
272
+ : el('div', { class: 'fold' }, c3ic('check', 'sm'), 'Every report that fits this scope is on the dashboard.'), null, { 'data-rp3-addlist': '' }));
318
273
  }
319
- P.replaceChildren(...kids);
274
+ P.replaceChildren(el('div', { class: 'calbody brief' }, ...kids));
320
275
  if (!model && reps.length) {
321
276
  const want = keyOf(S.scope, id);
322
277
  readData(get, C.ws, S.scope, id).then((m) => {
@@ -359,20 +314,23 @@ export function mountReports(host, ctx) {
359
314
  /* ── Share: the report frame for an audience, one draft in the outbox, a schedule that is not available ── */
360
315
  function share(ids) {
361
316
  const scope = S.scope; const id = curId(); const title = dashTitle(); const reps = ids.map(repOf).filter(Boolean);
362
- const st = { aud: 'Leadership', saved: {}, busy: false, msg: '' };
317
+ const cur = schedOf(scope, id);
318
+ const st = { aud: cur ? cur.audience : 'Leadership', saved: {}, busy: false, msg: '', sched: cur ? cur.plan : 'none', saving: false };
363
319
  const body = el('div', { class: 'rp3-share', 'data-rp3-sheet': '' });
364
320
  const paint = (raw) => {
365
321
  const today = (raw && raw.today) || todayStr(Date.now());
366
322
  const F = frameOf({ title, reports: reps, raw: raw && raw.ok ? raw : {}, audience: st.aud, today });
367
- const sec = (name, ...kids) => el('section', { 'data-rp3-frame': name }, el('h4', null, name), ...kids);
323
+ /* the frame in the mockup's cards: a .lh head, then Headline, Sections, Needs your call and Sources as section cards */
324
+ const sec = (name, icon, count, ...kids) => sect(name, icon, count, kids, null, { 'data-rp3-frame': name });
368
325
  const frame = el('div', { class: 'rp3-frame', 'data-rp3-aud': st.aud },
369
- el('div', { class: 'chips2' }, el('span', { class: 'tag' }, 'Draft'), el('b', null, title), el('span', { class: 'dim' }, 'For ' + st.aud + ', ' + today)),
370
- sec('Headline', el('p', { class: 'rp3-hl' }, F.headline)),
371
- sec('Sections', ...(F.sections.length ? F.sections.flatMap((s) => [el('h5', null, s.title), el('p', null, s.line), s.nums.length ? el('ul', { class: 'rp3-nums', 'data-rp3-nums': s.id }, s.nums.map((t) => el('li', null, t))) : null]) : [el('p', { class: 'dim' }, 'Nothing to show.')])),
372
- sec('Needs your call', ...(F.needs.length ? F.needs.map((t) => el('p', null, t)) : [el('p', { class: 'dim' }, 'Nothing needs your call in this scope.')])),
373
- sec('Sources', el('p', { class: 'dim' }, (F.sources.length ? F.sources.join('; ') + '. ' : '') + 'Read from the repository on ' + today + '.')));
326
+ el('div', { class: 'lh' }, el('span', { class: 'ki' }, c3ic('mail')), el('div', { class: 'rp3-min0' }, el('b', null, title), el('div', { class: 'dim' }, 'For ' + st.aud + ' · ' + today)), el('span', { class: 'chip2' }, 'Draft')),
327
+ sec('Headline', null, null, el('div', { class: 'hero' }, el('b', { class: 'rp3-hl' }, F.headline))),
328
+ sec('Sections', null, F.sections.length, ...(F.sections.length ? [el('ul', { class: 'chgl' }, F.sections.map((s) => el('li', null, c3ic('report', 'sm'),
329
+ el('span', null, el('b', null, s.title), el('br'), s.line, s.nums.length ? el('ul', { class: 'rp3-nums', 'data-rp3-nums': s.id }, s.nums.map((t) => el('li', null, t))) : null), el('span', null))))] : [el('div', { class: 'fold' }, 'Nothing to show.')])),
330
+ sec('Needs your call', null, F.needs.length, ...(F.needs.length ? [el('ul', { class: 'chgl' }, F.needs.map((t) => el('li', { class: 'warn' }, c3ic('flag', 'sm'), el('span', null, t), el('span', null))))] : [el('div', { class: 'fold' }, 'Nothing needs your call in this scope.')])),
331
+ sec('Sources', null, null, el('div', { class: 'chips' }, F.sources.map((x) => el('span', { class: 'chip2' }, x))), el('div', { class: 'dim rp3-sub' }, 'Read from the repository on ' + today + '.')));
374
332
  const saved = st.saved[st.aud];
375
- const saveBtn = btn({ class: 'rp3-btn rp3-pri', 'data-rp3-draft': '', disabled: st.busy || !!saved || !raw || !raw.ok,
333
+ const saveBtn = btnV('outline', { 'data-rp3-draft': '', disabled: st.busy || !!saved || !raw || !raw.ok,
376
334
  onclick: async () => {
377
335
  st.busy = true; paint(raw);
378
336
  const r = await pmPost('/api/outbox/draft', { channel: 'customer-note', recipient: st.aud, subject: title + ' for ' + st.aud, body: draftBody(F) });
@@ -382,18 +340,33 @@ export function mountReports(host, ctx) {
382
340
  st.msg = r.entry && r.entry.state === 'blocked' ? 'Saved in the outbox, but the claims check blocks it: ' + ((r.entry.claims && r.entry.claims.refused || []).join(' ') || 'a claim has no label') + '. Nothing was sent.'
383
341
  : 'Saved as one draft in the outbox. Nothing was sent: no channel is connected.';
384
342
  } else st.msg = 'The draft was not saved: ' + ((r && (r.error || r.reason)) || 'no answer') + '. Nothing was sent.';
385
- toast(st.msg); paint(raw);
386
- } }, c3ic('doc', 'sm'), saved ? 'Saved as draft' : st.busy ? 'Saving' : 'Save as draft');
387
- body.replaceChildren(
388
- el('div', { class: 'sec' }, el('div', { class: 'dim' }, 'Audience'),
389
- el('div', { class: 'seg', role: 'group', 'aria-label': 'Audience' }, ['Leadership', 'Team'].map((a) => pill({ 'aria-pressed': String(st.aud === a), 'data-rp3-audbtn': a, onclick: () => { st.aud = a; st.msg = ''; paint(raw); } }, a))),
390
- el('p', { class: 'dim' }, st.aud === 'Leadership' ? 'Short: one sentence per report, and what needs your call.' : 'Fuller: adds the numbers behind each sentence.')),
343
+ toast(st.msg); paint(raw); loadOutbox();
344
+ } }, c3ic('mail', 'sm'), saved ? 'Saved as draft' : st.busy ? 'Saving' : 'Make a draft now');
345
+ /* Schedule: one per dashboard. A run makes a ready draft in the outbox and a notice on the bell; nothing is sent (lib/report-schedules.js). */
346
+ const next = nextDraftOf(st.sched);
347
+ const saveSched = async () => {
348
+ st.saving = true; paint(raw);
349
+ const r = await pmPost('/api/report-dashboards/schedule', { key: keyOf(scope, id), plan: st.sched, audience: st.aud, ws: C.ws || null });
350
+ st.saving = false;
351
+ if (r && r.ok) {
352
+ S.schedules = r.schedules || {};
353
+ st.msg = st.sched === 'none' ? 'Schedule removed' : 'Scheduled: ' + SCHED_TXT[st.sched] + '. You get a draft and a notification.';
354
+ toast(st.msg); drawDash(); if (S.tab === 'drafts') drawDrafts();
355
+ } else { st.msg = 'The schedule was not saved: ' + ((r && (r.error || r.reason)) || 'no answer') + '.'; toast({ text: st.msg, tone: 'error', timeout: 6000 }); }
356
+ paint(raw);
357
+ };
358
+ const schedule = sect('Schedule', 'cal', null, [
359
+ el('div', { class: 'seg', role: 'group', 'aria-label': 'Send', 'data-rp3-sched': '' }, SCHEDULES.map(([k, label]) => pill({ type: 'button', 'aria-pressed': String(k === st.sched), 'data-sched': k, onclick: () => { st.sched = k; st.msg = ''; paint(raw); } }, label))),
360
+ next ? el('div', { class: 'dim rp3-sub', 'data-rp3-next': '' }, 'Next draft: ' + localDate(next)) : null,
361
+ el('div', { class: 'fold w rp3-why', id: 'rp3-sched-why' }, c3ic('alert', 'sm'), 'No sender channel is connected yet. A scheduled send makes a ready draft and notifies you. Nothing is sent to ' + (st.aud === 'Leadership' ? 'leadership' : 'the team') + ' until you connect a channel.'),
362
+ el('div', { class: 'chips2' }, btnV('primary', { 'data-rp3-sched-save': '', disabled: st.saving, onclick: saveSched }, st.saving ? 'Saving' : 'Save schedule'), saveBtn),
363
+ el('p', { class: 'dim rp3-sub rp3-flat', role: 'status', 'data-rp3-share-msg': '' }, st.msg || 'Make a draft now puts one draft in the outbox. Nothing is sent from this page.')],
364
+ null, { 'data-rp3-schedule': '', class: 'rp3-stack' });
365
+ body.replaceChildren(el('div', { class: 'calbody brief' },
366
+ el('div', { class: 'calbar' }, el('div', { class: 'seg', role: 'group', 'aria-label': 'Audience' }, ['Leadership', 'Team'].map((a) => pill({ type: 'button', 'aria-pressed': String(st.aud === a), 'data-rp3-audbtn': a, onclick: () => { st.aud = a; st.msg = ''; paint(raw); } }, a))),
367
+ el('span', { class: 'dim rp3-sub' }, st.aud === 'Leadership' ? 'Short: one sentence per report, and what needs your call.' : 'Fuller: adds the numbers behind each sentence.')),
391
368
  raw ? (raw.ok ? frame : el('div', { class: 'fold', role: 'status' }, c3ic('alert', 'sm'), 'The reports could not be read: ' + raw.error)) : el('p', { class: 'dim mono', role: 'status' }, 'Reading the repository'),
392
- el('div', { class: 'rp3-acts rp3-wrap' }, saveBtn),
393
- el('p', { class: 'dim', role: 'status', 'data-rp3-share-msg': '' }, st.msg || 'Save as draft puts one draft in the outbox. Nothing is sent from this page.'),
394
- el('section', { class: 'sec', 'data-rp3-schedule': '' }, el('div', { class: 'sh' }, el('h3', null, 'Schedule')),
395
- el('label', { class: 'rp3-pick' }, el('span', { class: 'dim' }, 'Send'), el('select', { class: 'rp3-select', disabled: true, 'aria-describedby': 'rp3-sched-why', 'data-rp3-sched': '' }, el('option', null, 'Not scheduled'))),
396
- el('p', { class: 'rp3-why', id: 'rp3-sched-why' }, 'Scheduled sending is not available yet, because nothing is sent outward from this page. Nothing is stored or run for a schedule.')));
369
+ schedule));
397
370
  };
398
371
  const cached = cachedData(scope, id);
399
372
  openSheet({ title: 'Share: ' + title, icon: 'out', wide: true, body });
@@ -402,8 +375,12 @@ export function mountReports(host, ctx) {
402
375
  }
403
376
 
404
377
  /* ── Library: the home of every report ── */
405
- function usedOn(rid) {
378
+ function usedOn(rid, short) {
406
379
  if (!S.meta) return 'nowhere yet';
380
+ if (short) { // the mockup's chip: the scopes it is on, by short name
381
+ const sc = ['portfolio', 'board', 'initiative'].filter((s) => (s !== 'portfolio' ? (defaultOf(s).includes(rid) || Object.keys(S.layouts).some((k) => k.startsWith(s + ':') && known(S.layouts[k]).includes(rid))) : layoutOf(s, '').includes(rid)));
382
+ return sc.length ? sc.map((s) => s[0].toUpperCase() + s.slice(1)).join(', ') : 'no dashboard';
383
+ }
407
384
  const on = [];
408
385
  if (layoutOf('portfolio', '').includes(rid)) on.push('Portfolio');
409
386
  for (const [scope, label] of [['board', 'Board'], ['initiative', 'Initiative']]) {
@@ -422,7 +399,7 @@ export function mountReports(host, ctx) {
422
399
  const row = el('li', { class: 'nrow rp3-librow', 'data-home': r.id },
423
400
  el('span', { class: 'ki hot' }, c3ic(ICON[builderOf(r)] || 'report')),
424
401
  el('div', { class: 'nb' }, el('span', { class: 'tt' }, r.title), el('div', { class: 'dim' }, r.question || ''),
425
- el('div', { class: 'chips' }, el('span', { class: 'tag' }, r.source || 'no source named'), el('span', { class: 'dim', 'data-rp3-used': r.id }, 'Used on: ' + usedOn(r.id))), xp));
402
+ el('div', { class: 'chips' }, el('span', { class: 'chip2' }, r.source || 'no source named'), el('span', { class: 'chip2', 'data-rp3-used': r.id }, 'On: ' + usedOn(r.id, true))), xp));
426
403
  const fill = () => {
427
404
  const m = cachedData('portfolio', '');
428
405
  if (m) { card.replaceChildren(m.ok ? drawReport(r, m, { goInit }) : el('p', { class: 'dim mono' }, 'This report could not be read: ' + m.error)); return; }
@@ -433,8 +410,8 @@ export function mountReports(host, ctx) {
433
410
  if (S.libOpen.has(r.id)) api.open();
434
411
  return row;
435
412
  });
436
- P.replaceChildren(el('p', { class: 'dim rp3-note' }, 'Every report the workspace can draw lives here. A dashboard points at these. Removing a report from a dashboard keeps it here. Open a row to see the report across the portfolio.'),
437
- el('ul', { class: 'list', 'data-rp3-lib': '' }, rows));
413
+ P.replaceChildren(el('div', { class: 'calbody brief' }, el('div', { class: 'fold rp3-note' }, c3ic('report', 'sm'), 'Every report PMOS can draw lives here. Dashboards point at these; changing a report changes it everywhere it is used. Open a row to see the report across the portfolio.'),
414
+ sect('Reports', 'report', rows.length, el('ul', { class: 'list', role: 'list', 'data-rp3-lib': '' }, rows))));
438
415
  }
439
416
  function openInLibrary(rid) {
440
417
  S.libOpen.add(rid); showTab('lib');
@@ -442,13 +419,74 @@ export function mountReports(host, ctx) {
442
419
  if (row) { try { row.focus({ preventScroll: true }); row.scrollIntoView({ block: 'nearest' }); } catch { /* not focusable */ } }
443
420
  }
444
421
 
422
+ /* ── Drafts: the dashboard drafts, what is scheduled (nothing), what the Briefing used to carry, and the old report families ── */
423
+ const dashDrafts = () => ((S.outbox || []).filter((e) => e.channel === 'customer-note' && DRAFT_RE.test(e.subject || '')));
424
+ function loadOutbox() {
425
+ return Promise.resolve(get('/api/outbox')).then((r) => {
426
+ if (!S.alive) return;
427
+ if (r && r.ok) { S.outbox = r.entries || []; S.outboxErr = null; } else S.outboxErr = (r && (r.error || r.reason)) || 'not available';
428
+ drawTabs(); if (S.tab === 'drafts') drawDrafts();
429
+ }, (e) => { S.outboxErr = String((e && e.message) || e); if (S.tab === 'drafts') drawDrafts(); });
430
+ }
431
+ const draftRows = () => dashDrafts().map((x) => {
432
+ const m = DRAFT_RE.exec(x.subject);
433
+ return el('li', { class: 'nrow', 'data-draft-row': x.id }, el('span', { class: 'ki' }, c3ic('mail')),
434
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, m[1]),
435
+ el('div', { class: 'chips' }, el('span', { class: 'chip2' }, 'Draft'), el('span', { class: 'chip2' }, c3ic('user', 'sm'), 'For ' + m[2]),
436
+ x.created_at ? el('span', { class: 'chip2' }, c3ic('cal', 'sm'), dateOf(x.created_at)) : null, el('span', { class: 'tag', 'data-rp3-madeby': x.made_by === 'schedule' ? 'schedule' : 'you' }, x.made_by === 'schedule' ? 'Made by schedule' : 'Made by you'), el('span', { class: 'tag' }, x.state === 'blocked' ? 'Blocked by the claims check' : 'Not sent'))),
437
+ el('div', { class: 'acts' }, btnV('outline', { 'data-draft': 'preview', 'data-id': x.id, onclick: () => previewDraft(x, m) }, c3ic('eye', 'sm'), 'Preview'),
438
+ btnV('ghost', { 'data-draft': 'discard', 'data-id': x.id, onclick: () => discardDraft(x) }, c3ic('x', 'sm'), 'Discard')));
439
+ });
440
+ function previewDraft(x, m) {
441
+ openSheet({ title: 'Draft: ' + m[1], icon: 'mail', wide: true, body: el('div', { class: 'calbody brief' },
442
+ el('div', { class: 'lh' }, el('span', { class: 'ki' }, c3ic('mail')), el('div', { class: 'rp3-min0' }, el('b', null, m[1]), el('div', { class: 'dim' }, 'For ' + m[2] + (x.created_at ? ' · ' + dateOf(x.created_at) : ''))), el('span', { class: 'chip2' }, 'Draft')),
443
+ sect('Text', 'doc', null, el('pre', { class: 'rp3-draft-text', 'data-rp3-draft-text': '' }, x.body || ''))) });
444
+ }
445
+ async function discardDraft(x) {
446
+ const r = await pmPost('/api/outbox/discard', { id: x.id });
447
+ if (!r || !r.ok) { toast({ text: 'The draft was not discarded: ' + ((r && (r.error || r.reason)) || 'no answer'), tone: 'error', timeout: 6000 }); return; }
448
+ S.outbox = (S.outbox || []).filter((e) => e.id !== x.id); drawTabs(); drawDrafts();
449
+ toast({ text: 'Draft discarded', actionLabel: 'Undo', timeout: 6000, onAction: async () => { await pmPost('/api/outbox/discard', { id: x.id, undo: true }); loadOutbox(); } });
450
+ }
451
+ function scheduledList() {
452
+ const L = Object.values(S.schedules || {});
453
+ if (!L.length) return el('div', { class: 'fold' }, c3ic('clock', 'sm'), 'No dashboard is scheduled. Share a dashboard to set one.');
454
+ return el('ul', { class: 'list', role: 'list' }, L.map((x) => el('li', { class: 'nrow', 'data-rp3-sched-row': x.key }, el('span', { class: 'ki' }, c3ic('cal')),
455
+ el('div', { class: 'nb' }, el('span', { class: 'tt' }, titleOfKey(x.key)),
456
+ el('div', { class: 'chips' }, el('span', { class: 'chip2' }, SCHED_TXT[x.plan] || x.plan), el('span', { class: 'chip2' }, c3ic('user', 'sm'), 'For ' + x.audience),
457
+ el('span', { class: 'chip2' }, 'Next draft ' + localDate(x.next_run ? new Date(x.next_run) : nextDraftOf(x.plan))),
458
+ x.last_status === 'failed' ? el('span', { class: 'tag', 'data-rp3-sched-failed': '' }, 'The last run made no draft: ' + (x.last_error || 'no reason recorded')) : null)),
459
+ el('span', null))));
460
+ }
461
+ function drawDrafts() {
462
+ const P = panes.drafts;
463
+ if (!S.draftsHost) {
464
+ S.draftsHost = { list: el('div', { 'data-rp3-draftlist': '', class: 'rp3-stack4' }), more: el('div', { 'data-rp3-more': '', class: 'rp3-stack4' }), send: el('div', { class: 'rp3-drafts', 'data-rp3-drafts': '' }) };
465
+ let sendBody;
466
+ if (typeof C.drafts === 'function') { sendBody = S.draftsHost.send; C.drafts(sendBody); } else sendBody = el('div', { class: 'fold' }, c3ic('info', 'sm'), 'The report drafts are not available on this page.');
467
+ if (typeof C.more === 'function') C.more(S.draftsHost.more); else S.draftsHost.more.hidden = true;
468
+ S.draftsHost.sendSect = sect('Reports to send', 'out', null, [el('div', { class: 'dim rp3-sub' }, 'The earlier report families, each with its claims check. Nothing is sent without your press.'), sendBody]);
469
+ P.replaceChildren(el('div', { class: 'calbody brief' }, S.draftsHost.list, S.draftsHost.more, S.draftsHost.sendSect));
470
+ }
471
+ const rows = draftRows();
472
+ S.draftsHost.list.replaceChildren(
473
+ el('div', { class: 'fold w' }, c3ic('alert', 'sm'), 'Nothing here is sent. No sender channel is connected yet, so a schedule only makes a ready draft and notifies you.'),
474
+ sect('Drafts', 'mail', S.outbox ? rows.length : null, S.outboxErr ? el('div', { class: 'fold', role: 'status' }, c3ic('alert', 'sm'), 'The drafts could not be read: ' + S.outboxErr)
475
+ : !S.outbox ? el('p', { class: 'dim mono rp3-status', role: 'status' }, 'Reading the repository')
476
+ : rows.length ? el('ul', { class: 'list', role: 'list', 'data-rp3-drafts-list': '' }, rows) : el('div', { class: 'fold' }, c3ic('check', 'sm'), 'No drafts. Share a dashboard to make one.')),
477
+ sect('Scheduled', 'cal', null, scheduledList(), null, { 'data-rp3-scheduled': '' }));
478
+ }
479
+
445
480
  /* boot */
446
- drawTabs(); drawDash();
481
+ drawTabs(); drawDash(); loadOutbox();
447
482
  Promise.resolve(get('/api/report-dashboards' + (C.ws ? '?ws=' + encodeURIComponent(C.ws) : ''))).then((r) => {
448
483
  if (!S.alive) return;
449
- if (r && r.ok) { S.meta = r; S.layouts = { ...(r.layouts || {}) }; } else S.metaErr = (r && (r.error || r.reason)) || 'not available';
450
- drawTabs(); drawDash(); if (S.tab === 'lib') drawLib();
484
+ if (r && r.ok) { S.meta = r; S.layouts = { ...(r.layouts || {}) }; S.schedules = { ...(r.schedules || {}) }; } else S.metaErr = (r && (r.error || r.reason)) || 'not available';
485
+ drawTabs(); drawDash(); if (S.tab === 'lib') drawLib(); if (S.tab === 'drafts') drawDrafts();
451
486
  }, (e) => { S.metaErr = String((e && e.message) || e); drawDash(); });
452
487
 
453
- return { dispose() { S.alive = false; stopPhone(); root.remove(); }, root };
488
+ const api = { dispose() { S.alive = false; if (livePage === api) livePage = null; stopPhone(); root.remove(); }, root, showTab: (v) => { if (panes[v]) showTab(v); } };
489
+ livePage = api;
490
+ if (wantView) { const v = wantView; wantView = null; api.showTab(v); }
491
+ return api;
454
492
  }