segreant 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSING.md +5 -3
  2. package/README.md +57 -19
  3. package/bin/segreant-statusline.mjs +20 -0
  4. package/bin/segreant.mjs +19 -8
  5. package/dist/alerts/detect.js +13 -0
  6. package/dist/billing/epistemic.js +4 -0
  7. package/dist/billing/importer.js +29 -10
  8. package/dist/billing/reconcile.js +122 -32
  9. package/dist/billing/scope.js +7 -0
  10. package/dist/budget/guard.js +33 -4
  11. package/dist/cli/allocCmd.js +73 -4
  12. package/dist/cli/askModelPrices.js +59 -0
  13. package/dist/cli/backupCmd.js +2 -1
  14. package/dist/cli/basisLines.js +45 -0
  15. package/dist/cli/billingCmd.js +68 -9
  16. package/dist/cli/capitalCmd.js +2 -1
  17. package/dist/cli/connectCmd.js +50 -9
  18. package/dist/cli/egressCmd.js +15 -3
  19. package/dist/cli/evidenceCmd.js +6 -3
  20. package/dist/cli/flags.js +68 -0
  21. package/dist/cli/homeCmd.js +117 -0
  22. package/dist/cli/importCmd.js +210 -54
  23. package/dist/cli/launchCmd.js +2 -1
  24. package/dist/cli/opsCmd.js +49 -2
  25. package/dist/cli/planCmd.js +181 -0
  26. package/dist/cli/pricingDeclareCmd.js +178 -0
  27. package/dist/cli/quotaCmd.js +190 -0
  28. package/dist/cli/runCmd.js +203 -51
  29. package/dist/cli/showCmd.js +37 -11
  30. package/dist/cli/teamCmd.js +5 -5
  31. package/dist/cli/ui.js +24 -8
  32. package/dist/cli/valueCmd.js +189 -14
  33. package/dist/cli.js +102 -21
  34. package/dist/config.js +95 -6
  35. package/dist/connect/antigravity.js +310 -0
  36. package/dist/connect/claudeCode.js +43 -3
  37. package/dist/connect/codex.js +38 -1
  38. package/dist/connect/importShared.js +58 -5
  39. package/dist/connect/opencode.js +37 -9
  40. package/dist/connect/protobuf.js +126 -0
  41. package/dist/connect/registry.js +46 -0
  42. package/dist/connect/repoRoot.js +112 -0
  43. package/dist/cost/basis.js +168 -0
  44. package/dist/cost/declarations.js +37 -0
  45. package/dist/cost/exactPricing.js +4 -1
  46. package/dist/cost/firstSight.js +98 -0
  47. package/dist/cost/overrides.js +230 -0
  48. package/dist/cost/pricing.js +228 -15
  49. package/dist/cost/reprice.js +130 -0
  50. package/dist/dashboard/claim-support.js +11 -8
  51. package/dist/dashboard/contracts.js +23 -3
  52. package/dist/dashboard/routes.js +980 -112
  53. package/dist/dashboard/sessions.js +345 -0
  54. package/dist/dashboard/settings.js +2 -1
  55. package/dist/dashboard/value-cache.js +105 -0
  56. package/dist/dashboard/web/app/components/claimInspector.js +6 -5
  57. package/dist/dashboard/web/app/components/drawer.js +105 -35
  58. package/dist/dashboard/web/app/components/ruleFields.js +144 -0
  59. package/dist/dashboard/web/app/components/spine.js +54 -24
  60. package/dist/dashboard/web/app/components/workflow.js +82 -0
  61. package/dist/dashboard/web/app/core/actions.js +734 -23
  62. package/dist/dashboard/web/app/core/api.js +37 -1
  63. package/dist/dashboard/web/app/core/chain.js +11 -4
  64. package/dist/dashboard/web/app/core/claimLayers.js +179 -51
  65. package/dist/dashboard/web/app/core/claimTypes.js +30 -0
  66. package/dist/dashboard/web/app/core/fmt.js +19 -2
  67. package/dist/dashboard/web/app/core/generated-contract.js +23 -3
  68. package/dist/dashboard/web/app/core/generated-payload-contract.js +1944 -465
  69. package/dist/dashboard/web/app/core/generated-types.js +1 -1
  70. package/dist/dashboard/web/app/core/griffin.js +10 -0
  71. package/dist/dashboard/web/app/core/jobs.js +12 -0
  72. package/dist/dashboard/web/app/core/period.js +35 -0
  73. package/dist/dashboard/web/app/core/periodWords.js +10 -0
  74. package/dist/dashboard/web/app/core/periods.js +32 -0
  75. package/dist/dashboard/web/app/core/pricingActions.js +264 -0
  76. package/dist/dashboard/web/app/core/privacyText.js +78 -0
  77. package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
  78. package/dist/dashboard/web/app/core/registry.js +79 -46
  79. package/dist/dashboard/web/app/core/slug.js +9 -0
  80. package/dist/dashboard/web/app/core/theme.js +48 -0
  81. package/dist/dashboard/web/app/core/view.js +69 -0
  82. package/dist/dashboard/web/app/main.js +197 -15
  83. package/dist/dashboard/web/app/views/allocation.js +249 -61
  84. package/dist/dashboard/web/app/views/control.js +57 -16
  85. package/dist/dashboard/web/app/views/data.js +4 -4
  86. package/dist/dashboard/web/app/views/evidence.js +336 -87
  87. package/dist/dashboard/web/app/views/privacy.js +13 -0
  88. package/dist/dashboard/web/app/views/spend.js +30 -15
  89. package/dist/dashboard/web/app/views/statement.js +189 -0
  90. package/dist/dashboard/web/app/views/system.js +44 -4
  91. package/dist/dashboard/web/app/views/value.js +105 -59
  92. package/dist/dashboard/web/app/views/workbench.js +312 -0
  93. package/dist/dashboard/web/classic.html +16 -4
  94. package/dist/dashboard/web/index.html +1 -1
  95. package/dist/dashboard/web/styles/app.css +536 -23
  96. package/dist/demo/seed.js +153 -14
  97. package/dist/economics/ledger.js +1 -1
  98. package/dist/egress/policy.js +129 -5
  99. package/dist/export/allocationCsv.js +70 -0
  100. package/dist/export/billingCsv.js +6 -2
  101. package/dist/export/csv.js +10 -2
  102. package/dist/export/economic.js +6 -2
  103. package/dist/export/statementCsv.js +104 -0
  104. package/dist/git/correlate.js +86 -9
  105. package/dist/git/quality.js +107 -24
  106. package/dist/git/repoScope.js +207 -0
  107. package/dist/git/survivalStore.js +186 -0
  108. package/dist/guide.js +9 -4
  109. package/dist/plans/detect.js +147 -0
  110. package/dist/proxy/server.js +134 -14
  111. package/dist/proxy/usage.js +9 -4
  112. package/dist/quota/limits.js +138 -0
  113. package/dist/quota/statusline.js +68 -0
  114. package/dist/quota/view.js +122 -0
  115. package/dist/scan/knownApps.js +6 -2
  116. package/dist/scan/scan.js +9 -0
  117. package/dist/store/allocation.js +14 -0
  118. package/dist/store/db.js +259 -34
  119. package/dist/store/realization.js +78 -14
  120. package/dist/store/schema.js +59 -1
  121. package/dist/store/scopeEvidence.js +67 -0
  122. package/dist/util/pool.js +19 -0
  123. package/dist/util/time.js +7 -0
  124. package/dist/value/realization.js +321 -30
  125. package/dist/value/report.js +28 -8
  126. package/dist/value/selfReported.js +10 -9
  127. package/docs/ARCHITECTURE.md +8 -4
  128. package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +33 -11
  129. package/docs/CLI-JSON-CONTRACT.md +38 -0
  130. package/docs/DATA-BOUNDARIES.md +66 -6
  131. package/docs/FAQ.md +43 -9
  132. package/docs/GETTING-STARTED.md +19 -12
  133. package/docs/GUIDE.md +81 -18
  134. package/docs/INTEGRATIONS.md +9 -9
  135. package/docs/METHODOLOGY.md +3 -2
  136. package/docs/NEUTRALITY.md +1 -1
  137. package/docs/PRIVACY.md +51 -0
  138. package/docs/README.md +1 -0
  139. package/docs/RELEASE-GATE.md +52 -7
  140. package/docs/THREAT-MODEL.md +2 -1
  141. package/package.json +3 -2
  142. package/pricing/models.json +1366 -60
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Statement — one calendar month, as someone who looks after the money reads it.
3
+ *
4
+ * The four claims sit side by side and are never added together: what Segreant
5
+ * measured, what the provider billed, how it was split across cost centres, and
6
+ * what the work produced. Each carries its own basis, and a claim the ledger
7
+ * cannot establish for the month says so and names the page that establishes
8
+ * it, rather than showing a zero.
9
+ *
10
+ * A billed or allocated run is a fact about ITS OWN period. When that period is
11
+ * not exactly the month, its figures are labelled with its dates, never as the
12
+ * month's, and never prorated into it.
13
+ */
14
+ import { h } from '../core/dom.js';
15
+ import { signal, scopedEffect } from '../core/signal.js';
16
+ import { api } from '../core/api.js';
17
+ import { isPrecise, usd, usdFromMicros, count, pct, share, date, relative } from '../core/fmt.js';
18
+ import { quantizationWords } from '../core/reconcileWords.js';
19
+ import { meteredBasisLine } from '../core/claimLayers.js';
20
+ import { errorText } from '../components/workflow.js';
21
+ function monthKey(d) {
22
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
23
+ }
24
+ function shiftMonth(key, by) {
25
+ const [y, m] = key.split('-').map(Number);
26
+ return monthKey(new Date(y, m - 1 + by, 1));
27
+ }
28
+ /** A run's own period, with the last day shown inclusively. */
29
+ function runPeriod(startMs, endMs, timeZone) {
30
+ return `${date(startMs, timeZone)} – ${date(endMs - 1, timeZone)}${timeZone ? ' UTC' : ''}`;
31
+ }
32
+ /**
33
+ * How a run's own period relates to the month. In a month still in progress, a
34
+ * run from the 1st up to when it was recorded is the month so far, which
35
+ * "part of the month only" would misdescribe.
36
+ */
37
+ function coverWords(covers, startMs, endMs, period, computedAtMs, timeZone) {
38
+ const span = runPeriod(startMs, endMs, timeZone);
39
+ if (covers)
40
+ return `covers the whole month (${span})`;
41
+ if (!period.complete && startMs <= period.startMs && endMs >= computedAtMs - 3_600_000)
42
+ return `covers the month so far (${span})`;
43
+ return `covers part of the month only (${span})`;
44
+ }
45
+ export function statementView() {
46
+ const month = signal(monthKey(new Date()));
47
+ const data = signal(null);
48
+ const error = signal(null);
49
+ scopedEffect(() => {
50
+ const m = month();
51
+ data.set(null);
52
+ error.set(null);
53
+ void api.statement(m).then((p) => data.set(p)).catch((e) => error.set(errorText(e)));
54
+ });
55
+ const thisMonth = monthKey(new Date());
56
+ // While a month loads, the live label says its name, never the raw key: NVDA
57
+ // read "2026-09" and then "September 2026" (2026-10-15).
58
+ const monthName = (key) => {
59
+ const [y, m] = key.split('-').map(Number);
60
+ return new Date(y, (m ?? 1) - 1, 1).toLocaleString('en-GB', { month: 'long', year: 'numeric' });
61
+ };
62
+ return h('div', null, h('div', { class: 'view-head' }, h('h1', { class: 'view-title', text: 'Statement' }), h('p', { class: 'view-plain', text: () => isPrecise()
63
+ ? 'One calendar month: metered, provider-billed, allocated and realized, each on its own evidence. They answer different questions and are never summed.'
64
+ : 'One month at a glance: what we measured, what your provider billed, how it was split, and what it produced. Four different questions, so the four figures are never added up.' })), h('div', { class: 'month-bar', role: 'group', 'aria-label': 'Month' }, h('button', { class: 'btn-ghost', type: 'button', 'aria-label': 'Previous month', text: '‹', onclick: () => month.set(shiftMonth(month(), -1)) }), h('span', { class: 'month-label', 'aria-live': 'polite', text: () => data()?.period.label ?? monthName(month()) }), h('button', { class: 'btn-ghost', type: 'button', 'aria-label': 'Next month', text: '›',
65
+ disabled: () => month() >= thisMonth, onclick: () => month.set(shiftMonth(month(), 1)) }), h('span', { class: 'month-state', text: () => {
66
+ const d = data();
67
+ if (!d)
68
+ return '';
69
+ return d.period.complete ? 'month complete' : 'month in progress, figures so far';
70
+ } }),
71
+ // A plain link the browser saves: the same four claims, every project and
72
+ // model, as four sections with no total row.
73
+ h('a', { class: 'statement-download', href: () => api.statementCsvHref(month()), download: '',
74
+ text: () => (isPrecise() ? 'Export CSV' : 'Download as CSV') })), () => {
75
+ const err = error();
76
+ if (err)
77
+ return h('div', { class: 'card' }, h('p', { class: 'drawer-error', role: 'alert', text: err }));
78
+ const d = data();
79
+ if (!d)
80
+ return h('div', { class: 'card' }, h('p', { class: 'drawer-muted', role: 'status', 'aria-busy': 'true', text: 'Reading the month…' }));
81
+ const names = new Map(d.allocated.costCentres.map((c) => [c.costCentreId, c.name]));
82
+ return h('div', null, d.demo ? h('p', { class: 'section-note', role: 'note', text: 'Sample data: these figures were generated, not measured on your machine.' }) : null, h('div', { class: 'statement-grid' }, meteredCard(d), billedCard(d), allocatedCard(d, (id) => names.get(id) ?? id), realizedCard(d)), h('p', { class: 'basis statement-rule', text: () => (isPrecise()
83
+ ? 'Not summable: metered is a local estimate, billed is a provider figure compared with it, allocated is a split of the estimate, realized is value. Each card states its own basis.'
84
+ : 'These four are not parts of one total. Each answers its own question, on its own evidence.') }), breakdown(d), closeLine(d));
85
+ });
86
+ }
87
+ function card(title, pill, ...body) {
88
+ return h('div', { class: 'card statement-card' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', text: title }), pill ? h('span', { class: `pill ${pill.tone}`, text: pill.text }) : null), ...body);
89
+ }
90
+ function goLink(route, text) {
91
+ return h('a', { class: 'statement-link', href: `#/${route}`, text });
92
+ }
93
+ function meteredCard(d) {
94
+ const m = d.metered;
95
+ return card(() => (isPrecise() ? 'Metered' : 'What we measured'), { text: 'estimate', tone: 'pill-estimate' }, h('div', { class: 'stat', text: usd(m.costUsd) }), h('span', { class: 'basis', text: `${count(m.requests)} request${m.requests === 1 ? '' : 's'} · ${meteredBasisLine(m.basis)}` }), m.retention.truncated
96
+ ? h('p', { class: 'drawer-error', text: `Retention deleted ${count(m.retention.rowsRemoved)} record(s) from this period, so this covers what survived.` })
97
+ : null, goLink('spend', 'See where it went'));
98
+ }
99
+ /** Whether one recorded comparison carries a problem the reader must see. */
100
+ function runWarnings(r) {
101
+ const out = [];
102
+ if (r.snapshotStability === 'changed_across_observations')
103
+ out.push('The provider reported different figures for the same days. This comparison is conflicted.');
104
+ else if (r.snapshotStability === null)
105
+ out.push('Provider snapshot stability was not recorded.');
106
+ if (r.offPathBound === 'none_local_estimate_exceeds_provider')
107
+ out.push('The local estimate exceeds the provider total. This remainder cannot bound spend outside Segreant.');
108
+ else if (r.offPathBound === 'unknown_local_total_truncated_by_retention')
109
+ out.push('Retention deleted local request rows in this period. This remainder cannot bound spend outside Segreant.');
110
+ else if (r.offPathBound === null)
111
+ out.push('The remainder bound was not recorded; what it establishes is unknown.');
112
+ return out;
113
+ }
114
+ /** The conditions a sound comparison still rests on, said once and quietly. */
115
+ function runConditions(r) {
116
+ const parts = [];
117
+ if (r.snapshotStability === 'stable_across_observations')
118
+ parts.push('Repeated provider observations agreed.');
119
+ else if (r.snapshotStability === 'single_observation')
120
+ parts.push('One provider observation, so agreement across observations is not established.');
121
+ if (r.offPathBound === 'upper_bound_conditional')
122
+ parts.push('The remainder bounds spend outside Segreant only if our pricing did not overstate the cost of requests through Segreant.');
123
+ return parts.join(' ');
124
+ }
125
+ function billedRunLine(r, period) {
126
+ const conditions = runConditions(r);
127
+ return h('div', { class: 'statement-run', role: 'group', 'aria-label': `Provider comparison for ${r.providerProjectRef ?? 'an unrecorded project'}` }, h('p', { text: `Provider billed ${usdFromMicros(r.providerReportedMicros)}; we measured ${usdFromMicros(r.localCapturedMicros)}; ${usdFromMicros(r.unexplainedVarianceMicros)} not explained${r.providerReportedMicros > 0 ? ` (${pct(r.unexplainedVarianceMicros / r.providerReportedMicros)})` : ''}.` }), ...runWarnings(r).map((w) => h('p', { class: 'drawer-error', text: w })), h('span', { class: 'basis', text: `${coverWords(r.coversWholeMonth, r.periodStartMs, r.periodEndMs, period, r.computedAtMs, 'UTC')} · project ${r.providerProjectRef ?? 'not recorded'} · ${r.providerSourceKind === 'operator_supplied_export' ? 'from an export you supplied' : r.providerSourceKind === 'provider_api_pull' ? 'read from the provider' : 'source not recorded'} · recorded ${relative(r.computedAtMs)}` }), conditions ? h('span', { class: 'basis', text: conditions }) : null, () => { const q = quantizationWords(r.localQuantization, isPrecise()); return q ? h('span', { class: 'basis', text: q }) : null; });
128
+ }
129
+ /**
130
+ * The card's one-word summary. Calendar coverage alone never earns a green
131
+ * pill: a conflicted or unbounded comparison says so even when it spans the
132
+ * whole month.
133
+ */
134
+ function billedPill(b) {
135
+ if (b.runs.some((r) => r.snapshotStability === 'changed_across_observations'))
136
+ return { text: 'conflicted', tone: 'pill-warn' };
137
+ if (b.runs.some((r) => runWarnings(r).length > 0))
138
+ return { text: 'compared, limits apply', tone: 'pill-unverified' };
139
+ return b.monthCoverage === 'whole' ? { text: 'compared', tone: 'pill-ok' } : { text: 'partly compared', tone: 'pill-unverified' };
140
+ }
141
+ function billedCard(d) {
142
+ const b = d.billed;
143
+ if (b.monthCoverage === 'not_established') {
144
+ return card(() => (isPrecise() ? 'Provider-billed' : 'What the provider billed'), { text: 'not established', tone: 'pill-empty' }, h('p', { class: 'statement-none', text: b.scanComplete
145
+ ? 'No comparison with a provider bill covers this month.'
146
+ : 'No comparison found among the most recent ones; an older one may exist.' }), goLink('evidence', 'Close this month'));
147
+ }
148
+ return card(() => (isPrecise() ? 'Provider-billed' : 'What the provider billed'), billedPill(b), ...b.runs.slice(0, 3).map((r) => billedRunLine(r, d.period)), b.runs.length > 3 ? h('span', { class: 'basis', text: `${b.runs.length - 3} more comparison(s) overlap this month.` }) : null, goLink('evidence', 'Open Close'));
149
+ }
150
+ function allocatedRunLine(r, nameOf, period) {
151
+ return h('div', { class: 'statement-run' }, h('ul', { class: 'statement-shares' }, ...r.byCostCentre.map((c) => h('li', null, h('span', { text: nameOf(c.costCentreId) }), h('span', { class: 'num', text: `${usdFromMicros(c.allocatedMicros)}${r.totalMicros > 0 ? ` · ${share(c.allocatedMicros / r.totalMicros)}` : ''}` }))), r.unallocatedMicros > 0
152
+ ? h('li', { class: 'row-unalloc' }, h('span', { text: 'Not allocated' }), h('span', { class: 'num', text: usdFromMicros(r.unallocatedMicros) }))
153
+ : null), h('span', { class: 'basis', text: `a split of ${usdFromMicros(r.totalMicros)} of our estimate · ${coverWords(r.coversWholeMonth, r.periodStartMs, r.periodEndMs, period, r.computedAtMs)} · recorded ${relative(r.computedAtMs)}` }));
154
+ }
155
+ function allocatedCard(d, nameOf) {
156
+ const a = d.allocated;
157
+ if (a.monthCoverage === 'not_established') {
158
+ return card(() => (isPrecise() ? 'Allocated' : 'How it was split'), { text: 'not established', tone: 'pill-empty' }, h('p', { class: 'statement-none', text: a.scanComplete
159
+ ? 'No recorded split covers this month.'
160
+ : 'No split found among the most recent ones; an older one may exist.' }), goLink('allocation', 'Split this month'));
161
+ }
162
+ return card(() => (isPrecise() ? 'Allocated' : 'How it was split'), { text: 'showback only', tone: 'pill-estimate' }, ...a.runs.slice(0, 2).map((r) => allocatedRunLine(r, nameOf, d.period)), a.runs.length > 2 ? h('span', { class: 'basis', text: `${a.runs.length - 2} more split(s) overlap this month.` }) : null, goLink('allocation', 'Open Allocate'));
163
+ }
164
+ function realizedCard(d) {
165
+ const r = d.realized;
166
+ const title = () => (isPrecise() ? 'Realized' : 'What it produced');
167
+ if (r.standing === 'none_judged' && r.tooEarly.units === 0) {
168
+ return card(title, { text: 'nothing judged', tone: 'pill-empty' }, h('p', { class: 'statement-none', text: r.measuredAt === null ? r.reason : 'No work had its check in this month.' }), goLink('value', 'Open Value'));
169
+ }
170
+ const pieces = (n) => `${count(n)} ${n === 1 ? 'piece' : 'pieces'} of work`;
171
+ const line = (label, c, tone) => h('div', { class: 'statement-kept-row' }, h('span', { class: `statement-kept-key ${tone}`, text: label }), h('span', { class: 'statement-kept-val', text: `${pieces(c.units)} · ${usd(c.costUsd)} list cost` }));
172
+ const due = r.tooEarly.nextCheckMs === null ? '' : `, first check ${new Date(r.tooEarly.nextCheckMs).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })}`;
173
+ return card(title, { text: 'counts, not value', tone: 'pill-estimate' }, h('div', { class: 'statement-kept' }, line('Kept', r.kept, 'gate-pass'), line('Not kept', r.notKept, 'gate-fail'), r.contested.units > 0 ? line('Results disagree', r.contested, 'gate-unchecked') : null, r.unknown.units > 0 ? line('No signal', r.unknown, 'gate-unchecked') : null, r.tooEarly.units > 0 ? h('div', { class: 'statement-kept-row' }, h('span', { class: 'statement-kept-key gate-unchecked', text: 'Too early to tell' }), h('span', { class: 'statement-kept-val', text: `${pieces(r.tooEarly.units)}${due}` })) : null), h('span', { class: 'basis', text: r.reason }), goLink('value', 'Open Value'));
174
+ }
175
+ function breakdown(d) {
176
+ const m = d.metered;
177
+ if (m.byProject.length === 0 && m.byModel.length === 0)
178
+ return null;
179
+ const table = (label, rows) => h('div', { class: 'table-wrap' }, h('table', { 'aria-label': label }, h('thead', null, h('tr', null, h('th', { scope: 'col', text: label }), h('th', { scope: 'col', class: 'num', text: 'Measured' }), h('th', { scope: 'col', class: 'num', text: 'Share' }), h('th', { scope: 'col', class: 'num', text: 'Requests' }))), h('tbody', null, ...rows.map((r) => h('tr', null, h('td', { class: 'name-cell', text: r.label }), h('td', { class: 'num', text: usd(r.costUsd) }), h('td', { class: 'num', text: m.costUsd > 0 ? share(r.costUsd / m.costUsd) : '—' }), h('td', { class: 'num', text: count(r.requests) }))))));
180
+ return h('section', { class: 'section' }, h('h2', { class: 'section-title', text: () => (isPrecise() ? 'Metered, by project and model' : 'Where the measured spend went') }), h('div', { class: 'grid grid-2' }, table('Project', m.byProject), table('Model', m.byModel)));
181
+ }
182
+ function closeLine(d) {
183
+ const s = d.close.status;
184
+ const words = s === 'finalized' ? 'This month is finalized in the economic ledger.'
185
+ : s === 'reopened' ? 'This month was finalized, then reopened.'
186
+ : s === 'conflicted' ? 'This month has conflicting finalizations.'
187
+ : 'This month is not finalized in the economic ledger.';
188
+ return h('p', { class: 'basis', style: 'margin-top: var(--s6)', text: () => (isPrecise() ? `period close: ${s}` : words) });
189
+ }
@@ -7,10 +7,15 @@
7
7
  * routes through. A capability with no screen says so here, inside the product,
8
8
  * rather than in a README nobody opens.
9
9
  */
10
+ import { themeChoice, setTheme } from '../core/theme.js';
11
+ import { viewPrefs, setView, setJobShown, jobShown } from '../core/view.js';
12
+ import { JOBS } from '../core/jobs.js';
10
13
  import { h } from '../core/dom.js';
11
14
  import { CAPABILITY_SPECS, TERRITORIES, paritySummary } from '../core/registry.js';
12
15
  import { isPrecise } from '../core/fmt.js';
13
16
  import { actionCard } from './spend.js';
17
+ import { actionSpec, hasRunner } from '../core/actions.js';
18
+ import { openAction } from '../components/drawer.js';
14
19
  const COVERAGE_WORDS = {
15
20
  full: 'in the GUI',
16
21
  partial: 'partly in the GUI',
@@ -19,22 +24,57 @@ const COVERAGE_WORDS = {
19
24
  // screen has not been built, and here one cannot be.
20
25
  not_applicable: 'cannot have a screen',
21
26
  };
27
+ const THEME_WORDS = [['system', 'Match this computer'], ['light', 'Light'], ['dark', 'Dark']];
28
+ /** Appearance is kept in this browser only; nothing about it reaches the ledger or the config file. */
29
+ function appearance() {
30
+ return h('div', { class: 'card appearance' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', id: 'appearance-title', text: 'Appearance' })), h('div', { class: 'register-switch theme-switch', role: 'group', 'aria-labelledby': 'appearance-title' }, ...THEME_WORDS.map(([choice, label]) => h('button', {
31
+ type: 'button',
32
+ 'aria-pressed': () => (themeChoice() === choice ? 'true' : 'false'),
33
+ onclick: () => setTheme(choice),
34
+ text: label,
35
+ }))), h('p', { class: 'basis', text: 'Saved in this browser only.' }));
36
+ }
37
+ const DENSITY_WORDS = [['comfortable', 'Comfortable'], ['compact', 'Compact']];
38
+ function check(label, on, change) {
39
+ return h('label', { class: 'pref' }, h('input', { type: 'checkbox', checked: on, onchange: (e) => change(e.target.checked) }), h('span', { text: label }));
40
+ }
41
+ /** What you see. Display only: hiding a part never changes a figure. */
42
+ function viewChoices() {
43
+ const p = viewPrefs();
44
+ return h('div', { class: 'card view-prefs' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', id: 'view-title', text: 'What you see' })), h('div', { class: 'register-switch theme-switch', role: 'group', 'aria-label': 'Density' }, ...DENSITY_WORDS.map(([d, label]) => h('button', {
45
+ type: 'button',
46
+ 'aria-pressed': () => (viewPrefs().density === d ? 'true' : 'false'),
47
+ onclick: () => setView({ density: d }),
48
+ text: label,
49
+ }))), h('fieldset', { class: 'pref-group' }, h('legend', { text: 'On every page' }), check('The four figures above the money pages', p.strip, (on) => setView({ strip: on })), check('A short explanation under each page title', p.intros, (on) => setView({ intros: on }))), h('fieldset', { class: 'pref-group' }, h('legend', { text: 'Jobs in the top bar' }), ...JOBS.map((j) => check(j.label, jobShown(j.id), (on) => setJobShown(j.id, on))), h('p', { class: 'form-hint', text: 'A hidden job still opens from here, and the job you are in always shows.' }), () => h('div', { class: 'pref-links' }, ...JOBS.filter((j) => !jobShown(j.id)).map((j) => h('a', { href: `#/${j.id}`, text: `Open ${j.label}` })))), h('p', { class: 'basis', text: 'Saved in this browser only. Hiding something never changes a figure.' }));
50
+ }
22
51
  export function systemView() {
23
52
  const parity = paritySummary();
24
- return h('div', null, h('div', { class: 'view-head' }, h('h1', { class: 'view-title', text: 'System' }), h('p', { class: 'view-plain', text: () => isPrecise()
53
+ return h('div', null, h('div', { class: 'view-head' }, h('h1', { class: 'view-title', text: 'Settings' }), h('p', { class: 'view-plain', text: () => isPrecise()
25
54
  ? 'Local configuration, rate cards, maintenance, and the CLI/GUI parity map rendered from the same registry the GUI routes through.'
26
- : 'Settings for this machine, plus an honest list of everything Segreant can do and whether this web interface can do it yet.' })), h('div', { class: 'grid' }, h('div', { class: 'card' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', text: 'In the GUI' })), h('div', { class: 'stat', text: String(parity.full) }), h('span', { class: 'basis', text: `of ${parity.total} capabilities` })), h('div', { class: 'card' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', text: 'Partly' })), h('div', { class: 'stat', text: String(parity.partial) }), h('span', { class: 'basis', text: 'usable, with options only on the CLI' })), h('div', { class: 'card' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', text: 'Command line only' })), h('div', { class: 'stat', text: String(parity.planned) }), h('span', { class: 'basis', text: 'no screen yet — the command is listed' })),
55
+ : 'Settings for this machine, plus a list of everything Segreant can do and whether it is done here or from the command line.' })),
56
+ // The settings come first: this is the page someone opens to change one.
57
+ // The parity map used to lead, a 60-row table that made this the longest page
58
+ // in the product (10,140 px) with the settings at the very bottom.
59
+ appearance(), viewChoices(), h('div', { class: 'actions', style: 'margin-top: var(--s5)' }, actionCard('settings'), actionCard('features'), actionCard('doctor'), actionCard('clear-proposals'), actionCard('prune')),
60
+ // Kept, and kept complete: the map is a claim the product makes about itself.
61
+ // Closed by default, with its counts in the summary so the answer to "how
62
+ // much can the GUI do" is readable without opening it.
63
+ h('details', { class: 'section parity-details' }, h('summary', { class: 'parity-summary' }, h('span', { class: 'section-title', text: () => (isPrecise() ? 'CLI/GUI parity' : 'What this dashboard can and cannot do') }), h('span', { class: 'parity-counts', text: `${parity.full} in the GUI · ${parity.partial} partly · ${parity.planned} command line only · ${parity.notApplicable} cannot have a screen · of ${parity.total}` })), h('div', { class: 'grid' }, h('div', { class: 'card' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', text: 'In the GUI' })), h('div', { class: 'stat', text: String(parity.full) }), h('span', { class: 'basis', text: `of ${parity.total} capabilities` })), h('div', { class: 'card' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', text: 'Partly' })), h('div', { class: 'stat', text: String(parity.partial) }), h('span', { class: 'basis', text: 'usable, with options only on the CLI' })), h('div', { class: 'card' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', text: 'Command line only' })), h('div', { class: 'stat', text: String(parity.planned) }), h('span', { class: 'basis', text: 'no screen yet — the command is listed' })),
27
64
  // The fourth state gets its own card rather than being folded into
28
65
  // "command line only". A capability the GUI cannot offer is not a gap in
29
66
  // the GUI, and counting it as one would make the parity fraction read
30
67
  // worse than the truth -- while leaving it out of `total` altogether was
31
68
  // the error this card exists to correct.
32
- h('div', { class: 'card' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', text: 'Cannot have a screen' })), h('div', { class: 'stat', text: String(parity.notApplicable) }), h('span', { class: 'basis', text: 'structurally unavailable here — each row says why' }))), h('div', { class: 'card', style: 'margin-top: var(--s4)' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', text: 'Parity map' })), h('p', { class: 'view-plain', style: 'margin-bottom: var(--s4)', text: 'Every capability Segreant has, where it lives, what it costs you to run it, and whether this interface covers it. Generated from the same registry the navigation uses, and checked against the command line\u2019s own dispatch, so it cannot drift from what the GUI does or quietly leave a command out.' }), h('div', { class: 'table-wrap' }, h('table', { 'aria-label': 'CLI/GUI parity map' }, h('thead', null, h('tr', null, h('th', { scope: 'col', text: 'Capability' }), h('th', { scope: 'col', text: 'Section' }), h('th', { scope: 'col', text: 'Consequence' }), h('th', { scope: 'col', text: 'Here' }), h('th', { scope: 'col', text: 'Command' }))), h('tbody', null, ...CAPABILITY_SPECS.map((c) => h('tr', null, h('td', null, h('strong', { text: c.label }), h('br'), h('span', { class: 'action-plain', text: c.plain })), h('td', { text: TERRITORIES.find((t) => t.id === c.territory)?.label ?? c.territory }), h('td', null, h('span', { class: `tag tag-${c.consequence}`, text: c.consequence === 'read' ? 'reads only' : c.consequence })), h('td', null, h('span', {
69
+ h('div', { class: 'card' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', text: 'Cannot have a screen' })), h('div', { class: 'stat', text: String(parity.notApplicable) }), h('span', { class: 'basis', text: 'structurally unavailable here — each row says why' }))), h('div', { class: 'card', style: 'margin-top: var(--s4)' }, h('div', { class: 'card-head' }, h('span', { class: 'card-title', text: 'Parity map' })), h('p', { class: 'view-plain', style: 'margin-bottom: var(--s4)', text: 'Every capability Segreant has, where it lives, what it costs you to run it, and whether this interface covers it. Generated from the same registry the navigation uses, and checked against the command line\u2019s own dispatch, so it cannot drift from what the GUI does or quietly leave a command out.' }), h('div', { class: 'table-wrap' }, h('table', { class: 'parity-table', 'aria-label': 'CLI/GUI parity map' }, h('thead', null, h('tr', null, h('th', { scope: 'col', text: 'Capability' }), h('th', { scope: 'col', text: 'Section' }), h('th', { scope: 'col', text: 'Consequence' }), h('th', { scope: 'col', text: 'Here' }), h('th', { scope: 'col', text: 'Command' }))), h('tbody', null, ...CAPABILITY_SPECS.map((c) => h('tr', null, h('td', null, h('strong', { text: c.label }), h('br'), h('span', { class: 'action-plain', text: c.plain })), h('td', { text: TERRITORIES.find((t) => t.id === c.territory)?.label ?? c.territory }), h('td', null, h('span', { class: `tag tag-${c.consequence}`, text: c.consequence === 'read' ? 'reads only' : c.consequence })), h('td', null, h('span', {
33
70
  class: c.coverage === 'planned' || c.coverage === 'not_applicable' ? 'tag tag-planned' : 'tag',
34
71
  text: COVERAGE_WORDS[c.coverage] ?? c.coverage,
35
72
  }),
36
73
  // The reason travels with the state, in the same cell. A row that
37
74
  // says the GUI cannot do something and does not say why is the
38
75
  // assertion without its basis.
39
- ...(c.coverageNote ? [h('br'), h('span', { class: 'action-plain', text: c.coverageNote })] : [])), h('td', null, h('code', { class: 'cmd', style: 'white-space: nowrap', text: c.command })))))))), h('div', { style: 'margin-top: var(--s6)' }, h('h2', { class: 'card-title', style: 'margin-bottom: var(--s3)', text: 'Maintenance' }), h('div', { class: 'actions' }, actionCard('settings'), actionCard('features'), actionCard('doctor'), actionCard('clear-proposals'), actionCard('prune'))));
76
+ ...(c.coverageNote ? [h('br'), h('span', { class: 'action-plain', text: c.coverageNote })] : []),
77
+ // The map is where a reader learns a capability exists, so a row
78
+ // the GUI can run opens its drawer right here, preview first.
79
+ ...(hasRunner(c) ? [h('br'), h('button', { class: 'link-btn parity-open', type: 'button', 'aria-label': `Open ${c.label}`, text: 'Open', onclick: () => openAction(actionSpec(c)) })] : [])), h('td', null, h('code', { class: 'cmd', style: 'white-space: nowrap', text: c.command }))))))))));
40
80
  }