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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* routes-stage-tools.js — what each workflow stage offers, server side (initiative stage-tools-records; PRD R1 to R8).
|
|
3
|
+
*
|
|
4
|
+
* GET /api/stage-tools/records?ws&id → { ok, ws, id, records: { problem, rice, launch, kill, learnings } }
|
|
5
|
+
* each record { data, at, by } or null; rice data adds `score`
|
|
6
|
+
* POST /api/stage-tools/records { ws, id, tool, data } → { ok, ws, id, tool, record }
|
|
7
|
+
* GET /api/stage-tools/rice?ws[&ids=A,B] → { ok, ws, rows: [{ id, score, rank, reach, impact, confidence, effort, at }] }
|
|
8
|
+
* GET /api/stage-tools/held?ws&id[&board] → { ok, ws, id, anchor, jira, tickets, breakdown, releases, dates, measurements }
|
|
9
|
+
* GET /api/stage-tools/burndown?ws[&ids=A,B][&board][&sprint=<name>]
|
|
10
|
+
* → { ok, available: true, sprint, committed, done, left, daysLeft, needPerDay,
|
|
11
|
+
* series: [{ date, remaining, ideal }], tickets, unpointed, readAt }
|
|
12
|
+
* | { ok, available: false, reason }
|
|
13
|
+
*
|
|
14
|
+
* WHERE THE RECORDS LIVE. <the repository's state folder>/initiative-tools.jsonl (ctx.stateDirFor(w)), the folder the
|
|
15
|
+
* boards ledger uses: the PM's choice of 2026-10-11, "one append-only file in each board's own state folder". A workspace
|
|
16
|
+
* with no state folder (an imported one) keeps it in its import overlay, <runtime>/imports/<ws>/initiative-tools.jsonl.
|
|
17
|
+
*
|
|
18
|
+
* THE INITIATIVE ID is checked by shape only (letters, digits, . _ -): an imported initiative has no record file, and the
|
|
19
|
+
* boards ledger may hold ids the record tree does not. A read of an id nobody saved is empty records, never a 404.
|
|
20
|
+
*
|
|
21
|
+
* BUILD AND DELIVER. With `board`, the board's own `buildDeliver` line in boards.jsonl is read (missing: on). When it is
|
|
22
|
+
* off, `tickets` is { source: 'none' } and the burndown is { available: false, reason: 'Jira not enabled on this board' }.
|
|
23
|
+
*
|
|
24
|
+
* WHO WRITES. The POST passes pm-guard requirePagePress; `by` is the guard's actor. The factory never writes the file (D65).
|
|
25
|
+
* NOTHING HERE READS OR WRITES JIRA: tickets come from the cached snapshots of lib/jira-status.js. Nothing is sent outward.
|
|
26
|
+
*/
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const guard = require('./pm-guard.js');
|
|
30
|
+
const S = require('./stage-tools.js');
|
|
31
|
+
|
|
32
|
+
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
33
|
+
const BOARD_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
34
|
+
const NO_JIRA = 'Jira not enabled on this board';
|
|
35
|
+
const text = (v) => (typeof v === 'string' ? v : '');
|
|
36
|
+
const bad = (status, error) => ({ status, body: { ok: false, error } });
|
|
37
|
+
|
|
38
|
+
function wsOf(ctx, id) {
|
|
39
|
+
const w = ctx.getWs(id || '');
|
|
40
|
+
return w && (!id || w.id === id) ? w : null;
|
|
41
|
+
}
|
|
42
|
+
const stateRootOf = (ctx) => (ctx && (ctx.runtimeDir || ctx.stateRoot)) || require('./state-root.js').defaultRoot();
|
|
43
|
+
function ledgerFile(ctx, w) {
|
|
44
|
+
const d = ctx.stateDirFor ? ctx.stateDirFor(w) : null;
|
|
45
|
+
if (d) return path.join(d, S.LEDGER);
|
|
46
|
+
try { return path.join(require('./import-overlay.js').overlayDir(stateRootOf(ctx), w.id), S.LEDGER); } catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
function boardsFile(ctx, w) {
|
|
49
|
+
const d = ctx.stateDirFor ? ctx.stateDirFor(w) : null;
|
|
50
|
+
return d ? path.join(d, 'boards.jsonl') : null;
|
|
51
|
+
}
|
|
52
|
+
function idsOf(url) {
|
|
53
|
+
const raw = text(url.searchParams.get('ids'));
|
|
54
|
+
return raw ? raw.split(',').map((s) => s.trim()).filter((s) => ID_RE.test(s)).slice(0, 500) : null;
|
|
55
|
+
}
|
|
56
|
+
const err = (e) => String((e && e.message) || e || 'error').slice(0, 160);
|
|
57
|
+
|
|
58
|
+
// ── what PMOS already holds ───────────────────────────────────────────────────────────────────────
|
|
59
|
+
function snapshotTickets(root, id) {
|
|
60
|
+
try {
|
|
61
|
+
const snap = require('./jira-status.js').readSnapshot(root, id);
|
|
62
|
+
return snap ? { readAt: snap.fetchedAt, tickets: [...snap.tickets.values()].map((t) => ({ ...t, initiative: id })) } : null;
|
|
63
|
+
} catch { return null; }
|
|
64
|
+
}
|
|
65
|
+
function ticketsHeld(root, id, on) {
|
|
66
|
+
if (!on) return { source: 'none', reason: NO_JIRA, readAt: null, items: [], byCategory: { todo: 0, doing: 0, done: 0 } };
|
|
67
|
+
const snap = snapshotTickets(root, id);
|
|
68
|
+
if (!snap) return { source: 'none', reason: 'no Jira read for this initiative yet', readAt: null, items: [], byCategory: { todo: 0, doing: 0, done: 0 } };
|
|
69
|
+
const items = snap.tickets.map((t) => ({ key: t.key, summary: t.summary, status: t.status, category: t.category, sprint: t.sprint || null,
|
|
70
|
+
points: typeof t.points === 'number' ? t.points : null, resolved: t.resolved || null, flagged: !!t.flagged, reason: t.reason || null, url: t.url || null, stale: !!t.stale }));
|
|
71
|
+
const byCategory = { todo: 0, doing: 0, done: 0 };
|
|
72
|
+
for (const t of items) if (t.category in byCategory) byCategory[t.category]++;
|
|
73
|
+
return { source: 'jira', readAt: snap.readAt, items, byCategory };
|
|
74
|
+
}
|
|
75
|
+
function planItemsOf(root, id) {
|
|
76
|
+
try {
|
|
77
|
+
const P = require('./plan-items.js');
|
|
78
|
+
const fold = P.readFold(root);
|
|
79
|
+
const items = Object.values(fold.items).filter((it) => it.initiative === id)
|
|
80
|
+
.map((it) => ({ id: it.id, source: 'plan', title: it.title, stage: it.stage, state: it.state, owner: it.owner || null, start: it.start || null,
|
|
81
|
+
due: it.due || null, milestone: !!it.milestone, proposed: !!it.proposed, late: !!it.late }));
|
|
82
|
+
return { items, progress: P.progressOf(fold, id) };
|
|
83
|
+
} catch { return { items: [], progress: { done: 0, total: 0, proposed: 0 } }; }
|
|
84
|
+
}
|
|
85
|
+
function pmosTasksOf(ctx, id) {
|
|
86
|
+
try {
|
|
87
|
+
const root = require('./routes-roadmap-ops.js').rootOf(ctx);
|
|
88
|
+
const { repoScript } = require('./repo-script.js');
|
|
89
|
+
const B = require(repoScript('breakdown-ops.js'));
|
|
90
|
+
return B.list(root).filter((it) => it && it.initiative === id)
|
|
91
|
+
.map((it) => ({ id: it.rid, source: 'pmos', title: it.title || '', phase: it.phase || null, state: it.status || null, owner: it.owner || null,
|
|
92
|
+
points: typeof it.points === 'number' ? it.points : null, due: it.due || null, jira: it.jira_key || null }));
|
|
93
|
+
} catch { return []; }
|
|
94
|
+
}
|
|
95
|
+
function releasesOf(ctx, id) {
|
|
96
|
+
try {
|
|
97
|
+
const { repoScript } = require('./repo-script.js');
|
|
98
|
+
const R = require(repoScript('releases.js'));
|
|
99
|
+
const f = path.join(ctx.runtimeDir, 'releases', 'releases.jsonl');
|
|
100
|
+
let lines = [];
|
|
101
|
+
try { lines = fs.readFileSync(f, 'utf8').split('\n').filter(Boolean); } catch { return []; }
|
|
102
|
+
return R.foldReleases(lines).filter((r) => Array.isArray(r.initiative_ids) && r.initiative_ids.includes(id))
|
|
103
|
+
.map((r) => ({ id: r.id, type: r.type || null, fix_version: r.fix_version || null, deploy_at: r.deploy_at || null, release_at: r.release_at || null,
|
|
104
|
+
accepted: !!r.accepted_at, scope: r.initiative_ids.slice() }));
|
|
105
|
+
} catch { return []; }
|
|
106
|
+
}
|
|
107
|
+
function anchorOf(root, id) {
|
|
108
|
+
try {
|
|
109
|
+
const fm = require('./jira-tickets.js').recordFm(root, id);
|
|
110
|
+
const a = fm && (fm.parent_kr || fm.anchor);
|
|
111
|
+
return typeof a === 'string' && a.trim() ? a.trim().replace(/^["']|["']$/g, '') : null;
|
|
112
|
+
} catch { return null; }
|
|
113
|
+
}
|
|
114
|
+
function measurementsOf(root, anchor) {
|
|
115
|
+
if (!anchor) return { anchor: null, latest: null, series: [] };
|
|
116
|
+
try {
|
|
117
|
+
const M = require('./measurements.js');
|
|
118
|
+
const r = M.readMeasurements(root);
|
|
119
|
+
const series = ((r.byAnchor || {})[anchor] || []).map((m) => ({ value: m.value, target: m.target == null ? null : m.target, unit: m.unit || null, as_of: m.as_of, source: m.source }));
|
|
120
|
+
return { anchor, latest: M.latest(r, anchor), series };
|
|
121
|
+
} catch { return { anchor, latest: null, series: [] }; }
|
|
122
|
+
}
|
|
123
|
+
function datesOf(planItems) {
|
|
124
|
+
const live = planItems.filter((it) => !it.proposed);
|
|
125
|
+
const dated = live.filter((it) => it.start || it.due);
|
|
126
|
+
let start = null; let end = null;
|
|
127
|
+
for (const it of dated) {
|
|
128
|
+
const s = it.start || it.due; const e = it.due || it.start;
|
|
129
|
+
if (!start || s < start) start = s;
|
|
130
|
+
if (!end || e > end) end = e;
|
|
131
|
+
}
|
|
132
|
+
return { start, end, dated: dated.length, undated: live.length - dated.length };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function held(ctx, w, id, board) {
|
|
136
|
+
const on = S.buildDeliverOn(boardsFile(ctx, w), board);
|
|
137
|
+
const plan = planItemsOf(w.root, id);
|
|
138
|
+
const tasks = pmosTasksOf(ctx, id);
|
|
139
|
+
const anchor = anchorOf(w.root, id);
|
|
140
|
+
const items = [...plan.items, ...tasks];
|
|
141
|
+
const done = plan.items.filter((it) => !it.proposed && it.state === 'done').length + tasks.filter((t) => /^done$/i.test(String(t.state || ''))).length;
|
|
142
|
+
const total = plan.items.filter((it) => !it.proposed).length + tasks.length;
|
|
143
|
+
return {
|
|
144
|
+
ok: true, ws: w.id, id, anchor, jira: on,
|
|
145
|
+
tickets: ticketsHeld(w.root, id, on),
|
|
146
|
+
breakdown: { items, done, total, proposed: plan.progress.proposed || 0 },
|
|
147
|
+
releases: releasesOf(ctx, id),
|
|
148
|
+
dates: datesOf(plan.items),
|
|
149
|
+
measurements: measurementsOf(w.root, anchor),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function burndownFor(ctx, w, ids, board, sprint) {
|
|
154
|
+
if (!S.buildDeliverOn(boardsFile(ctx, w), board)) return { ok: true, available: false, reason: NO_JIRA };
|
|
155
|
+
let list = ids;
|
|
156
|
+
if (!list) {
|
|
157
|
+
try { list = require('./jira-status.js').linkedInitiatives(w.root).filter((q) => q.linked).map((q) => q.initiative); } catch { list = []; }
|
|
158
|
+
}
|
|
159
|
+
const tickets = []; let readAt = null;
|
|
160
|
+
for (const id of list) {
|
|
161
|
+
const snap = snapshotTickets(w.root, id);
|
|
162
|
+
if (!snap) continue;
|
|
163
|
+
tickets.push(...snap.tickets);
|
|
164
|
+
if (snap.readAt && (!readAt || snap.readAt < readAt)) readAt = snap.readAt; // the oldest read is the honest age
|
|
165
|
+
}
|
|
166
|
+
if (!tickets.length) return { ok: true, available: false, reason: 'no Jira read for these initiatives yet' };
|
|
167
|
+
return { ok: true, ...S.burndown(tickets, { sprint: sprint || null }), readAt };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── the routes ────────────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
async function handle(req, res, url, ctx) {
|
|
172
|
+
const p = url.pathname;
|
|
173
|
+
const send = (r) => { ctx.json(res, r.body, r.status); return true; };
|
|
174
|
+
if (!['/api/stage-tools/records', '/api/stage-tools/rice', '/api/stage-tools/held', '/api/stage-tools/burndown'].includes(p)) {
|
|
175
|
+
return send(bad(404, 'no such stage-tools route'));
|
|
176
|
+
}
|
|
177
|
+
if (p === '/api/stage-tools/records' && req.method === 'POST') {
|
|
178
|
+
const read = await ctx.readJson(req);
|
|
179
|
+
if (!read.ok) return send(bad(400, 'the body is not JSON'));
|
|
180
|
+
const body = read.body;
|
|
181
|
+
const g = guard.requirePagePress(req, body);
|
|
182
|
+
if (!g.ok) return send(bad(g.status, g.error));
|
|
183
|
+
const w = wsOf(ctx, text(body.ws));
|
|
184
|
+
if (!w) return send(bad(404, 'no such workspace'));
|
|
185
|
+
const id = text(body.id);
|
|
186
|
+
if (!ID_RE.test(id)) return send(bad(400, 'id must be an initiative id (letters, digits, . _ -)'));
|
|
187
|
+
const c = S.checkData(text(body.tool), body.data);
|
|
188
|
+
if (!c.ok) return send(bad(400, c.error));
|
|
189
|
+
const file = ledgerFile(ctx, w);
|
|
190
|
+
if (!file) return send(bad(409, 'this workspace has no folder to keep the records in'));
|
|
191
|
+
try { S.appendTool(file, { initiative: id, tool: body.tool, data: c.data }, g.actor); }
|
|
192
|
+
catch (e) { return send(bad(500, 'the record could not be saved: ' + err(e))); }
|
|
193
|
+
const rec = S.recordsOf(S.readLedger(file), id)[body.tool];
|
|
194
|
+
return send({ status: 200, body: { ok: true, ws: w.id, id, tool: body.tool, record: rec } });
|
|
195
|
+
}
|
|
196
|
+
if (req.method !== 'GET') return send(bad(405, p === '/api/stage-tools/records' ? 'GET or POST only' : 'GET only'));
|
|
197
|
+
const w = wsOf(ctx, url.searchParams.get('ws'));
|
|
198
|
+
if (!w) return send(bad(404, 'no such workspace'));
|
|
199
|
+
const board = text(url.searchParams.get('board'));
|
|
200
|
+
if (board && !BOARD_RE.test(board)) return send(bad(400, 'board must be a board id'));
|
|
201
|
+
if (p === '/api/stage-tools/rice') {
|
|
202
|
+
const file = ledgerFile(ctx, w);
|
|
203
|
+
return send({ status: 200, body: { ok: true, ws: w.id, rows: file ? S.riceRank(S.readLedger(file), idsOf(url)) : [] } });
|
|
204
|
+
}
|
|
205
|
+
if (p === '/api/stage-tools/burndown') {
|
|
206
|
+
const sprint = text(url.searchParams.get('sprint')).slice(0, 80);
|
|
207
|
+
try { return send({ status: 200, body: { ws: w.id, ...burndownFor(ctx, w, idsOf(url), board || null, sprint) } }); }
|
|
208
|
+
catch (e) { return send(bad(500, 'the burndown could not be read: ' + err(e))); }
|
|
209
|
+
}
|
|
210
|
+
const id = text(url.searchParams.get('id'));
|
|
211
|
+
if (!ID_RE.test(id)) return send(bad(400, 'id must be an initiative id (letters, digits, . _ -)'));
|
|
212
|
+
if (p === '/api/stage-tools/records') {
|
|
213
|
+
const file = ledgerFile(ctx, w);
|
|
214
|
+
return send({ status: 200, body: { ok: true, ws: w.id, id, records: S.recordsOf(file ? S.readLedger(file) : new Map(), id) } });
|
|
215
|
+
}
|
|
216
|
+
try { return send({ status: 200, body: held(ctx, w, id, board || null) }); }
|
|
217
|
+
catch (e) { return send(bad(500, 'what PMOS holds could not be read: ' + err(e))); }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { prefixes: ['/api/stage-tools'], handle, held, burndownFor, NO_JIRA };
|
|
@@ -15,7 +15,8 @@ const fs = require('fs');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const crypto = require('crypto');
|
|
17
17
|
const actor = require('./actor.js');
|
|
18
|
-
const {
|
|
18
|
+
const { repoScript } = require('./repo-script.js');
|
|
19
|
+
const { appendOutbox } = require(repoScript('outbox.js'));
|
|
19
20
|
|
|
20
21
|
const BEHAVIORS = ['allow', 'deny', 'accept', 'answer'];
|
|
21
22
|
const RUN_RE = /^sess-\d+$/;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* settings-store.js — the console3 settings that have no home of their own yet (initiative console3-settings).
|
|
3
|
+
*
|
|
4
|
+
* DEFS every setting this store keeps: key → { section, scope, type, options|min|max, def }
|
|
5
|
+
* get(stateDir, key, ws) the value, or its default; what other modules call to read a setting
|
|
6
|
+
* all(stateDir, ws, { env }) every setting with value, default and scope, and the environment overrides
|
|
7
|
+
* set(stateDir, key, value, ws) → { ok: true, value } | { ok: false, status, error }
|
|
8
|
+
* reset(stateDir, key, ws) back to the default
|
|
9
|
+
* validate(key, value) → null | the reason the value is refused
|
|
10
|
+
* overrides(env) the environment variables that win over a setting, with their values
|
|
11
|
+
*
|
|
12
|
+
* One home per setting: a setting that already has a store (the recording language, the Online record, the TypeSafe key,
|
|
13
|
+
* validate consent, profiles) is NOT here. The Settings page reads and writes those through their own routes. This file
|
|
14
|
+
* holds only the settings the console3 features need and that had nowhere to live.
|
|
15
|
+
*
|
|
16
|
+
* Files, in the per-user state folder (outside every repository, so no commit or sync carries them):
|
|
17
|
+
* <state>/settings.json settings for this computer
|
|
18
|
+
* <state>/settings-ws/<ws>.json settings for one workspace
|
|
19
|
+
* Both are written mode 0600 through a temporary file and a rename. A file that cannot be read counts as empty. */
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
const CATEGORIES = Object.freeze(['Discover', 'Plan', 'Do', 'Learn']);
|
|
24
|
+
const TEMPLATES = Object.freeze({
|
|
25
|
+
product: { name: 'Product', stages: [{ name: 'Discover', cat: 'Discover' }, { name: 'Plan', cat: 'Plan' }, { name: 'Build & Deliver', cat: 'Do' }, { name: 'Monitor & Evaluate', cat: 'Learn' }] },
|
|
26
|
+
project: { name: 'Project', stages: [{ name: 'Plan', cat: 'Plan' }, { name: 'Execute', cat: 'Do' }, { name: 'Close', cat: 'Learn' }] },
|
|
27
|
+
simple: { name: 'Simple', stages: [{ name: 'To do', cat: 'Plan' }, { name: 'Doing', cat: 'Do' }, { name: 'Done', cat: 'Learn' }] },
|
|
28
|
+
});
|
|
29
|
+
const AUDIENCES = Object.freeze({ Leadership: [], Team: [] });
|
|
30
|
+
|
|
31
|
+
const one = (section, scope, options, def) => ({ section, scope, type: 'choice', options, def });
|
|
32
|
+
const bool = (section, scope, def) => ({ section, scope, type: 'bool', def });
|
|
33
|
+
const DEFS = Object.freeze({
|
|
34
|
+
'ws.start': one('workspaces', 'computer', ['last', 'ask'], 'last'),
|
|
35
|
+
'ws.briefing': one('workspaces', 'computer', ['one', 'all'], 'one'),
|
|
36
|
+
'board.template': one('boards', 'ws', ['product', 'project', 'simple'], 'product'),
|
|
37
|
+
'board.rules': one('boards', 'ws', ['warn', 'off'], 'warn'),
|
|
38
|
+
'board.templates': { section: 'boards', scope: 'computer', type: 'templates', def: TEMPLATES },
|
|
39
|
+
'conn.cal.window': one('connectors', 'computer', ['7-14', '14-30', '30-60'], '14-30'),
|
|
40
|
+
'conn.cal.refresh': one('connectors', 'computer', ['open', 'hour', 'manual'], 'open'),
|
|
41
|
+
'conn.cal.private': one('connectors', 'computer', ['busy', 'hide', 'title'], 'busy'),
|
|
42
|
+
'conn.jira.refresh': one('connectors', 'ws', ['open', '15', 'manual'], '15'),
|
|
43
|
+
'conn.wispr.wait': one('connectors', 'computer', ['2', '5', '10'], '5'),
|
|
44
|
+
'conn.sender': one('connectors', 'ws', ['none', 'slack', 'gmail'], 'none'),
|
|
45
|
+
'rec.sources': one('recording', 'computer', ['both', 'mic', 'meeting'], 'both'),
|
|
46
|
+
'rec.guard': bool('recording', 'computer', true),
|
|
47
|
+
'rec.echo': bool('recording', 'computer', true),
|
|
48
|
+
'rec.settle': bool('recording', 'computer', false),
|
|
49
|
+
'rec.final': bool('recording', 'computer', true),
|
|
50
|
+
'rec.vocab': { section: 'recording', scope: 'ws', type: 'text', max: 400, def: '' },
|
|
51
|
+
'rec.retain': one('recording', 'computer', ['never', '90', '30', '7'], 'never'),
|
|
52
|
+
'time.zone': { section: 'time', scope: 'computer', type: 'zone', def: '' },
|
|
53
|
+
'time.weekStart': one('time', 'computer', ['mon', 'sun'], 'mon'),
|
|
54
|
+
'time.workStart': { section: 'time', scope: 'computer', type: 'hour', min: 5, max: 12, def: 9 },
|
|
55
|
+
'time.workEnd': { section: 'time', scope: 'computer', type: 'hour', min: 13, max: 23, def: 18 },
|
|
56
|
+
'time.prep': one('time', 'computer', ['15', '30', '45', '60'], '30'),
|
|
57
|
+
'time.buffer': one('time', 'computer', ['0', '5', '10', '15'], '10'),
|
|
58
|
+
'time.tint': bool('time', 'computer', true),
|
|
59
|
+
'time.strip': bool('time', 'computer', true),
|
|
60
|
+
'share.mode': one('sharing', 'ws', ['draft', 'send'], 'draft'),
|
|
61
|
+
'share.audiences': { section: 'sharing', scope: 'ws', type: 'audiences', def: AUDIENCES },
|
|
62
|
+
'sess.max': one('sessions', 'computer', ['2', '3', '4', '6', '8'], '4'),
|
|
63
|
+
'sess.idle': one('sessions', 'computer', ['15', '30', '60', 'never'], '30'),
|
|
64
|
+
'sess.share': one('sessions', 'computer', ['ws', 'job'], 'ws'),
|
|
65
|
+
'look.start': one('appearance', 'computer', ['brief', 'cal', 'work', 'init', 'rep'], 'brief'),
|
|
66
|
+
'upd.check': one('updates', 'computer', ['press', 'daily'], 'press'),
|
|
67
|
+
'upd.apply': one('updates', 'computer', ['ask', 'idle'], 'ask'),
|
|
68
|
+
'upd.kits': one('updates', 'computer', ['all', 'each'], 'all'),
|
|
69
|
+
'upd.channel': one('updates', 'computer', ['stable', 'early'], 'stable'),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/* The environment variables that win over a setting, here or in a setting's own store. `key` names the setting the page
|
|
73
|
+
* shows; `off` is the value that turns the thing off, or null when any value wins. */
|
|
74
|
+
const ENV = Object.freeze([
|
|
75
|
+
{ key: 'notes.language', env: 'PMOS_WHISPER_LANG', off: null, effect: 'Every line is transcribed in this language, whatever the picker says.' },
|
|
76
|
+
{ key: 'rec.echo', env: 'PMOS_NOTES_ECHO', off: 'off', effect: 'Echo removal is off on this computer.' },
|
|
77
|
+
{ key: 'rec.final', env: 'PMOS_NOTES_FINAL', off: 'off', effect: 'The pass after Stop is off on this computer.' },
|
|
78
|
+
{ key: 'notes.wispr', env: 'PMOS_NOTES_WISPR', off: 'off', effect: 'Wispr Flow notes are not fetched on this computer.' },
|
|
79
|
+
{ key: 'notes.engine', env: 'PMOS_NOTES_QWEN', off: 'off', effect: 'Qwen3-ASR is not used on this computer.' },
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const WS_ID = /^[A-Za-z0-9_-]{1,80}$/;
|
|
83
|
+
const clone = (v) => (v && typeof v === 'object' ? JSON.parse(JSON.stringify(v)) : v);
|
|
84
|
+
const fileOf = (stateDir, ws) => (ws ? path.join(stateDir, 'settings-ws', ws + '.json') : path.join(stateDir, 'settings.json'));
|
|
85
|
+
|
|
86
|
+
function readFile(f) {
|
|
87
|
+
try {
|
|
88
|
+
const st = fs.lstatSync(f);
|
|
89
|
+
if (!st.isFile()) return {};
|
|
90
|
+
const o = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
91
|
+
return o && typeof o === 'object' && !Array.isArray(o) ? o : {};
|
|
92
|
+
} catch { return {}; }
|
|
93
|
+
}
|
|
94
|
+
function writeFile(f, obj) {
|
|
95
|
+
fs.mkdirSync(path.dirname(f), { recursive: true, mode: 0o700 });
|
|
96
|
+
try { if (fs.lstatSync(f).isSymbolicLink()) throw new Error('the settings file is a symbolic link; it was not written'); } catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
97
|
+
const tmp = f + '.' + process.pid + '.' + Date.now() + '.tmp';
|
|
98
|
+
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', { mode: 0o600 });
|
|
99
|
+
fs.renameSync(tmp, f);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Which file a key lives in: the workspace's for a workspace setting (ws required), else this computer's. */
|
|
103
|
+
function where(stateDir, key, ws) {
|
|
104
|
+
const d = DEFS[key];
|
|
105
|
+
if (!d) return { ok: false, status: 400, error: `no setting is called ${JSON.stringify(String(key))}` };
|
|
106
|
+
if (d.scope !== 'ws') return { ok: true, file: fileOf(stateDir, null), d };
|
|
107
|
+
if (!ws || !WS_ID.test(String(ws))) return { ok: false, status: 400, error: `${key} belongs to a workspace: say which one with ws` };
|
|
108
|
+
return { ok: true, file: fileOf(stateDir, String(ws)), d };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validName(s, max) { return typeof s === 'string' && s.trim().length > 0 && s.trim().length <= max && !/[\u0000-\u001f]/.test(s); }
|
|
112
|
+
|
|
113
|
+
function validate(key, value) {
|
|
114
|
+
const d = DEFS[key];
|
|
115
|
+
if (!d) return `no setting is called ${JSON.stringify(String(key))}`;
|
|
116
|
+
switch (d.type) {
|
|
117
|
+
case 'choice': return d.options.includes(value) ? null : `${key} must be one of ${d.options.join(', ')}`;
|
|
118
|
+
case 'bool': return typeof value === 'boolean' ? null : `${key} must be true or false`;
|
|
119
|
+
case 'text': return typeof value === 'string' && value.length <= d.max && !/[\u0000-\u0008\u000b-\u001f]/.test(value) ? null : `${key} must be text of at most ${d.max} characters`;
|
|
120
|
+
case 'hour': return Number.isInteger(value) && value >= d.min && value <= d.max ? null : `${key} must be a whole hour from ${d.min} to ${d.max}`;
|
|
121
|
+
case 'zone': {
|
|
122
|
+
if (value === '') return null;
|
|
123
|
+
if (typeof value !== 'string' || value.length > 64) return `${key} must be a time zone such as Asia/Jakarta, or empty for this computer's`;
|
|
124
|
+
try { new Intl.DateTimeFormat('en', { timeZone: value }); return null; } catch { return `${value} is not a time zone this computer knows`; }
|
|
125
|
+
}
|
|
126
|
+
case 'templates': {
|
|
127
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return `${key} must name the templates`;
|
|
128
|
+
const ids = Object.keys(value);
|
|
129
|
+
if (ids.join() !== Object.keys(TEMPLATES).join()) return `${key} must keep the templates ${Object.keys(TEMPLATES).join(', ')}`;
|
|
130
|
+
for (const id of ids) {
|
|
131
|
+
const t = value[id];
|
|
132
|
+
if (!t || !validName(t.name, 40)) return `the ${id} template needs a name of at most 40 characters`;
|
|
133
|
+
if (!Array.isArray(t.stages) || t.stages.length < 1 || t.stages.length > 12) return `the ${id} template needs 1 to 12 stages`;
|
|
134
|
+
const seen = new Set();
|
|
135
|
+
for (const s of t.stages) {
|
|
136
|
+
if (!s || !validName(s.name, 40)) return `every stage of the ${id} template needs a name of at most 40 characters`;
|
|
137
|
+
if (!CATEGORIES.includes(s.cat)) return `a stage's category must be one of ${CATEGORIES.join(', ')}`;
|
|
138
|
+
const n = s.name.trim().toLowerCase();
|
|
139
|
+
if (seen.has(n)) return `the ${id} template has two stages called ${s.name.trim()}`;
|
|
140
|
+
seen.add(n);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
case 'audiences': {
|
|
146
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return `${key} must name the audiences`;
|
|
147
|
+
const names = Object.keys(value);
|
|
148
|
+
if (names.length < 1 || names.length > 8) return 'keep 1 to 8 audiences';
|
|
149
|
+
for (const a of names) {
|
|
150
|
+
if (!validName(a, 40)) return 'an audience needs a name of at most 40 characters';
|
|
151
|
+
const p = value[a];
|
|
152
|
+
if (!Array.isArray(p) || p.length > 30 || !p.every((x) => validName(x, 60))) return `the ${a} audience must list at most 30 people, each a name of at most 60 characters`;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
default: return `${key} cannot be set`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Tidy a valid value before it is stored: names trimmed. */
|
|
161
|
+
function normal(key, value) {
|
|
162
|
+
const t = DEFS[key].type;
|
|
163
|
+
if (t === 'templates') return Object.fromEntries(Object.entries(value).map(([id, x]) => [id, { name: x.name.trim(), stages: x.stages.map((s) => ({ name: s.name.trim(), cat: s.cat })) }]));
|
|
164
|
+
if (t === 'audiences') return Object.fromEntries(Object.entries(value).map(([a, p]) => [a.trim(), [...new Set(p.map((x) => x.trim()))]]));
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function get(stateDir, key, ws) {
|
|
169
|
+
const w = where(stateDir, key, ws);
|
|
170
|
+
if (!w.ok) return DEFS[key] ? clone(DEFS[key].def) : undefined;
|
|
171
|
+
const v = readFile(w.file)[key];
|
|
172
|
+
return v === undefined || validate(key, v) ? clone(w.d.def) : v;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A cross-setting rule: shares can go out on schedule only once a sender is chosen for the workspace. */
|
|
176
|
+
function blockedBy(stateDir, key, value, ws) {
|
|
177
|
+
if (key === 'share.mode' && value === 'send' && get(stateDir, 'conn.sender', ws) === 'none') return 'choose a sender in Connectors before shares go out on schedule';
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function set(stateDir, key, value, ws) {
|
|
182
|
+
const w = where(stateDir, key, ws);
|
|
183
|
+
if (!w.ok) return w;
|
|
184
|
+
const bad = validate(key, value);
|
|
185
|
+
if (bad) return { ok: false, status: 400, error: bad };
|
|
186
|
+
const blocked = blockedBy(stateDir, key, value, ws);
|
|
187
|
+
if (blocked) return { ok: false, status: 409, error: blocked };
|
|
188
|
+
const cur = readFile(w.file);
|
|
189
|
+
cur[key] = normal(key, value);
|
|
190
|
+
// a sender taken away puts shares back to drafts
|
|
191
|
+
if (key === 'conn.sender' && value === 'none' && cur['share.mode'] === 'send') delete cur['share.mode'];
|
|
192
|
+
try { writeFile(w.file, cur); } catch (e) { return { ok: false, status: 500, error: e.message }; }
|
|
193
|
+
return { ok: true, value: clone(cur[key]) };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function reset(stateDir, key, ws) {
|
|
197
|
+
const w = where(stateDir, key, ws);
|
|
198
|
+
if (!w.ok) return w;
|
|
199
|
+
const cur = readFile(w.file);
|
|
200
|
+
if (key in cur) { delete cur[key]; try { writeFile(w.file, cur); } catch (e) { return { ok: false, status: 500, error: e.message }; } }
|
|
201
|
+
return { ok: true, value: clone(w.d.def) };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function overrides(env = process.env) {
|
|
205
|
+
return ENV.filter((o) => env[o.env] !== undefined && env[o.env] !== '' && (o.off === null || String(env[o.env]) === o.off))
|
|
206
|
+
.map((o) => ({ key: o.key, env: o.env, value: String(env[o.env]).slice(0, 40), effect: o.effect }));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function all(stateDir, ws, { env = process.env } = {}) {
|
|
210
|
+
const okWs = ws && WS_ID.test(String(ws)) ? String(ws) : null;
|
|
211
|
+
const mine = readFile(fileOf(stateDir, null));
|
|
212
|
+
const theirs = okWs ? readFile(fileOf(stateDir, okWs)) : {};
|
|
213
|
+
const settings = Object.entries(DEFS).map(([key, d]) => {
|
|
214
|
+
const src = d.scope === 'ws' ? theirs : mine;
|
|
215
|
+
const has = src[key] !== undefined && !validate(key, src[key]);
|
|
216
|
+
const out = { key, section: d.section, scope: d.scope, type: d.type, def: clone(d.def), value: has ? clone(src[key]) : clone(d.def), changed: has && JSON.stringify(src[key]) !== JSON.stringify(d.def) };
|
|
217
|
+
if (d.options) out.options = d.options.slice();
|
|
218
|
+
if (d.type === 'hour') { out.min = d.min; out.max = d.max; }
|
|
219
|
+
if (d.type === 'text') out.max = d.max;
|
|
220
|
+
if (d.scope === 'ws' && !okWs) out.value = clone(d.def);
|
|
221
|
+
return out;
|
|
222
|
+
});
|
|
223
|
+
return { settings, overrides: overrides(env), categories: CATEGORIES.slice() };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { DEFS, ENV, CATEGORIES, TEMPLATES, get, set, reset, all, validate, overrides, fileOf };
|