create-pmos 0.133.0 → 0.135.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/bin/notes-replay.js +235 -0
- package/workspace/c3-briefing-test.js +130 -0
- package/workspace/c3-foundation-test.js +159 -0
- package/workspace/c3-ib-items-test.js +105 -0
- package/workspace/c3-ib-more-test.js +120 -0
- package/workspace/c3-ib-triage-backlog-test.js +119 -0
- package/workspace/c3-ini-harness.js +66 -0
- package/workspace/c3-initiatives-test.js +140 -0
- package/workspace/c3-one-home-test.js +14 -1
- package/workspace/c3-reports-test.js +57 -18
- package/workspace/c3-settings-test.js +139 -0
- package/workspace/c3-work-test.js +42 -10
- package/workspace/fixtures/one-home-baseline.json +1 -9
- package/workspace/guide/briefing-cards.md +15 -3
- package/workspace/guide/briefing.md +12 -8
- package/workspace/guide/calendar-popup.md +2 -2
- package/workspace/guide/console-calendar.md +39 -10
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +65 -0
- package/workspace/guide/jira.md +3 -1
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/my-work.md +5 -3
- package/workspace/guide/notes.md +35 -2
- package/workspace/guide/reports-dashboards.md +29 -5
- package/workspace/guide/settings.md +68 -0
- package/workspace/guide/welcome.md +10 -10
- package/workspace/lib/calendar-cli.js +3 -2
- package/workspace/lib/calendar-layers.js +21 -10
- package/workspace/lib/calendar-tidy.js +230 -0
- package/workspace/lib/engine/boards.js +542 -0
- package/workspace/lib/engine/breakdown-ops.js +180 -0
- package/workspace/lib/engine/calendar-import.js +84 -0
- package/workspace/lib/engine/calendar-model.js +185 -0
- package/workspace/lib/engine/card-state.js +92 -0
- package/workspace/lib/engine/derived/derive.js +92 -0
- package/workspace/lib/engine/derived/plan.js +3 -0
- package/workspace/lib/engine/derived/trust.js +3 -0
- package/workspace/lib/engine/doc-health.js +142 -0
- package/workspace/lib/engine/doc-sheet.js +75 -0
- package/workspace/lib/engine/intake.js +78 -0
- package/workspace/lib/engine/outbox.js +86 -0
- package/workspace/lib/engine/plan-forecast.js +342 -0
- package/workspace/lib/engine/promises.js +29 -0
- package/workspace/lib/engine/records/index.js +114 -0
- package/workspace/lib/engine/releases.js +96 -0
- package/workspace/lib/engine/replies.js +99 -0
- package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
- package/workspace/lib/engine/report-families.js +175 -0
- package/workspace/lib/engine/roadmap-ops.js +150 -0
- package/workspace/lib/engine/sessions-list.js +27 -0
- package/workspace/lib/engine/signal-rules.js +95 -0
- package/workspace/lib/engine/signals/cards.js +57 -0
- package/workspace/lib/engine/signals/detectors.js +370 -0
- package/workspace/lib/engine/signals/facts-cli.js +8 -0
- package/workspace/lib/engine/signals/facts.js +197 -0
- package/workspace/lib/engine/signals/index.js +33 -0
- package/workspace/lib/engine/signals/kinds.js +110 -0
- package/workspace/lib/engine/signals/layout-paths.js +44 -0
- package/workspace/lib/engine/signals/options.js +24 -0
- package/workspace/lib/engine/signals/score.js +72 -0
- package/workspace/lib/engine/signals/select.js +47 -0
- package/workspace/lib/engine/step-planner.js +65 -0
- package/workspace/lib/engine/trust/claim-rules.json +93 -0
- package/workspace/lib/engine/trust/import-calendar.js +21 -0
- package/workspace/lib/engine/trust/import-guard.js +36 -0
- package/workspace/lib/engine/trust/ledger.js +53 -0
- package/workspace/lib/engine/trust/outbox.js +45 -0
- package/workspace/lib/engine/trust/plan.js +5 -0
- package/workspace/lib/engine/trust/trust.js +21 -0
- package/workspace/lib/engine/truth-table.js +57 -0
- package/workspace/lib/engine/workspace-lib.js +24 -0
- package/workspace/lib/factory-join.js +1 -1
- package/workspace/lib/facts-async.js +4 -4
- package/workspace/lib/headless.js +95 -0
- package/workspace/lib/import-overlay.js +2 -1
- package/workspace/lib/jira-status.js +30 -3
- package/workspace/lib/notes-audio.js +28 -9
- package/workspace/lib/notes-capture.js +136 -0
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-live-guard.js +117 -0
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +139 -16
- package/workspace/lib/notes.js +4 -2
- package/workspace/lib/outbox-store.js +10 -6
- package/workspace/lib/pmos-verbs.js +3 -1
- package/workspace/lib/proactive-cli.js +7 -0
- package/workspace/lib/proactive.js +15 -2
- package/workspace/lib/repo-script.js +36 -0
- package/workspace/lib/report-dashboards.js +5 -2
- package/workspace/lib/report-frame.js +122 -0
- package/workspace/lib/report-schedules.js +113 -0
- package/workspace/lib/route-modules.js +9 -2
- package/workspace/lib/routes-boards.js +734 -0
- package/workspace/lib/routes-brief-page.js +13 -4
- package/workspace/lib/routes-briefing.js +22 -11
- package/workspace/lib/routes-c3-init.js +4 -3
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +96 -13
- package/workspace/lib/routes-c3-strategy.js +4 -3
- package/workspace/lib/routes-c3-timeline.js +3 -2
- package/workspace/lib/routes-calendar-import.js +3 -2
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-doc-health.js +2 -1
- package/workspace/lib/routes-doc-sheet.js +2 -1
- package/workspace/lib/routes-forecast.js +2 -1
- package/workspace/lib/routes-hooks.js +3 -2
- package/workspace/lib/routes-initiatives.js +2 -1
- package/workspace/lib/routes-intake.js +2 -1
- package/workspace/lib/routes-notes.js +113 -15
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +3 -2
- package/workspace/lib/routes-releases.js +2 -1
- package/workspace/lib/routes-report-facts.js +4 -3
- package/workspace/lib/routes-report-families.js +2 -1
- package/workspace/lib/routes-roadmap-ops.js +5 -4
- package/workspace/lib/routes-runs.js +6 -5
- package/workspace/lib/routes-settings.js +51 -0
- package/workspace/lib/routes-signals.js +3 -2
- package/workspace/lib/routes-stage-tools.js +220 -0
- package/workspace/lib/run-answers.js +2 -1
- package/workspace/lib/settings-store.js +226 -0
- package/workspace/lib/stage-tools.js +208 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
- package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
- package/workspace/lib/work-evidence.js +73 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +63 -17
- package/workspace/public/pm/briefing-model.js +36 -7
- package/workspace/public/pm/c3-ambient.js +100 -0
- package/workspace/public/pm/c3-brief-needs.js +26 -6
- package/workspace/public/pm/c3-brief-pulse.js +12 -4
- package/workspace/public/pm/c3-brief-today.js +56 -21
- package/workspace/public/pm/c3-brief.js +2 -1
- package/workspace/public/pm/c3-calendar.js +161 -42
- package/workspace/public/pm/c3-calpop.js +5 -5
- package/workspace/public/pm/c3-demo-boards.js +649 -0
- package/workspace/public/pm/c3-demo-brief-page.js +1 -1
- package/workspace/public/pm/c3-demo-cal4.js +3 -2
- package/workspace/public/pm/c3-demo-cal5.js +52 -0
- package/workspace/public/pm/c3-demo-data.js +17 -27
- package/workspace/public/pm/c3-demo-rep3.js +46 -5
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-dock.js +1 -1
- package/workspace/public/pm/c3-ib-docs.js +80 -0
- package/workspace/public/pm/c3-ib-initiatives.js +134 -0
- package/workspace/public/pm/c3-ib-items.js +201 -0
- package/workspace/public/pm/c3-ib-releases.js +51 -0
- package/workspace/public/pm/c3-ib-sprints.js +55 -0
- package/workspace/public/pm/c3-ib-triage.js +178 -0
- package/workspace/public/pm/c3-ii-tabs.js +240 -0
- package/workspace/public/pm/c3-ini-records.js +197 -0
- package/workspace/public/pm/c3-ini-timeline.js +278 -0
- package/workspace/public/pm/c3-ini-ui.js +229 -0
- package/workspace/public/pm/c3-ini-work.js +139 -0
- package/workspace/public/pm/c3-initiatives.css +218 -0
- package/workspace/public/pm/c3-initiatives.js +741 -0
- package/workspace/public/pm/c3-plan.js +2 -2
- package/workspace/public/pm/c3-reports.css +47 -47
- package/workspace/public/pm/c3-reports.js +223 -185
- package/workspace/public/pm/c3-settings.css +74 -0
- package/workspace/public/pm/c3-settings.js +703 -0
- package/workspace/public/pm/c3-shell.js +102 -36
- package/workspace/public/pm/c3-terminal-icons.js +2 -2
- package/workspace/public/pm/c3-terminal.js +2 -2
- package/workspace/public/pm/calendar-views.js +26 -13
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +118 -23
- package/workspace/public/pm/dest-notes.js +26 -4
- package/workspace/public/pm/notes-screen-capture.js +165 -0
- package/workspace/public/pm/notes-screen-view.js +98 -0
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/pm-work-view.js +233 -146
- package/workspace/public/pm/shell-bell.js +3 -2
- package/workspace/public/pm/shell-recording.js +148 -17
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +53 -11
- package/workspace/public/pm/welcome.js +197 -89
- package/workspace/public/welcome.html +6 -4
- package/workspace/server.js +105 -6
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/jira-sync.skill +6 -4
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/test-files.json +14 -3
- package/workspace/test-lists/c3-ib-items.json +9 -0
- package/workspace/test-lists/c3-ib-more.json +9 -0
- package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
- package/workspace/test-lists/c3-initiative-boards.json +6 -0
- package/workspace/test-lists/calendar-tidy-up.json +6 -0
- package/workspace/test-lists/console3-briefing.json +9 -0
- package/workspace/test-lists/console3-foundation.json +9 -0
- package/workspace/test-lists/console3-reports.json +3 -3
- package/workspace/test-lists/meeting-notes.json +12 -4
- package/workspace/test-lists/workspace-headless-jobs.json +5 -0
- package/workspace/test-lists/workspace-package-script-closure.json +6 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
- package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
- package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
- package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
- package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
- package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
- package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
- package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
- 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
|
-
* "
|
|
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
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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', '
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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: '
|
|
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'
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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('
|
|
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(
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
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
|
-
|
|
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: '
|
|
370
|
-
sec('Headline', el('
|
|
371
|
-
sec('Sections',
|
|
372
|
-
|
|
373
|
-
sec('
|
|
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 =
|
|
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('
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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: '
|
|
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('
|
|
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
|
-
|
|
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
|
}
|