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.
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- 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/payload/state/log.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 +16 -1
- 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-capture.js +9 -1
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +10 -3
- 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/records-dir.js +29 -0
- 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 +9 -8
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +99 -15
- package/workspace/lib/routes-c3-strategy.js +7 -5
- 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 +55 -9
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +10 -7
- 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 +17 -14
- 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 +1 -1
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +99 -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 +29 -8
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +46 -9
- 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 +12 -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 +10 -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
|
@@ -37,7 +37,7 @@ export function installBriefPage() {
|
|
|
37
37
|
const u = new URL(raw, location.origin);
|
|
38
38
|
const method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
39
39
|
if (u.origin === location.origin) {
|
|
40
|
-
if (method === 'GET' && u.pathname === '/api/brief-page') return Promise.resolve(json({ ok: true, date: '2026-10-08', today: TODAY.map((m) => ({ ...m, minutes: (Date.parse(m.end) - Date.parse(m.start)) / 60000, state: Date.parse(m.end) <= Date.now() ? 'past' : 'later' })).map((m, i, a) => ({ ...m, state: m.state === 'past' ? 'past' : (a.findIndex((x) => x.state !== 'past' && Date.parse(x.start) > Date.now()) === i ? 'next' : 'later') })), needs: NEEDS.filter((n) => !gone[n.id]) }));
|
|
40
|
+
if (method === 'GET' && u.pathname === '/api/brief-page') return Promise.resolve(json({ ok: true, date: '2026-10-08', me: 'Wawan', today: TODAY.map((m) => ({ ...m, minutes: (Date.parse(m.end) - Date.parse(m.start)) / 60000, state: Date.parse(m.end) <= Date.now() ? 'past' : 'later' })).map((m, i, a) => ({ ...m, state: m.state === 'past' ? 'past' : (a.findIndex((x) => x.state !== 'past' && Date.parse(x.start) > Date.now()) === i ? 'next' : 'later') })), needs: NEEDS.filter((n) => !gone[n.id]) }));
|
|
41
41
|
const m = /^\/api\/pm-items\/(done|notyet|remove|reopen|undo)$/.exec(u.pathname);
|
|
42
42
|
if (method !== 'GET' && m) {
|
|
43
43
|
let b = {}; try { b = init && init.body ? JSON.parse(init.body) : {}; } catch { b = {}; }
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* The content reuses the demo's initiatives (checkout, sso, search, returns), its people and its meetings and work items
|
|
10
10
|
* (c3-demo-data.js). Real mode never loads this file. Nothing here reaches the server. */
|
|
11
11
|
import * as D from './c3-demo-data.js';
|
|
12
|
+
import { withCalendarTidy } from './c3-demo-cal5.js'; // the tidy-up suggestions and the one meeting they need
|
|
12
13
|
|
|
13
14
|
const DAY = 86400000;
|
|
14
15
|
const addDays = (iso, n) => new Date(Date.parse(iso + 'T00:00:00Z') + n * DAY).toISOString().slice(0, 10);
|
|
@@ -56,7 +57,7 @@ const SUGGEST = [
|
|
|
56
57
|
const MIN = 90; const START = 9 * 60; const END = 18 * 60;
|
|
57
58
|
|
|
58
59
|
function meetingsOn(date) {
|
|
59
|
-
const day = D.calendarViews('/api/calendar-views?view=day&date=' + date);
|
|
60
|
+
const day = withCalendarTidy(D.calendarViews('/api/calendar-views?view=day&date=' + date));
|
|
60
61
|
return ((day.cells[0] && day.cells[0].events) || []).filter((e) => e.kind !== 'milestone')
|
|
61
62
|
.map((e) => [minOf(e.at), minOf(e.end)]).filter((x) => x[0] !== null && x[1] !== null).sort((a, b) => a[0] - b[0]);
|
|
62
63
|
}
|
|
@@ -110,5 +111,5 @@ const CONTEXT = {
|
|
|
110
111
|
export function withCalendarPage(out) {
|
|
111
112
|
if (!out || !out.range) return out;
|
|
112
113
|
const { start, end } = out.range;
|
|
113
|
-
return { ...out, layers: layersIn(start, end), free: freeIn(start, end), context: CONTEXT };
|
|
114
|
+
return withCalendarTidy({ ...out, layers: layersIn(start, end), free: freeIn(start, end), context: CONTEXT });
|
|
114
115
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* pm/c3-demo-cal5.js — DEMO fixtures for the Calendar tidy-up (initiative calendar-tidy-up) on /c3.html?demo=1.
|
|
2
|
+
* c3-demo-cal4.js passes its answer through withCalendarTidy(), which adds:
|
|
3
|
+
* one meeting "Checkout design review" on Thu 15 Oct, 09:45 to 10:30, about Checkout v2. It overlaps the Standup.
|
|
4
|
+
* tidy the suggestions of the range, from the rules of lib/calendar-tidy.js applied by hand to the demo's
|
|
5
|
+
* meetings and initiatives: one of each severity across the zooms, with an overlap and a review too early.
|
|
6
|
+
* tidy_total, tidy_note
|
|
7
|
+
* With &tidy=none on the page address the three tidy keys are left out, to show the state of a server that sends none;
|
|
8
|
+
* with &tidy=unread the list is empty and tidy_note names a rule that could not run.
|
|
9
|
+
* Each suggestion points at a demo meeting or initiative that the page draws. Real mode never loads this file. Nothing
|
|
10
|
+
* here reaches the server, and nothing writes. */
|
|
11
|
+
|
|
12
|
+
const REVIEW = { id: 'm6@2026-10-15', title: 'Checkout design review', at: '2026-10-15T09:45:00', end: '2026-10-15T10:30:00', kind: 'ready', initiative: 'checkout' };
|
|
13
|
+
|
|
14
|
+
const ev = (id) => ({ type: 'event', id });
|
|
15
|
+
const init = (id) => ({ type: 'initiative', id });
|
|
16
|
+
|
|
17
|
+
/* [id, rule, severity, date, title, why, suggestion, refs] */
|
|
18
|
+
const TIDY = [
|
|
19
|
+
['overlap:m1@2026-10-15+m6@2026-10-15', 'overlap', 'bad', '2026-10-15', 'Standup overlaps Checkout design review',
|
|
20
|
+
'Standup runs from 09:30 to 10:00 and Checkout design review starts at 09:45, so you cannot attend both in full.',
|
|
21
|
+
'Move Checkout design review to 10:00 or shorten Standup.', [ev('m1@2026-10-15'), ev('m6@2026-10-15')]],
|
|
22
|
+
['likely-delay:ini:checkout:promised', 'likely-delay', 'bad', '2026-10-24', 'The promised date of Checkout v2 is likely to slip',
|
|
23
|
+
'Checkout v2 is delayed, and the promised date of Checkout v2 is on 2026-10-24.',
|
|
24
|
+
'Agree a new date for the promised date with the people who depend on it, or cut its scope to keep 2026-10-24.', [init('checkout')]],
|
|
25
|
+
['no-buffer:m3@2026-10-08+m4@2026-10-08', 'no-buffer', 'warn', '2026-10-08', 'No break between Customer interview and Sprint planning',
|
|
26
|
+
'Sprint planning starts at 14:00, the minute Customer interview ends. You have no time to move, take notes or prepare.',
|
|
27
|
+
'Move Sprint planning to 14:10 or end Customer interview 10 minutes earlier.', [ev('m3@2026-10-08'), ev('m4@2026-10-08')]],
|
|
28
|
+
['prep-risk:m4@2026-10-08', 'prep-risk', 'warn', '2026-10-08', 'Little time to prepare for Sprint planning',
|
|
29
|
+
'Sprint planning has 1 prep step left, and only 0 minutes of free working time remain before it. Each step needs about 30 minutes.',
|
|
30
|
+
'Block time before Sprint planning for the prep, or move Sprint planning to a later day.', [ev('m4@2026-10-08')]],
|
|
31
|
+
['likely-delay:ini:search:promised', 'likely-delay', 'warn', '2026-10-30', 'The promised date of Search relevance is likely to slip',
|
|
32
|
+
'Search relevance is at risk, and the promised date of Search relevance is on 2026-10-30.',
|
|
33
|
+
'Agree a new date for the promised date with the people who depend on it, or cut its scope to keep 2026-10-30.', [init('search')]],
|
|
34
|
+
['review-early:m6@2026-10-15', 'review-early', 'info', '2026-10-15', 'Checkout design review comes before what it reviews',
|
|
35
|
+
'Checkout design review is on 2026-10-15, but the promised date of Checkout v2 is on 2026-10-24 and is not done yet.',
|
|
36
|
+
'Move Checkout design review to 2026-10-24 or later, or keep it as a progress check and say so in the invite.', [ev('m6@2026-10-15'), init('checkout')]],
|
|
37
|
+
].map(([id, rule, severity, date, title, why, suggestion, refs]) => ({ id, rule, severity, date, title, why, suggestion, refs }));
|
|
38
|
+
|
|
39
|
+
/** The calendar-views answer with the extra meeting in its day cell and the tidy-up keys of its range. */
|
|
40
|
+
export function withCalendarTidy(out) {
|
|
41
|
+
if (!out || !out.range) return out;
|
|
42
|
+
const { start, end } = out.range;
|
|
43
|
+
const cells = (out.cells || []).map((c) => (c.start === c.end && c.start === REVIEW.at.slice(0, 10)
|
|
44
|
+
? { ...c, events: [...(c.events || []).filter((e) => e.id !== REVIEW.id), { ...REVIEW }] } : c));
|
|
45
|
+
// /c3.html?demo=1&tidy=none shows the page of a server that sends no tidy-up list (the honest absent state)
|
|
46
|
+
// and &tidy=unread the state of a server that could not read the initiatives (an empty list with the reason)
|
|
47
|
+
let knob = ''; try { knob = new URLSearchParams(location.search).get('tidy') || ''; } catch { knob = ''; }
|
|
48
|
+
if (knob === 'none') return { ...out, cells };
|
|
49
|
+
if (knob === 'unread') return { ...out, cells, tidy: [], tidy_total: 0, tidy_note: { 'likely-delay': 'no initiative status was read, so initiatives cannot be checked' } };
|
|
50
|
+
const tidy = TIDY.filter((s) => s.date >= start && s.date <= end).map((s) => ({ ...s, refs: s.refs.map((r) => ({ ...r })) }));
|
|
51
|
+
return { ...out, cells, tidy, tidy_total: tidy.length, tidy_note: {} };
|
|
52
|
+
}
|
|
@@ -71,53 +71,43 @@ const card = (key, tag, headline, sub, rich, o) => ({ key, tag, headline, sub, h
|
|
|
71
71
|
|
|
72
72
|
const DECISION = card('decision', 'Critical', 'Cut Checkout v2 to 3 flows to hold 24 Oct?', 'Dara needs it before Sprint planning. Jira shows 3 blockers on the full scope.',
|
|
73
73
|
{ kind: 'chain', ring: { frac: 0.36, lines: ['5h 48m', 'left'], tone: C.acc }, steps: [['Decide', 'by 14:00', C.acc], ['Sprint planning', 'blocked', C.bad], ['Ship 24 Oct', 'at risk', C.warn]] },
|
|
74
|
-
{ kpis: [['5h 48m', 'left', C.acc], ['3', 'blockers', C.bad], ['82%', 'confidence', C.ok]], factsX: [['user', 'Dara is waiting'], ['alert', '3 blockers on full scope']],
|
|
74
|
+
{ pov: 'Decision', kpis: [['5h 48m', 'left', C.acc], ['3', 'blockers', C.bad], ['82%', 'confidence', C.ok]], factsX: [['user', 'Dara is waiting'], ['alert', '3 blockers on full scope']],
|
|
75
75
|
move: { suggested: { id: 'cut', label: 'Cut to 3 flows', conf: 82 }, options: [{ id: 'cut', label: 'Cut to 3 flows', slipAfter: 0 }, { id: 'add', label: 'Cut and add one engineer', slipAfter: 0 }, { id: 'keep', label: 'Keep the full scope', slipAfter: 7 }], note: 'Dara\'s estimate for flow 4 is a range of 3 to 6 days.' },
|
|
76
76
|
deadline: { days: 0.24, source: 'calendar', text: 'Sprint planning at 14:00, 5h 48m left' }, waitsFor: 'Dara', why: { text: 'Sprint planning cannot start until scope is decided; the 24 Oct date depends on it.', impact: 0.9, urgency: 0.85, irreversible: 0.4, score: 0.82 },
|
|
77
77
|
pinned: false });
|
|
78
78
|
const METRIC = card('metric', 'Just in', 'Conversion is down 0.2 points', 'Landed at 07:30. The biggest loss is cart to address: 45% of carts stop.',
|
|
79
79
|
{ kind: 'num', big: '3.1', unit: '%', delta: '-0.2 pts since last week', tone: C.bad, arr: [3.5, 3.4, 3.4, 3.3, 3.3, 3.2, 3.1], target: 4 },
|
|
80
|
-
{ kpis: [['3.1%', 'now', C.bad], ['4.0%', 'target', C.mfg], ['45%', 'lost at address', C.warn]], factsX: [['bolt', 'Data landed 07:30'], ['alert', 'Mobile shows the same pattern']],
|
|
80
|
+
{ pov: 'Early signal', kpis: [['3.1%', 'now', C.bad], ['4.0%', 'target', C.mfg], ['45%', 'lost at address', C.warn]], factsX: [['bolt', 'Data landed 07:30'], ['alert', 'Mobile shows the same pattern']],
|
|
81
81
|
move: { suggested: { id: 'x', label: 'Draft an experiment for the address step', conf: 71 }, options: [{ id: 'x', label: 'Draft an experiment for the address step' }, { id: 'w', label: 'Watch 3 more days' }, { id: 't', label: 'Tell Priya in the 10:30 update' }], note: 'One week of data. A seasonal dip is not ruled out.' },
|
|
82
82
|
deadline: null, why: { text: 'Conversion is the earliest warning on the checkout bet.', impact: 0.8, urgency: 0.6, irreversible: 0.2, score: 0.71 } });
|
|
83
83
|
const DATE = card('date', 'Critical', 'Checkout v2 may slip 7 days', 'Forecast Oct 31 vs promised Oct 24 (+7 days). Scope is the cause, not capacity.',
|
|
84
84
|
{ kind: 'rows', rows: [['target', 'Promised to leadership on 2 Oct', '', '24 Oct'], ['alert', 'Forecast on the full scope', 'bad', '31 Oct'], ['scale', 'Cause: scope, not capacity', 'warn', '']] },
|
|
85
|
-
{ kpis: [['+7 d', 'vs promise', C.bad], ['24 Oct', 'promised', 'var(--foreground)'], ['74%', 'confidence', C.acc]], factsX: [['target', 'Promised to leadership on 2 Oct'], ['alert', 'Cause: scope, not capacity']],
|
|
85
|
+
{ pov: 'Deadline moved', kpis: [['+7 d', 'vs promise', C.bad], ['24 Oct', 'promised', 'var(--foreground)'], ['74%', 'confidence', C.acc]], factsX: [['target', 'Promised to leadership on 2 Oct'], ['alert', 'Cause: scope, not capacity']],
|
|
86
86
|
move: { suggested: { id: 'warn', label: 'Warn Priya now, with the fix attached', conf: 74 }, options: [{ id: 'warn', label: 'Warn Priya now, with the fix attached' }, { id: 'wait', label: 'Wait for the 14:00 decision' }] }, deadline: null });
|
|
87
87
|
const INSIGHT = card('insight', 'Opportunity', 'Label printing is the main pain', '3 of 3 calls name it so far. Interview 3 is today at 13:00.',
|
|
88
88
|
{ kind: 'bars', bars: [['Labels are the main pain', 1, C.ok, '3 of 3'], ['Customers miss the 30-day window', 0.67, C.warn, '2 of 3'], ['Support is a workaround', 0, 'var(--ring)', 'untested']] },
|
|
89
|
-
{ kpis: [['3 of 3', 'calls agree', C.ok], ['2 of 5', 'interviews done', C.acc], ['76%', 'confidence', C.opp]], factsX: [['cal', 'Interview 3 at 13:00'], ['target', 'Stop rule after interview 5']],
|
|
89
|
+
{ pov: 'Discovery', kpis: [['3 of 3', 'calls agree', C.ok], ['2 of 5', 'interviews done', C.acc], ['76%', 'confidence', C.opp]], factsX: [['cal', 'Interview 3 at 13:00'], ['target', 'Stop rule after interview 5']],
|
|
90
90
|
move: { suggested: { id: 'a', label: 'Add a support-route question to interview 3', conf: 76 }, options: [{ id: 'a', label: 'Add a support-route question to interview 3' }, { id: 'k', label: 'Keep the guide as it is' }] } });
|
|
91
91
|
const SSO = card('sso', 'Big win', 'SSO is live with 3 customers', 'No incidents in 7 days. Release on 14 Oct is on track.',
|
|
92
92
|
{ kind: 'ring', done: 5, total: 8, cap: '5 of 8 gates', sub: 'release checklist' },
|
|
93
|
-
{ kpis: [['3', 'customers live', C.ok], ['0', 'incidents', C.ok], ['5 of 8', 'gates', C.acc]], factsX: [['user', 'Marta and Priya have not heard yet'], ['flag', 'First enterprise milestone']],
|
|
93
|
+
{ pov: 'Milestone', kpis: [['3', 'customers live', C.ok], ['0', 'incidents', C.ok], ['5 of 8', 'gates', C.acc]], factsX: [['user', 'Marta and Priya have not heard yet'], ['flag', 'First enterprise milestone']],
|
|
94
94
|
move: { suggested: { id: 's', label: 'Lead the weekly status with it', conf: 90 }, options: [{ id: 's', label: 'Lead the weekly status with it' }, { id: 'n', label: 'Send Marta and Priya a note now' }, { id: 'h', label: 'Hold until the release' }] } });
|
|
95
95
|
const SEARCH = card('search', 'Small win', 'Search got 28% faster', 'p95 latency fell from 420 to 300 ms after the 6 Oct merge.',
|
|
96
96
|
{ kind: 'num', big: '300', unit: 'ms', delta: '-120 ms since the merge', tone: C.ok, arr: [420, 418, 410, 405, 300, 296, 300], target: 350 },
|
|
97
|
-
{ kpis: [['-28%', 'p95 latency', C.ok], ['300 ms', 'now', 'var(--foreground)'], ['350 ms', 'target', C.mfg]], factsX: [['check', 'Merged 6 Oct, CI green'], ['report', 'Measured over 7 days']],
|
|
97
|
+
{ pov: 'Milestone', kpis: [['-28%', 'p95 latency', C.ok], ['300 ms', 'now', 'var(--foreground)'], ['350 ms', 'target', C.mfg]], factsX: [['check', 'Merged 6 Oct, CI green'], ['report', 'Measured over 7 days']],
|
|
98
98
|
move: { suggested: { id: 'b', label: 'Bank it in Friday\'s report', conf: 93 }, options: [{ id: 'b', label: 'Bank it in Friday\'s report' }, { id: 's', label: 'Skip' }] } });
|
|
99
99
|
const BOARD = card('board', 'Opportunity', 'The board update is 70% ready', 'Three sections wait on Thursday\'s numbers. It sends on 20 Dec.',
|
|
100
100
|
{ kind: 'ring', done: 7, total: 10, cap: '7 of 10 sections', sub: '3 wait on numbers' },
|
|
101
|
-
{ kpis: [['70%', 'ready', C.acc], ['3', 'waiting', C.warn], ['20 Dec', 'sends', 'var(--foreground)']], factsX: [['clock', 'Sends 20 Dec'], ['spark', 'The agent is filling the rest']],
|
|
101
|
+
{ pov: 'Prep', kpis: [['70%', 'ready', C.acc], ['3', 'waiting', C.warn], ['20 Dec', 'sends', 'var(--foreground)']], factsX: [['clock', 'Sends 20 Dec'], ['spark', 'The agent is filling the rest']],
|
|
102
102
|
move: { suggested: { id: 'f', label: 'Let the agent fill from Thursday\'s numbers', conf: 79 }, options: [{ id: 'f', label: 'Let the agent fill from Thursday\'s numbers' }, { id: 'r', label: 'Review the sections yourself first' }] } });
|
|
103
103
|
|
|
104
|
-
/* Calendar tab (the mockup's
|
|
105
|
-
const
|
|
106
|
-
{ kind: '
|
|
107
|
-
{ kpis: [['
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
{ kpis: [['4', 'due today', C.acc], ['2 d', 'oldest late', C.bad], ['80%', 'confidence', C.ok]], factsX: [['clock', '4 due today'], ['alert', 'Both block 24 Oct']] });
|
|
112
|
-
const CAL_DF = card('df', 'Opportunity', '1.8 h of focus at 11:15', 'The longest free window today. Best used before the 14:00 decision.',
|
|
113
|
-
{ kind: 'tiles', tiles: [['1.8 h', 'free', C.opp], ['11:15', 'starts', 'var(--foreground)'], ['74%', 'confidence', C.acc]] },
|
|
114
|
-
{ kpis: [['1.8 h', 'free', C.opp], ['11:15', 'starts', 'var(--foreground)'], ['74%', 'confidence', C.acc]], factsX: [['clock', '11:15 to 13:00'], ['doc', 'Pre-read is half drafted']] });
|
|
115
|
-
const CAL_DL = card('dl', 'Small win', 'Get back 45 minutes', 'Skip Standup and trim one sync. 3.8 h of meetings today, 3.3 h tied to a promise.',
|
|
116
|
-
{ kind: 'tiles', tiles: [['3.3 h', 'tied to a promise', C.acc], ['30 min', 'routine', C.mfg], ['45 min', 'you could win back', C.ok]] },
|
|
117
|
-
{ kpis: [['45 min', 'to win back', C.ok], ['3.8 h', 'meetings', C.acc], ['71%', 'confidence', C.ok]], factsX: [['check', 'Standup has an async summary'], ['clock', 'Sync can end in 15 min']] });
|
|
118
|
-
const CAL_DN = card('dn', 'Just in', 'Marta confirmed 3 slots and Priya replied', 'Two quick items since you last looked. One unblocks the interview.',
|
|
119
|
-
{ kind: 'rows', rows: [['mail', 'Marta: 3 interview slots confirmed', 'ok', '08:05'], ['user', 'Priya: replied on the numbers', 'ok', '08:10']] },
|
|
120
|
-
{ kpis: [['2', 'new', C.acc], ['<2 min', 'each', C.ok], ['88%', 'confidence', C.ok]], factsX: [['bolt', '2 new'], ['clock', 'Both take under 2 minutes']] });
|
|
104
|
+
/* the Calendar tab's headline deck (the mockup's CARDS.calendar, c5 and c6): a Conflict and a Prep at risk, from the demo's own day */
|
|
105
|
+
const CAL_CF = card('cf', 'Small win', 'Customer interview runs into Sprint planning', '13:00 to 14:00, then 14:00 to 15:00. There is no break between them.',
|
|
106
|
+
{ kind: 'tiles', tiles: [['0 min', 'break', C.bad], ['14:10', 'planning could start', C.ok], ['80%', 'confidence', C.acc]] },
|
|
107
|
+
{ pov: 'Conflict', kpis: [['0 min', 'buffer', C.bad], ['14:10', 'proposed start', C.ok]], factsX: [['clock', 'You are free again at 15:30']], move: { suggested: { id: 's', label: 'Start Sprint planning at 14:10', conf: 80 }, options: [{ id: 's', label: 'Start Sprint planning at 14:10' }] } });
|
|
108
|
+
const CAL_PR = card('pr', 'Critical', 'Sprint planning has little time to prepare', 'One prep step is left and no free working time remains before 14:00.',
|
|
109
|
+
{ kind: 'tiles', tiles: [['1', 'step left', C.bad], ['0 min', 'free before', C.warn], ['70%', 'confidence', C.acc]] },
|
|
110
|
+
{ pov: 'Prep at risk', kpis: [['1', 'prep step', C.bad], ['0 min', 'free', C.warn]], factsX: [['clock', 'Starts 14:00']], move: { suggested: { id: 'b', label: 'Block time before Sprint planning', conf: 70 }, options: [{ id: 'b', label: 'Block time before Sprint planning' }] } });
|
|
121
111
|
|
|
122
112
|
/* Work tab (the deck per view: Inbox, Today, Upcoming, Waiting, Someday, Review). The decisions the mockup's Triage cards pointed at
|
|
123
113
|
* ("5 need a real decision", "The agent drafted 3 replies") live in Briefing · Needs you now, so no Work card names them. */
|
|
@@ -196,10 +186,10 @@ const unfilled = (c) => ({ ...c });
|
|
|
196
186
|
const PLAN_BRIEF = [DECISION, METRIC, DATE, INSIGHT, SSO].map(unfilled);
|
|
197
187
|
const BY_SCOPE = {
|
|
198
188
|
brief: PLAN_BRIEF,
|
|
199
|
-
cal: [
|
|
189
|
+
cal: [CAL_CF, CAL_PR],
|
|
200
190
|
work: [WK_KO, WK_KR, WORK_KI, WORK_KRV, WK_KDN],
|
|
201
191
|
init: [INI_DIR, INI_MAT, INI_SIG, { ...SSO, key: 'ysso' }, { ...BOARD, key: 'ybd' }],
|
|
202
|
-
rep: [REP_LEAD, REP_NEXT, { ...
|
|
192
|
+
rep: [REP_LEAD, REP_NEXT, { ...SEARCH, key: 'rsr' }], // report-specific cards only: the Reports page draws this deck above its tabs, and one home holds (the board and SSO cards live on Briefing and Initiatives)
|
|
203
193
|
};
|
|
204
194
|
const LENS_ORDER = {
|
|
205
195
|
Plan: ['decision', 'metric', 'date', 'insight', 'sso'],
|
|
@@ -252,7 +242,7 @@ export function briefing(url) {
|
|
|
252
242
|
if (wl && WORK_LENS[wl[1]]) { cards = WORK_LENS[wl[1]].slice(); if (wl[1] === 'today' && lens === 'Close') cards = [WK_KEV, ...cards].slice(0, 5); }
|
|
253
243
|
if (scope === 'brief') cards = LENS_ORDER[lens].map((k) => ALL[k]);
|
|
254
244
|
else if (lens !== 'Plan' && !wl) cards = cards.slice().reverse();
|
|
255
|
-
return { ok: true, lens, zoom: q.get('zoom') || 'week', cards: cards.map((c) => ({ ...c })), ticker: [], banner: null, unread: 2, kept_out: scope === 'brief' ? '3 FYI · raised 1 from a person with a deadline' : undefined, review: scope === 'brief' ? REVIEW : undefined, handled: scope === 'brief' ? HANDLED : undefined, trust: scope === 'brief' ? TRUST : undefined, foot: scope === 'brief' ? { left: 'used as is 68% · P1 missed 0', right: 'drag a row → done, ← later · tap to open · j k move' } : undefined, needs: scope === 'brief' ? NEEDS : undefined };
|
|
245
|
+
return { ok: true, scope, lens, zoom: q.get('zoom') || 'week', cards: cards.map((c) => ({ ...c })), ticker: [], banner: null, unread: 2, kept_out: scope === 'brief' ? '3 FYI · raised 1 from a person with a deadline' : undefined, review: scope === 'brief' ? REVIEW : undefined, handled: scope === 'brief' ? HANDLED : undefined, trust: scope === 'brief' ? TRUST : undefined, foot: scope === 'brief' ? { left: 'used as is 68% · P1 missed 0', right: 'drag a row → done, ← later · tap to open · j k move' } : undefined, needs: scope === 'brief' ? NEEDS : undefined };
|
|
256
246
|
}
|
|
257
247
|
|
|
258
248
|
/* ── the calendar ───────────────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* GET /api/report-dashboards/data the Calendar's demo strategy answer (c3-demo-cal3.js STRATEGY) narrowed to the scope
|
|
5
5
|
* POST /api/report-dashboards/layout checks the key and the report ids as the real route does (400 on an unknown one), and
|
|
6
6
|
* keeps the layout in this tab's sessionStorage, so a reload shows it. Nothing reaches the server.
|
|
7
|
-
* POST /api/
|
|
7
|
+
* POST /api/report-dashboards/schedule checks the key, plan and audience as the real route does, keeps the schedule in memory (one is seeded)
|
|
8
|
+
* POST /api/outbox/draft answers with an entry in the real route's shape and keeps it in memory for this tab
|
|
9
|
+
* GET /api/outbox the demo queue plus those drafts (two are seeded), for Reports > Drafts
|
|
10
|
+
* POST /api/outbox/discard hides a draft (undo: true brings it back), in memory
|
|
8
11
|
*
|
|
9
12
|
* Wiring: c3-demo.js imports installRep3 and calls it in installDemo() AFTER its own fetch wrapper is in place, the way it installs
|
|
10
13
|
* c3-demo-rep2.js. This wrapper must sit OVER c3-demo.js's: that wrapper answers every non-GET with { ok: true }, so a wrapper under it
|
|
@@ -66,10 +69,46 @@ function layout(b) {
|
|
|
66
69
|
L[b.key] = [...new Set(b.reports)]; store.write(L);
|
|
67
70
|
return json({ ok: true, key: b.key, reports: L[b.key] });
|
|
68
71
|
}
|
|
72
|
+
/* The demo's schedules (lib/report-schedules.js shape): the portfolio sends Leadership a monthly draft, as the mockup seeds it. In memory. */
|
|
73
|
+
const PLAN_TXT = { weekly: 'Weekly, Mondays 08:00', monthly: 'Monthly, 1st at 08:00' };
|
|
74
|
+
const nextOf = (plan) => { const d = new Date(); d.setHours(8, 0, 0, 0); if (plan === 'weekly') { do d.setDate(d.getDate() + 1); while (d.getDay() !== 1); } else { d.setDate(1); d.setMonth(d.getMonth() + 1); } return d.toISOString(); };
|
|
75
|
+
const SCHEDS = { portfolio: { key: 'portfolio', ws: null, plan: 'monthly', audience: 'Leadership', last_status: 'ok', last_error: null, last_draft: 'out-demo-rep3-s1' } };
|
|
76
|
+
const schedView = () => Object.fromEntries(Object.entries(SCHEDS).map(([k, v]) => [k, { ...v, label: PLAN_TXT[v.plan], next_run: nextOf(v.plan) }]));
|
|
77
|
+
function schedule(b) {
|
|
78
|
+
const m = KEY_RE.exec(String(b.key || ''));
|
|
79
|
+
if (!m || (m[2] && !BOARDS.some((x) => x.id === m[2])) || (m[3] && !INITIATIVES.some((x) => x.id === m[3]))) return json({ ok: false, error: 'key must be portfolio, board:<id> or initiative:<id>' }, 400);
|
|
80
|
+
if (b.plan === 'none') delete SCHEDS[b.key];
|
|
81
|
+
else if (!PLAN_TXT[b.plan]) return json({ ok: false, error: 'plan must be none, weekly or monthly' }, 400);
|
|
82
|
+
else if (!['Leadership', 'Team'].includes(b.audience)) return json({ ok: false, error: 'audience must be Leadership or Team' }, 400);
|
|
83
|
+
else SCHEDS[b.key] = { key: b.key, ws: null, plan: b.plan, audience: b.audience, last_status: null, last_error: null, last_draft: null };
|
|
84
|
+
const v = schedView();
|
|
85
|
+
return json({ ok: true, key: b.key, schedule: v[b.key] || null, schedules: v });
|
|
86
|
+
}
|
|
69
87
|
let seq = 0;
|
|
88
|
+
/* The dashboard drafts of the demo (Reports > Drafts): two seeded, then the ones the Share sheet makes in this tab. Kept in memory; a reload starts again. */
|
|
89
|
+
const mkEntry = (id, b, at, by) => ({ ...(by ? { made_by: by, schedule_key: 'portfolio' } : {}), id, channel: b.channel, recipient: b.recipient || null, subject: b.subject || '', body: b.body, origin: 'pm', state: 'queued', hash: 'demo-' + id, claims: { ok: true, refused: [] }, last_result: null, created_at: at });
|
|
90
|
+
const DRAFTS = [
|
|
91
|
+
mkEntry('out-demo-rep3-s1', { channel: 'customer-note', recipient: 'Leadership', subject: 'Portfolio for Leadership', body: 'Portfolio for Leadership, 2026-10-08\nHeadline: Conversion is off course. 1 more thing needs your call.\n\nSections\nQuarter outcomes\n- 2 of 4 outcomes are on course. [reported]\n\nNeeds your call\n- Conversion is off course. [reported]\n\nSources: KR metric records. Read on 2026-10-08.' }, '2026-10-01T08:00:00.000Z', 'schedule'),
|
|
92
|
+
mkEntry('out-demo-rep3-s2', { channel: 'customer-note', recipient: 'Team', subject: 'Board: Shop for Team', body: 'Board: Shop for Team, 2026-10-10\nHeadline: Checkout v2 is delayed.\n\nSections\nDelivery against promise\n- 1 of 4 commitments is delayed. [reported]\n\nNeeds your call\n- Checkout v2 is delayed. [reported]\n\nSources: Initiative plans and promised dates. Read on 2026-10-10.' }, '2026-10-10T16:20:00.000Z'),
|
|
93
|
+
];
|
|
94
|
+
const gone = new Set();
|
|
70
95
|
function draft(b) {
|
|
71
96
|
if (!b.body || !String(b.body).trim()) return json({ ok: false, error: 'body required' }, 400);
|
|
72
|
-
|
|
97
|
+
const e = mkEntry('out-demo-rep3-' + (++seq), b, new Date().toISOString());
|
|
98
|
+
DRAFTS.push(e);
|
|
99
|
+
return json({ ok: true, entry: e });
|
|
100
|
+
}
|
|
101
|
+
function discard(b) {
|
|
102
|
+
const id = String(b.id || '');
|
|
103
|
+
if (b.undo === true) { if (!gone.has(id)) return json({ ok: false, error: 'no discarded outbox entry with that id' }, 404); gone.delete(id); return json({ ok: true, id, restored: true }); }
|
|
104
|
+
if (!DRAFTS.some((e) => e.id === id) || gone.has(id)) return json({ ok: false, error: 'no such outbox entry' }, 404);
|
|
105
|
+
gone.add(id); return json({ ok: true, id, discarded: true });
|
|
106
|
+
}
|
|
107
|
+
/* The demo queue (Communications) plus the dashboard drafts, newest first. */
|
|
108
|
+
async function outboxWith(prevFetch, input, init) {
|
|
109
|
+
const r = await prevFetch(input, init); const j = await r.clone().json().catch(() => null);
|
|
110
|
+
if (!j || !j.ok) return r;
|
|
111
|
+
return json({ ...j, entries: [...(j.entries || []), ...DRAFTS.filter((e) => !gone.has(e.id)).slice().reverse()] });
|
|
73
112
|
}
|
|
74
113
|
|
|
75
114
|
export function installRep3() {
|
|
@@ -80,15 +119,17 @@ export function installRep3() {
|
|
|
80
119
|
const method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
81
120
|
const u = new URL(raw, location.origin);
|
|
82
121
|
if (u.origin === location.origin) {
|
|
83
|
-
if (method === 'GET' && u.pathname === '/api/report-dashboards') return Promise.resolve(json({ ok: true, library: LIBRARY, layouts: store.read(), defaults: DEFAULTS, boards: BOARDS, initiatives: INITIATIVES }));
|
|
122
|
+
if (method === 'GET' && u.pathname === '/api/report-dashboards') return Promise.resolve(json({ ok: true, library: LIBRARY, layouts: store.read(), defaults: DEFAULTS, boards: BOARDS, initiatives: INITIATIVES, schedules: schedView() }));
|
|
84
123
|
if (method === 'GET' && u.pathname === '/api/report-dashboards/data') {
|
|
85
124
|
const scope = u.searchParams.get('scope') || 'portfolio'; const id = u.searchParams.get('id') || '';
|
|
86
125
|
if (!['portfolio', 'board', 'initiative'].includes(scope)) return Promise.resolve(json({ ok: false, error: 'unknown scope: ' + scope }, 400));
|
|
87
126
|
return Promise.resolve(json(scoped(scope, id)));
|
|
88
127
|
}
|
|
89
|
-
if (method === '
|
|
128
|
+
if (method === 'GET' && u.pathname === '/api/outbox') return outboxWith(prev, input, init);
|
|
129
|
+
if (method === 'POST' && u.pathname === '/api/report-dashboards/schedule') { let b = {}; try { b = init && init.body ? JSON.parse(init.body) : {}; } catch { b = {}; } return Promise.resolve(schedule(b)); }
|
|
130
|
+
if (method === 'POST' && (u.pathname === '/api/report-dashboards/layout' || u.pathname === '/api/outbox/draft' || u.pathname === '/api/outbox/discard')) {
|
|
90
131
|
let b = {}; try { b = init && init.body ? JSON.parse(init.body) : {}; } catch { b = {}; }
|
|
91
|
-
return Promise.resolve(u.pathname === '/api/outbox/draft' ? draft(b) : layout(b));
|
|
132
|
+
return Promise.resolve(u.pathname === '/api/outbox/draft' ? draft(b) : u.pathname === '/api/outbox/discard' ? discard(b) : layout(b));
|
|
92
133
|
}
|
|
93
134
|
}
|
|
94
135
|
} catch { /* an unreadable request goes on as it is */ }
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { el } from './dom.js';
|
|
12
12
|
import * as D from './c3-demo-data.js';
|
|
13
13
|
import { installInit } from './c3-demo-init.js';
|
|
14
|
+
import { installBoards } from './c3-demo-boards.js'; // Initiatives boards: answers every /api/boards GET and POST (installed outermost, in installDemo)
|
|
14
15
|
import './c3-demo-cal-ix.js'; // Calendar and the ribbon: sets window.__c3calix (calendar-views.js reads it in demo only)
|
|
15
16
|
import './c3-demo-cal3.js'; // Calendar Quarter and Year: answers GET /api/c3-strategy (chained under this file's fetch wrapper)
|
|
16
17
|
import './c3-demo-rep-ix.js'; // Reports fixtures: sets window.__c3repix (report-families-view.js reads it in demo only)
|
|
@@ -118,4 +119,4 @@ export function demoPill() {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
export const DEMO_WS = D.WS_ID;
|
|
121
|
-
export function installDemo() { freezeClock(); install(); installRep3(); installInit(); import('./c3-demo-rep2.js').then((m) => m.installRep2()); window.__c3demo = true; window.__c3demoReady = import('./c3-demo-brief-ix.js').then((m) => m.installBriefIx()).then(() => import('./c3-demo-misc2.js')).then((m) => m.installMisc2()).then(() => import('./c3-demo-brief-page.js')).then((m) => m.installBriefPage()); } // Briefing case files, people and a simulated agent (c3-shell.js waits for __c3demoReady)
|
|
122
|
+
export function installDemo() { freezeClock(); install(); installRep3(); installInit(); installBoards(); import('./c3-demo-rep2.js').then((m) => m.installRep2()); window.__c3demo = true; window.__c3demoReady = import('./c3-demo-brief-ix.js').then((m) => m.installBriefIx()).then(() => import('./c3-demo-misc2.js')).then((m) => m.installMisc2()).then(() => import('./c3-demo-brief-page.js')).then((m) => m.installBriefPage()); } // Briefing case files, people and a simulated agent (c3-shell.js waits for __c3demoReady)
|
|
@@ -46,7 +46,7 @@ export function createDock(opts) {
|
|
|
46
46
|
/* the AGENT button: the full agent surfaces (the Terminal page), not a terminal. It carries the needs-you count; the header's button opens the same page. */
|
|
47
47
|
const agentBdg = el('span', { class: 'bdg', hidden: true, 'data-c3pg': 'dock-agent-count' }, '0');
|
|
48
48
|
const listBtn = el('button', { type: 'button', class: 'ib sm', title: TIPS.agentDock, 'aria-label': TIPS.agentDock, 'aria-haspopup': 'dialog', 'data-c3pg': 'dock-agent', style: 'position:relative',
|
|
49
|
-
onclick: () => { import('./c3-terminal.js').then((m) => m.openTerminalPage()); } }, ic('
|
|
49
|
+
onclick: () => { import('./c3-terminal.js').then((m) => m.openTerminalPage()); } }, ic('bolt'), agentBdg);
|
|
50
50
|
const side = el('aside', { class: 'c3dock dock', 'aria-label': 'Sessions', 'data-c3pg': 'dock' },
|
|
51
51
|
el('div', { class: 'dk-h' }, c3ic('repo', 'sm'), sel,
|
|
52
52
|
listBtn,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* pm/c3-ib-docs.js — the Docs tab of a board (mockup v5, 34-initiatives.js tabDocs / docList / docRow), and the doc helpers the
|
|
2
|
+
* initiative's own Docs tab (c3-ii-tabs.js) shares.
|
|
3
|
+
* registerBoardTab('docs', (host, ctx) => ...)
|
|
4
|
+
* DOCS The documents of the board's initiatives from the Documents data: GET /api/docs/library (files { path, title, category, date,
|
|
5
|
+
* initiative }, categories). A file belongs to the board when its `initiative` is one of the board's initiatives (from
|
|
6
|
+
* loadItems, c3-ini-work.js). Graph nodes (files with `node`) are left out. A search box filters by title, path, type and
|
|
7
|
+
* initiative. A row opens in place (its path, type and date) and "Open the document" opens the document sheet (c3-doc-sheet.js);
|
|
8
|
+
* when the sheet cannot load, the toast names the path.
|
|
9
|
+
* SHARED loadDocs(ctx, idSet) · renderDocs(host, ctx, docs, order, titles, grouped)
|
|
10
|
+
* Nothing is invented: a board with no linked documents says so. */
|
|
11
|
+
import { el } from './dom.js';
|
|
12
|
+
import { registerBoardTab } from './c3-initiatives.js';
|
|
13
|
+
import { icon, pbtn, lk, chip, tag, st, note, sect, list, empty, nrow, plural, day, ago, parseDay } from './c3-ini-ui.js';
|
|
14
|
+
import { loadItems } from './c3-ini-work.js';
|
|
15
|
+
|
|
16
|
+
const STALE_DAYS = 30;
|
|
17
|
+
/** The documents of the library that belong to the ids: { ok, error, docs: [{ path, name, cat, ty, date, ini, st }] }. */
|
|
18
|
+
export async function loadDocs(ctx, idSet) {
|
|
19
|
+
const lib = await ctx.api.get('/api/docs/library', { board: ctx.board.id });
|
|
20
|
+
if (!lib || !lib.ok) return { ok: false, error: (lib && lib.error) || 'no answer', docs: [] };
|
|
21
|
+
const catName = Object.fromEntries((lib.categories || []).map((c) => [c.key, c.name]));
|
|
22
|
+
const docs = (lib.files || []).filter((f) => !f.node && f.initiative && idSet.has(f.initiative)).map((f) => {
|
|
23
|
+
const age = parseDay(f.date) ? -(Math.round((parseDay(f.date) - parseDay(ctx.today)) / 864e5)) : null;
|
|
24
|
+
return { path: f.path, name: f.title || f.path, cat: f.category || '', ty: catName[f.category] || f.category || 'Document', date: f.date || '', ini: f.initiative, st: age != null && age > STALE_DAYS ? 'stale' : 'current' };
|
|
25
|
+
});
|
|
26
|
+
docs.sort((a, b) => String(b.date).localeCompare(String(a.date)));
|
|
27
|
+
return { ok: true, error: '', docs };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function openDoc(ctx, d, titles) {
|
|
31
|
+
try {
|
|
32
|
+
const { openDocSheet } = await import('./c3-doc-sheet.js');
|
|
33
|
+
openDocSheet({ ws: ctx.ws, doc: { path: d.path, name: d.name, ty: d.ty, v: '', st: d.st, ini: d.ini, iniName: titles.get(d.ini) || d.ini } });
|
|
34
|
+
} catch { ctx.toast('The document sheet could not open. The file is ' + d.path + '.'); }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The search box and the list of docs. `grouped` false draws one flat list (an initiative's own page). */
|
|
38
|
+
export function renderDocs(host, ctx, docs, order, titles, grouped) {
|
|
39
|
+
const results = el('div', { 'data-inb': 'docs-list' });
|
|
40
|
+
const input = el('input', { type: 'text', placeholder: 'Search documents', 'aria-label': 'Search documents', 'data-inb': 'docs-search', autocomplete: 'off' });
|
|
41
|
+
const count = el('span', { class: 'dim mono ini-meta', 'data-inb': 'docs-count', 'aria-live': 'polite' });
|
|
42
|
+
const row = (d) => nrow({ ic: 'doc', title: d.name, titleAttrs: { 'data-inb-doc': d.path },
|
|
43
|
+
chips: [/spec|prd/i.test(d.cat + ' ' + d.ty) ? tag(d.ty) : chip(d.ty), d.st === 'stale' ? st('warn', 'clock', 'Stale') : null, d.date ? chip(icon('clock', 'sm'), 'updated ' + ago(d.date, ctx.today)) : null,
|
|
44
|
+
grouped && d.ini ? lk({ onclick: () => ctx.openInitiative(d.ini), 'data-inb-docini': d.ini }, d.ini) : null],
|
|
45
|
+
xp: [note(d.path + (d.date ? ' · ' + day(d.date) : '')), el('div', { class: 'chips2' }, pbtn('outline', 'Open the document', 'next', { 'data-inb-docopen': d.path, onclick: () => openDoc(ctx, d, titles) }))],
|
|
46
|
+
attrs: { 'data-inb-docrow': d.path } });
|
|
47
|
+
const draw = () => {
|
|
48
|
+
const q = input.value.trim().toLowerCase();
|
|
49
|
+
const shown = docs.filter((d) => !q || (d.name + ' ' + d.path + ' ' + d.ty + ' ' + d.ini + ' ' + (titles.get(d.ini) || '')).toLowerCase().includes(q));
|
|
50
|
+
count.textContent = q ? plural(shown.length, 'document') + ' match' + (shown.length === 1 ? 'es' : '') : plural(docs.length, 'document');
|
|
51
|
+
if (!shown.length) { results.replaceChildren(empty(q ? 'No document matches "' + input.value.trim() + '".' : 'No documents.', { 'data-inb': 'docs-none' })); return; }
|
|
52
|
+
if (!grouped) { results.replaceChildren(sect({ ic: 'doc', title: 'Documents', count: shown.length }, list(shown.map(row)))); return; }
|
|
53
|
+
results.replaceChildren(...order.filter((id) => shown.some((d) => d.ini === id)).map((id) => {
|
|
54
|
+
const g = shown.filter((d) => d.ini === id);
|
|
55
|
+
return sect({ ic: 'init', title: id + ' ' + (titles.get(id) || ''), count: g.length, attrs: { 'data-inb-docgrp': id }, right: lk({ onclick: () => ctx.openInitiative(id, 'docs'), 'aria-label': 'Open ' + id }, 'Open initiative') }, list(g.map(row)));
|
|
56
|
+
}));
|
|
57
|
+
};
|
|
58
|
+
input.addEventListener('input', draw);
|
|
59
|
+
host.replaceChildren(el('div', { class: 'calbar' }, el('div', { class: 'form ini-grow' }, input), count), results);
|
|
60
|
+
draw();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
registerBoardTab('docs', async (host, ctx) => {
|
|
64
|
+
let dead = false;
|
|
65
|
+
host.replaceChildren(note('Reading the documents'));
|
|
66
|
+
const items = await loadItems(ctx);
|
|
67
|
+
if (dead) return () => {};
|
|
68
|
+
if (!items.ok) { host.replaceChildren(el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), 'The initiatives of this board could not be read: ' + items.error + '.')); return () => {}; }
|
|
69
|
+
const titles = new Map(items.initiatives.map((r) => [r.id, r.title]));
|
|
70
|
+
const lib = await loadDocs(ctx, new Set(titles.keys()));
|
|
71
|
+
if (dead) return () => {};
|
|
72
|
+
if (!lib.ok) { host.replaceChildren(el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), 'The documents could not be read: ' + lib.error + '.')); return () => {}; }
|
|
73
|
+
if (!lib.docs.length) {
|
|
74
|
+
host.replaceChildren(empty(items.initiatives.length ? 'No documents are linked to the initiatives of ' + ctx.board.name + ' yet.' : ctx.board.name + ' has no initiatives, so there are no documents.', { 'data-inb': 'docs-empty' }));
|
|
75
|
+
return () => { dead = true; };
|
|
76
|
+
}
|
|
77
|
+
const order = [...new Set(lib.docs.map((d) => d.ini))];
|
|
78
|
+
renderDocs(host, ctx, lib.docs, order, titles, true);
|
|
79
|
+
return () => { dead = true; };
|
|
80
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/* pm/c3-ib-initiatives.js — the Initiatives tab of a board (mockup v5, 34-initiatives.js tabInitiatives / iniToolbar / iniRow).
|
|
2
|
+
* registerBoardTab('initiatives', (host, ctx) => ...) ctx is documented in the header of c3-initiatives.js.
|
|
3
|
+
* What it draws, all of it paged on the server (GET /api/boards/backlog; q, view, cat, stage, status, owner, horizon, sort, offset, limit):
|
|
4
|
+
* - a search box (debounced), the saved views as pills (Now and next, Mine, At risk, No owner, All) and New initiative;
|
|
5
|
+
* - the selects Category, Stage, Status, Owner and Horizon, the Sort select and a direction toggle (sort=-key is descending). "Not done"
|
|
6
|
+
* and "At risk or late" are comma lists (status=on track,at risk,late), which the server reads as any of;
|
|
7
|
+
* - one console3 row per initiative: id, its workflow template, the stage dots of its own workflow, status, owner, progress, dates,
|
|
8
|
+
* warnings; Move (a menu of its own stages, ctx.moveTo) and Open;
|
|
9
|
+
* - a numbered pager of 25 ("26–50 of 312", Previous, 1 2 … 13, Next).
|
|
10
|
+
* ctx.filter (from the URL or a Summary tile) opens the tab on that view and those filters. One request is in flight at a time; a change
|
|
11
|
+
* made meanwhile is sent when it returns and the older answer is dropped. host.dataset.ibbState is loading | ready | error and
|
|
12
|
+
* host.dataset.ibbSeq counts the answers drawn (the browser suite polls both). */
|
|
13
|
+
import { el } from './dom.js';
|
|
14
|
+
import { registerBoardTab } from './c3-initiatives.js';
|
|
15
|
+
import { icon, pill, lk, ib, pbtn, chip, st, note, lbl, sect, list, empty, nrow, statusChip, ownerChip, stepsMini, progChip, dateChip, daysBetween, plural, openMenu, sw, CATS } from './c3-ini-ui.js';
|
|
16
|
+
import { wfOf, catOf, tplName } from './c3-ini-work.js';
|
|
17
|
+
|
|
18
|
+
const PAGE = 25;
|
|
19
|
+
const VIEWS = [['now', 'Now and next'], ['mine', 'Mine'], ['risk', 'At risk'], ['noowner', 'No owner'], ['all', 'All']];
|
|
20
|
+
const STATUS = [['', 'Any'], ['on track,at risk,late', 'Not done'], ['on track', 'On track'], ['at risk,late', 'At risk or late'], ['at risk', 'At risk'], ['late', 'Late'], ['done', 'Done']];
|
|
21
|
+
const HORIZON = [['', 'Any'], ['now,next', 'Now and next'], ['now', 'Now'], ['next', 'Next'], ['later', 'Later']];
|
|
22
|
+
const SORTS = [['horizon', 'Horizon'], ['end', 'Ends'], ['title', 'Title'], ['stage', 'Stage'], ['progress', 'Progress'], ['status', 'Status'], ['owner', 'Owner'], ['id', 'Key']];
|
|
23
|
+
const FKEYS = ['cat', 'stage', 'status', 'owner', 'horizon'];
|
|
24
|
+
const toneOf = (s) => (s === 'late' ? 'bad' : s === 'at risk' ? 'warn' : 'ok');
|
|
25
|
+
const warnChip = (w) => st('warn', 'alert', w, { title: 'An order rule, shown as a warning. It never blocks.', 'data-ini-warning': w });
|
|
26
|
+
|
|
27
|
+
registerBoardTab('initiatives', (host, ctx) => {
|
|
28
|
+
const b = ctx.board;
|
|
29
|
+
const f0 = ctx.filter || {};
|
|
30
|
+
const sort0 = String(f0.sort || 'horizon');
|
|
31
|
+
const Q = { q: String(f0.q || ''), view: VIEWS.some((v) => v[0] === f0.view) ? f0.view : 'now', cat: CATS.includes(f0.cat) ? f0.cat : '', stage: f0.stage || '', status: f0.status || '', owner: f0.owner || '', horizon: f0.horizon || '',
|
|
32
|
+
sort: SORTS.some((s) => s[0] === sort0.replace(/^-/, '')) ? sort0.replace(/^-/, '') : 'horizon', dir: sort0.startsWith('-') ? -1 : 1, offset: Math.max(0, Number(f0.offset) || 0) };
|
|
33
|
+
const S = { data: null, flying: false, again: false, dead: false, seq: 0, err: '' };
|
|
34
|
+
|
|
35
|
+
const search = el('input', { type: 'text', 'data-ibb': 'q', 'aria-label': 'Search initiatives', placeholder: 'Search ' + b.name + ' initiatives', value: Q.q, autocomplete: 'off' });
|
|
36
|
+
let deb = null;
|
|
37
|
+
search.addEventListener('input', () => { clearTimeout(deb); deb = setTimeout(() => { Q.q = search.value.trim(); Q.offset = 0; request(); }, 250); });
|
|
38
|
+
search.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); clearTimeout(deb); Q.q = search.value.trim(); Q.offset = 0; request(); } });
|
|
39
|
+
const views = el('div', { class: 'chips2', role: 'group', 'aria-label': 'Saved views', 'data-ibb': 'views' });
|
|
40
|
+
const top = el('div', { class: 'calbar' }, el('div', { class: 'form ini-grow' }, search), views,
|
|
41
|
+
pbtn('primary', 'New initiative', 'plus', { 'data-ini': 'new-initiative', 'aria-haspopup': 'dialog', onclick: () => ctx.newInitiative() }));
|
|
42
|
+
const filters = el('div', { class: 'chips2', 'data-ibb': 'filters' });
|
|
43
|
+
const count = el('div', { class: 'dim ini-note', 'data-ibb': 'count', 'aria-live': 'polite' });
|
|
44
|
+
const err = el('div', { class: 'fold w', role: 'alert', 'data-ibb': 'error', hidden: '' });
|
|
45
|
+
const body = el('div', { 'data-ibb': 'body' });
|
|
46
|
+
const pager = el('div', { class: 'chips2 ini-pager', 'data-ibb': 'pager' });
|
|
47
|
+
host.replaceChildren(el('div', { class: 'calbody', 'data-ibb': 'initiatives' }, top, filters, count, err, body, pager));
|
|
48
|
+
host.dataset.ibbState = 'loading'; host.dataset.ibbSeq = '0';
|
|
49
|
+
|
|
50
|
+
const sel = (k, label, opts) => el('label', { class: 'chips2' }, lbl(label),
|
|
51
|
+
el('select', { 'data-ibb-filter': k, 'aria-label': label, onchange: (e) => { Q[k] = e.target.value; Q.offset = 0; request(); } }, ...opts.map(([v, l]) => el('option', { value: v, selected: v === Q[k] }, l))));
|
|
52
|
+
function drawControls() {
|
|
53
|
+
views.replaceChildren(...VIEWS.map(([k, l]) => pill({ 'data-ibb-view': k, 'aria-pressed': String(Q.view === k), onclick: () => { Q.view = k; Q.offset = 0; request(); } }, l)));
|
|
54
|
+
const fa = (S.data && S.data.facets) || {};
|
|
55
|
+
const keep = (k, keys) => (Q[k] && !keys.includes(Q[k]) ? [Q[k], ...keys] : keys);
|
|
56
|
+
const order = (b.workflow || []).map((s) => s.name);
|
|
57
|
+
const stages = keep('stage', Object.keys(fa.stage || {}).sort((x, y) => ((order.indexOf(x) + 1 || 99) - (order.indexOf(y) + 1 || 99)) || x.localeCompare(y)));
|
|
58
|
+
const owners = keep('owner', Object.keys(fa.owner || {}).filter((o) => o !== 'none').sort((x, y) => (x === 'You' ? -1 : y === 'You' ? 1 : x.localeCompare(y))));
|
|
59
|
+
const n = (k, v) => (fa[k] && fa[k][v] != null ? ' (' + fa[k][v] + ')' : '');
|
|
60
|
+
filters.replaceChildren(
|
|
61
|
+
sel('cat', 'Category', [['', 'Any'], ...CATS.map((c) => [c, c + n('cat', c)])]),
|
|
62
|
+
sel('stage', 'Stage', [['', 'Any'], ...stages.map((s) => [s, s + n('stage', s)])]),
|
|
63
|
+
sel('status', 'Status', STATUS),
|
|
64
|
+
sel('owner', 'Owner', [['', 'Anyone'], ['none', 'No owner' + n('owner', 'none')], ...owners.map((o) => [o, o + n('owner', o)])]),
|
|
65
|
+
sel('horizon', 'Horizon', HORIZON),
|
|
66
|
+
el('label', { class: 'chips2' }, lbl('Sort'), el('select', { 'data-ibb': 'sort', 'aria-label': 'Sort by', onchange: (e) => { Q.sort = e.target.value; Q.offset = 0; request(); } }, ...SORTS.map(([v, l]) => el('option', { value: v, selected: v === Q.sort }, l)))),
|
|
67
|
+
ib(Q.dir > 0 ? 'Ascending, press to flip' : 'Descending, press to flip', el('span', { class: Q.dir > 0 ? 'ini-up' : 'ini-down' }, icon('next', 'sm')), { 'data-ibb': 'dir', onclick: () => { Q.dir = -Q.dir; Q.offset = 0; request(); } }));
|
|
68
|
+
}
|
|
69
|
+
const active = () => FKEYS.some((k) => Q[k]) || Q.q;
|
|
70
|
+
function clearFilters() { for (const k of FKEYS) Q[k] = ''; Q.q = ''; search.value = ''; Q.offset = 0; request(); }
|
|
71
|
+
|
|
72
|
+
function moveMenu(anchor, r) {
|
|
73
|
+
const wf = wfOf(r, b);
|
|
74
|
+
openMenu(anchor, { label: 'Move ' + r.id + ' to a stage', items: [{ group: 'Move ' + r.id + ' to' }, ...wf.map((s) => ({ kids: [sw(s.cat), s.name], current: s.name === r.stage, disabled: s.name === r.stage, attrs: { 'data-ini-moveto': s.name },
|
|
75
|
+
onPick: async () => { const m = await ctx.moveTo(r.id, s.name); if (m && m.ok) request(); } }))] });
|
|
76
|
+
}
|
|
77
|
+
function rowOf(r) {
|
|
78
|
+
const wf = wfOf(r, b);
|
|
79
|
+
const late = r.status !== 'done' && r.end && daysBetween(ctx.today, r.end) < 0;
|
|
80
|
+
return nrow({ ic: r.status === 'late' || r.status === 'at risk' ? 'alert' : r.status === 'done' ? 'check' : 'init', hot: r.status === 'late' || r.status === 'at risk', title: r.title || r.id,
|
|
81
|
+
onOpen: () => ctx.openInitiative(r.id), openLabel: 'Open ' + r.id + ' ' + (r.title || ''), titleAttrs: { 'data-ibb-open': r.id },
|
|
82
|
+
chips: [el('span', { class: 'chip2 mono' }, r.id), el('span', { class: 'tag', title: 'Workflow' }, tplName(r, b)), stepsMini(wf, r.stage, tplName(r, b)), statusChip(r.status), ownerChip(r.owner),
|
|
83
|
+
progChip(r.progress || 0, toneOf(r.status)), dateChip(r.start, r.end, late), r.anchor ? null : st('warn', 'alert', 'No anchor'), ...(r.warnings || []).map(warnChip)],
|
|
84
|
+
right: [ib('Move ' + r.id + ' to another stage', 'more', { 'data-ibb-move': r.id, 'aria-haspopup': 'menu', 'aria-expanded': 'false', disabled: !!b.self, onclick: (e) => moveMenu(e.currentTarget, r) }),
|
|
85
|
+
ib('Open ' + r.id, 'next', { 'data-ini-open': r.id, onclick: () => ctx.openInitiative(r.id) })],
|
|
86
|
+
attrs: { 'data-ibb-row': r.id, 'data-cat': r.cat || catOf(wf, r.stage) } });
|
|
87
|
+
}
|
|
88
|
+
function drawPager(d) {
|
|
89
|
+
const pages = Math.max(1, Math.ceil(d.total / PAGE)); const cur = Math.floor(d.offset / PAGE);
|
|
90
|
+
if (pages < 2) { pager.replaceChildren(); return; }
|
|
91
|
+
const nums = []; for (let p = 0; p < pages; p++) if (p < 2 || p >= pages - 1 || Math.abs(p - cur) <= 1) nums.push(p); else if (nums[nums.length - 1] !== '…') nums.push('…');
|
|
92
|
+
const to = (p) => { Q.offset = p * PAGE; request(true); };
|
|
93
|
+
pager.replaceChildren(el('span', { class: 'dim mono ini-meta' }, (d.offset + 1) + '–' + Math.min(d.total, d.offset + d.rows.length) + ' of ' + d.total),
|
|
94
|
+
pill({ 'data-ibb': 'prev', disabled: cur === 0, onclick: () => to(cur - 1) }, 'Previous'),
|
|
95
|
+
...nums.map((p) => (p === '…' ? el('span', { class: 'dim' }, '…') : pill({ 'data-ibb-page': String(p + 1), 'aria-pressed': String(p === cur), 'aria-label': 'Page ' + (p + 1), onclick: () => to(p) }, String(p + 1)))),
|
|
96
|
+
pill({ 'data-ibb': 'next', disabled: cur >= pages - 1, onclick: () => to(cur + 1) }, 'Next'));
|
|
97
|
+
}
|
|
98
|
+
function draw() {
|
|
99
|
+
err.textContent = S.err; err.hidden = !S.err;
|
|
100
|
+
drawControls();
|
|
101
|
+
const d = S.data;
|
|
102
|
+
if (!d) return;
|
|
103
|
+
const label = (VIEWS.find((v) => v[0] === Q.view) || [])[1];
|
|
104
|
+
count.replaceChildren(plural(d.total, 'initiative') + ' · ' + label + (active() ? ' · ' : ''), active() ? lk({ 'data-ibb': 'clear', onclick: clearFilters }, 'Clear filters') : '', ' · each row shows its own workflow and stage');
|
|
105
|
+
body.replaceChildren(sect({ ic: 'init', title: 'Initiatives', count: d.total },
|
|
106
|
+
d.rows.length ? list(d.rows.map(rowOf), { 'data-ibb': 'rows' }) : empty(active() || Q.view !== 'all' ? 'No initiatives match. Clear the filters or pick All.' : b.name + ' has no initiatives yet. Press New initiative, or accept an ask in Triage.', { 'data-ibb': 'empty' })));
|
|
107
|
+
drawPager(d);
|
|
108
|
+
host.dataset.ibbSeq = String(++S.seq);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function request(keepFocus) {
|
|
112
|
+
if (S.dead) return;
|
|
113
|
+
if (S.flying) { S.again = true; return; }
|
|
114
|
+
S.flying = true; host.dataset.ibbState = 'loading'; host.setAttribute('aria-busy', 'true');
|
|
115
|
+
let r;
|
|
116
|
+
for (;;) {
|
|
117
|
+
S.again = false;
|
|
118
|
+
const q = { ...Q };
|
|
119
|
+
r = await ctx.api.get('/api/boards/backlog', { board: b.id, q: q.q, view: q.view, cat: q.cat, stage: q.stage, status: q.status, owner: q.owner, horizon: q.horizon, sort: (q.dir < 0 ? '-' : '') + q.sort, offset: q.offset, limit: PAGE });
|
|
120
|
+
if (S.dead) return;
|
|
121
|
+
if (!S.again) break;
|
|
122
|
+
}
|
|
123
|
+
S.flying = false; host.removeAttribute('aria-busy');
|
|
124
|
+
if (!r || !r.ok || r.pending) { S.err = r && r.pending ? 'The repository is still being read. Try again in a moment.' : 'The initiatives could not be read: ' + ((r && r.error) || 'no answer') + '.'; host.dataset.ibbState = 'error'; draw(); return; }
|
|
125
|
+
S.err = ''; S.data = { total: r.total || 0, offset: r.offset || 0, limit: r.limit || PAGE, rows: r.rows || [], facets: r.facets || {} };
|
|
126
|
+
Q.offset = S.data.offset;
|
|
127
|
+
draw(); host.dataset.ibbState = 'ready';
|
|
128
|
+
if (keepFocus) { const f = pager.querySelector('[aria-pressed="true"]'); if (f) f.focus(); }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
drawControls();
|
|
132
|
+
request();
|
|
133
|
+
return () => { S.dead = true; clearTimeout(deb); };
|
|
134
|
+
});
|