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.
- package/LICENSING.md +5 -3
- package/README.md +57 -19
- package/bin/segreant-statusline.mjs +20 -0
- package/bin/segreant.mjs +19 -8
- package/dist/alerts/detect.js +13 -0
- package/dist/billing/epistemic.js +4 -0
- package/dist/billing/importer.js +29 -10
- package/dist/billing/reconcile.js +122 -32
- package/dist/billing/scope.js +7 -0
- package/dist/budget/guard.js +33 -4
- package/dist/cli/allocCmd.js +73 -4
- package/dist/cli/askModelPrices.js +59 -0
- package/dist/cli/backupCmd.js +2 -1
- package/dist/cli/basisLines.js +45 -0
- package/dist/cli/billingCmd.js +68 -9
- package/dist/cli/capitalCmd.js +2 -1
- package/dist/cli/connectCmd.js +50 -9
- package/dist/cli/egressCmd.js +15 -3
- package/dist/cli/evidenceCmd.js +6 -3
- package/dist/cli/flags.js +68 -0
- package/dist/cli/homeCmd.js +117 -0
- package/dist/cli/importCmd.js +210 -54
- package/dist/cli/launchCmd.js +2 -1
- package/dist/cli/opsCmd.js +49 -2
- package/dist/cli/planCmd.js +181 -0
- package/dist/cli/pricingDeclareCmd.js +178 -0
- package/dist/cli/quotaCmd.js +190 -0
- package/dist/cli/runCmd.js +203 -51
- package/dist/cli/showCmd.js +37 -11
- package/dist/cli/teamCmd.js +5 -5
- package/dist/cli/ui.js +24 -8
- package/dist/cli/valueCmd.js +189 -14
- package/dist/cli.js +102 -21
- package/dist/config.js +95 -6
- package/dist/connect/antigravity.js +310 -0
- package/dist/connect/claudeCode.js +43 -3
- package/dist/connect/codex.js +38 -1
- package/dist/connect/importShared.js +58 -5
- package/dist/connect/opencode.js +37 -9
- package/dist/connect/protobuf.js +126 -0
- package/dist/connect/registry.js +46 -0
- package/dist/connect/repoRoot.js +112 -0
- package/dist/cost/basis.js +168 -0
- package/dist/cost/declarations.js +37 -0
- package/dist/cost/exactPricing.js +4 -1
- package/dist/cost/firstSight.js +98 -0
- package/dist/cost/overrides.js +230 -0
- package/dist/cost/pricing.js +228 -15
- package/dist/cost/reprice.js +130 -0
- package/dist/dashboard/claim-support.js +11 -8
- package/dist/dashboard/contracts.js +23 -3
- package/dist/dashboard/routes.js +980 -112
- package/dist/dashboard/sessions.js +345 -0
- package/dist/dashboard/settings.js +2 -1
- package/dist/dashboard/value-cache.js +105 -0
- package/dist/dashboard/web/app/components/claimInspector.js +6 -5
- package/dist/dashboard/web/app/components/drawer.js +105 -35
- package/dist/dashboard/web/app/components/ruleFields.js +144 -0
- package/dist/dashboard/web/app/components/spine.js +54 -24
- package/dist/dashboard/web/app/components/workflow.js +82 -0
- package/dist/dashboard/web/app/core/actions.js +734 -23
- package/dist/dashboard/web/app/core/api.js +37 -1
- package/dist/dashboard/web/app/core/chain.js +11 -4
- package/dist/dashboard/web/app/core/claimLayers.js +179 -51
- package/dist/dashboard/web/app/core/claimTypes.js +30 -0
- package/dist/dashboard/web/app/core/fmt.js +19 -2
- package/dist/dashboard/web/app/core/generated-contract.js +23 -3
- package/dist/dashboard/web/app/core/generated-payload-contract.js +1944 -465
- package/dist/dashboard/web/app/core/generated-types.js +1 -1
- package/dist/dashboard/web/app/core/griffin.js +10 -0
- package/dist/dashboard/web/app/core/jobs.js +12 -0
- package/dist/dashboard/web/app/core/period.js +35 -0
- package/dist/dashboard/web/app/core/periodWords.js +10 -0
- package/dist/dashboard/web/app/core/periods.js +32 -0
- package/dist/dashboard/web/app/core/pricingActions.js +264 -0
- package/dist/dashboard/web/app/core/privacyText.js +78 -0
- package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
- package/dist/dashboard/web/app/core/registry.js +79 -46
- package/dist/dashboard/web/app/core/slug.js +9 -0
- package/dist/dashboard/web/app/core/theme.js +48 -0
- package/dist/dashboard/web/app/core/view.js +69 -0
- package/dist/dashboard/web/app/main.js +197 -15
- package/dist/dashboard/web/app/views/allocation.js +249 -61
- package/dist/dashboard/web/app/views/control.js +57 -16
- package/dist/dashboard/web/app/views/data.js +4 -4
- package/dist/dashboard/web/app/views/evidence.js +336 -87
- package/dist/dashboard/web/app/views/privacy.js +13 -0
- package/dist/dashboard/web/app/views/spend.js +30 -15
- package/dist/dashboard/web/app/views/statement.js +189 -0
- package/dist/dashboard/web/app/views/system.js +44 -4
- package/dist/dashboard/web/app/views/value.js +105 -59
- package/dist/dashboard/web/app/views/workbench.js +312 -0
- package/dist/dashboard/web/classic.html +16 -4
- package/dist/dashboard/web/index.html +1 -1
- package/dist/dashboard/web/styles/app.css +536 -23
- package/dist/demo/seed.js +153 -14
- package/dist/economics/ledger.js +1 -1
- package/dist/egress/policy.js +129 -5
- package/dist/export/allocationCsv.js +70 -0
- package/dist/export/billingCsv.js +6 -2
- package/dist/export/csv.js +10 -2
- package/dist/export/economic.js +6 -2
- package/dist/export/statementCsv.js +104 -0
- package/dist/git/correlate.js +86 -9
- package/dist/git/quality.js +107 -24
- package/dist/git/repoScope.js +207 -0
- package/dist/git/survivalStore.js +186 -0
- package/dist/guide.js +9 -4
- package/dist/plans/detect.js +147 -0
- package/dist/proxy/server.js +134 -14
- package/dist/proxy/usage.js +9 -4
- package/dist/quota/limits.js +138 -0
- package/dist/quota/statusline.js +68 -0
- package/dist/quota/view.js +122 -0
- package/dist/scan/knownApps.js +6 -2
- package/dist/scan/scan.js +9 -0
- package/dist/store/allocation.js +14 -0
- package/dist/store/db.js +259 -34
- package/dist/store/realization.js +78 -14
- package/dist/store/schema.js +59 -1
- package/dist/store/scopeEvidence.js +67 -0
- package/dist/util/pool.js +19 -0
- package/dist/util/time.js +7 -0
- package/dist/value/realization.js +321 -30
- package/dist/value/report.js +28 -8
- package/dist/value/selfReported.js +10 -9
- package/docs/ARCHITECTURE.md +8 -4
- package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +33 -11
- package/docs/CLI-JSON-CONTRACT.md +38 -0
- package/docs/DATA-BOUNDARIES.md +66 -6
- package/docs/FAQ.md +43 -9
- package/docs/GETTING-STARTED.md +19 -12
- package/docs/GUIDE.md +81 -18
- package/docs/INTEGRATIONS.md +9 -9
- package/docs/METHODOLOGY.md +3 -2
- package/docs/NEUTRALITY.md +1 -1
- package/docs/PRIVACY.md +51 -0
- package/docs/README.md +1 -0
- package/docs/RELEASE-GATE.md +52 -7
- package/docs/THREAT-MODEL.md +2 -1
- package/package.json +3 -2
- package/pricing/models.json +1366 -60
|
@@ -13,17 +13,25 @@
|
|
|
13
13
|
import { h, render, captureFocus, restoreFocus, trapFocus } from './core/dom.js';
|
|
14
14
|
import { signal, effect, computed, onCleanup } from './core/signal.js';
|
|
15
15
|
import { register, setRegister } from './core/fmt.js';
|
|
16
|
-
import {
|
|
16
|
+
import { GRIFFIN_PATH, GRIFFIN_TRANSFORM, GRIFFIN_VIEWBOX } from './core/griffin.js';
|
|
17
|
+
import { api, ApiError } from './core/api.js';
|
|
18
|
+
import { buildClaimLayers, chainIsSampleData } from './core/claimLayers.js';
|
|
19
|
+
import { period } from './core/period.js';
|
|
17
20
|
import { spine } from './components/spine.js';
|
|
18
21
|
import { mountDrawer } from './components/drawer.js';
|
|
19
22
|
import { mountClaimInspector, openClaimInspector } from './components/claimInspector.js';
|
|
20
23
|
import { spendView } from './views/spend.js';
|
|
21
24
|
import { evidenceView } from './views/evidence.js';
|
|
22
25
|
import { systemView } from './views/system.js';
|
|
26
|
+
import { privacyView } from './views/privacy.js';
|
|
23
27
|
import { allocationView } from './views/allocation.js';
|
|
24
28
|
import { valueView } from './views/value.js';
|
|
25
29
|
import { controlView } from './views/control.js';
|
|
26
30
|
import { dataView } from './views/data.js';
|
|
31
|
+
import { statementView } from './views/statement.js';
|
|
32
|
+
import { initTheme } from './core/theme.js';
|
|
33
|
+
import { initView, viewPrefs, jobShown } from './core/view.js';
|
|
34
|
+
import { JOBS as SHARED_JOBS } from './core/jobs.js';
|
|
27
35
|
/** The four spine bands map onto the territories that answer their question. */
|
|
28
36
|
const LAYER_ROUTE = {
|
|
29
37
|
metered: 'spend',
|
|
@@ -37,15 +45,64 @@ const ROUTE_LAYER = {
|
|
|
37
45
|
allocation: 'allocated',
|
|
38
46
|
value: 'realized',
|
|
39
47
|
};
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The navigation names the jobs people come to do, in the order they meet them.
|
|
50
|
+
* It used to list only Data, Control and System: the four money views were
|
|
51
|
+
* reachable through the spine alone, and Billed, Allocated and Realized had no
|
|
52
|
+
* name a finance reader would look for (critique 2026-10-12).
|
|
53
|
+
*/
|
|
54
|
+
const JOBS = SHARED_JOBS;
|
|
55
|
+
/** How you feed and configure the tool: needed, but not why anyone opens it. */
|
|
56
|
+
const UTILITIES = [
|
|
57
|
+
{ id: 'data', label: 'Data', plain: 'Bring usage in from your tools and providers.' },
|
|
58
|
+
{ id: 'system', label: 'Settings', plain: 'Pricing, settings, maintenance, and what this GUI covers.' },
|
|
45
59
|
];
|
|
46
|
-
const ALL_ROUTES = ['spend', 'evidence', 'allocation', 'value', 'data', 'control', 'system'];
|
|
60
|
+
const ALL_ROUTES = ['statement', 'spend', 'evidence', 'allocation', 'value', 'data', 'control', 'system', 'privacy'];
|
|
47
61
|
const current = signal(readRoute());
|
|
48
|
-
|
|
62
|
+
// The four payloads behind the claims. `undefined` = still on its way; `null` =
|
|
63
|
+
// the read failed, which the derivation already treats as missing evidence.
|
|
64
|
+
const overviewIn = signal(undefined);
|
|
65
|
+
const billingIn = signal(undefined);
|
|
66
|
+
const allocationIn = signal(undefined);
|
|
67
|
+
const valueIn = signal(undefined);
|
|
68
|
+
const chainInputs = computed(() => ({
|
|
69
|
+
overview: overviewIn() ?? null,
|
|
70
|
+
billing: billingIn() ?? null,
|
|
71
|
+
allocation: allocationIn() ?? null,
|
|
72
|
+
value: valueIn() ?? null,
|
|
73
|
+
}));
|
|
74
|
+
const chain = computed(() => buildClaimLayers(chainInputs(), period()));
|
|
75
|
+
const pendingClaims = computed(() => new Set([
|
|
76
|
+
...(overviewIn() === undefined ? ['metered'] : []),
|
|
77
|
+
...(billingIn() === undefined ? ['billed'] : []),
|
|
78
|
+
...(allocationIn() === undefined ? ['allocated'] : []),
|
|
79
|
+
...(valueIn() === undefined ? ['realized'] : []),
|
|
80
|
+
]));
|
|
81
|
+
const sampleData = computed(() => chainIsSampleData(chainInputs()));
|
|
82
|
+
/**
|
|
83
|
+
* The figures on screen were read from a server that has since stopped answering.
|
|
84
|
+
* Set by a failed read or by the timed re-check; cleared only by a fresh page load,
|
|
85
|
+
* because nothing re-reads the payloads in place.
|
|
86
|
+
*/
|
|
87
|
+
const figuresStale = signal(false);
|
|
88
|
+
/** When the last successful read landed, so the age of the figures can be stated. */
|
|
89
|
+
const lastReadAt = signal(null);
|
|
90
|
+
/** Advances on each re-check, so the stated age keeps moving while the page sits. */
|
|
91
|
+
const clock = signal(Date.now());
|
|
92
|
+
const STALE_RECHECK_MS = 15_000;
|
|
93
|
+
const unreachable = (e) => e instanceof ApiError && e.status === 0;
|
|
94
|
+
function ageText(readAt, now) {
|
|
95
|
+
if (readAt === null)
|
|
96
|
+
return 'at an unknown time';
|
|
97
|
+
const minutes = Math.floor((now - readAt) / 60_000);
|
|
98
|
+
if (minutes < 1)
|
|
99
|
+
return 'less than a minute ago';
|
|
100
|
+
return minutes === 1 ? '1 minute ago' : `${minutes} minutes ago`;
|
|
101
|
+
}
|
|
102
|
+
/** Stated on the page, above the figures, so a stale number is never read as current. */
|
|
103
|
+
function staleBanner() {
|
|
104
|
+
return h('div', { class: 'banner banner-stale', role: 'status', 'aria-live': 'polite' }, h('p', null, h('strong', { text: 'Segreant stopped responding. ' }), h('span', { text: () => `The figures below were read ${ageText(lastReadAt(), clock())} and are not current.` })), h('button', { type: 'button', class: 'btn-ghost banner-retry', onclick: () => window.location.reload(), text: 'Retry' }));
|
|
105
|
+
}
|
|
49
106
|
/**
|
|
50
107
|
* Whether the operator has chosen a register at all — NOT which one they chose.
|
|
51
108
|
*
|
|
@@ -63,12 +120,42 @@ function go(route) {
|
|
|
63
120
|
location.hash = `/${route}`;
|
|
64
121
|
current.set(route);
|
|
65
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Focus the current page's h1 once the view has rendered; give up quietly after
|
|
125
|
+
* a second, or as soon as the user has put focus somewhere themselves. The
|
|
126
|
+
* retries used to land late and pull focus out of wherever the user had just
|
|
127
|
+
* moved it: the skip link's target, a field, a button (caught by the browser
|
|
128
|
+
* suite's skip-link diagnostics on CI, 2026-10-17).
|
|
129
|
+
*/
|
|
130
|
+
function focusPageHeading(tries = 20, from = document.activeElement) {
|
|
131
|
+
const now = document.activeElement;
|
|
132
|
+
if (now !== from && now !== null && now !== document.body)
|
|
133
|
+
return;
|
|
134
|
+
const heading = document.querySelector('main h1');
|
|
135
|
+
if (!heading) {
|
|
136
|
+
if (tries > 0)
|
|
137
|
+
setTimeout(() => focusPageHeading(tries - 1, from), 50);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!heading.hasAttribute('tabindex'))
|
|
141
|
+
heading.setAttribute('tabindex', '-1');
|
|
142
|
+
heading.focus();
|
|
143
|
+
}
|
|
66
144
|
function brandMark() {
|
|
67
|
-
return h('svg', { class: 'brand-mark', viewBox:
|
|
145
|
+
return h('svg', { class: 'brand-mark', viewBox: GRIFFIN_VIEWBOX, 'aria-hidden': 'true' }, h('path', { d: GRIFFIN_PATH, transform: GRIFFIN_TRANSFORM, fill: 'var(--brand)', 'fill-rule': 'evenodd' }));
|
|
68
146
|
}
|
|
69
147
|
function firstRun() {
|
|
70
148
|
const previousFocus = captureFocus(document.activeElement);
|
|
71
|
-
const body = h('div', { class: 'firstrun', role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'fr-title', tabindex: '-1' }, h('div', { class: 'firstrun-card' }, h('div', { class: 'firstrun-mark' }, brandMark()),
|
|
149
|
+
const body = h('div', { class: 'firstrun', role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'fr-title', tabindex: '-1' }, h('div', { class: 'firstrun-card' }, h('div', { class: 'firstrun-mark' }, brandMark()),
|
|
150
|
+
// The product has two audiences on one set of screens, and the first run
|
|
151
|
+
// is where it learns which one is here. It used to ask only how figures
|
|
152
|
+
// should be worded, so a finance reader and a developer landed on the
|
|
153
|
+
// same screen with different decimals. The answer now also picks where
|
|
154
|
+
// each starts: the money, or getting usage in.
|
|
155
|
+
h('h1', { id: 'fr-title', text: 'What brings you to Segreant?' }), h('p', { class: 'lede', text: 'Pick the closer one. It decides where you start and how figures are worded. The numbers are the same either way, and you can switch at any time with Plain and Precise at the top right.' }), h('div', { class: 'choice-grid' }, h('button', { class: 'choice', onclick: () => { setRegister('plain'); go('statement'); } }, h('strong', { text: 'I look after the money' }), h('span', { text: 'See what AI cost, set limits, check it against the bill and assign it to budgets. Plain wording; no terminal needed.' }),
|
|
156
|
+
// A wording sample, not a figure from any ledger: it is the same on a
|
|
157
|
+
// fresh install with no data, so it says so (release gate, 2026-10-18).
|
|
158
|
+
h('code', { class: 'choice-eg', text: 'e.g. $59.16 list cost (estimate)' })), h('button', { class: 'choice', onclick: () => { setRegister('precise'); go('data'); } }, h('strong', { text: 'I build with AI tools' }), h('span', { text: 'Bring in usage from Claude Code, Codex and others, then see it by project and model. Exact figures and the matching commands.' }), h('code', { class: 'choice-eg', text: 'e.g. $59.163468 list cost (estimate)' }))), h('p', { class: 'fine', text: 'Runs entirely on this machine. This page loads nothing from the internet: no fonts, no analytics, no third parties.' })));
|
|
72
159
|
const release = trapFocus(body);
|
|
73
160
|
const onKey = (event) => {
|
|
74
161
|
if (event.key === 'Escape') {
|
|
@@ -82,14 +169,35 @@ function firstRun() {
|
|
|
82
169
|
onCleanup(() => {
|
|
83
170
|
release();
|
|
84
171
|
body.removeEventListener('keydown', onKey);
|
|
85
|
-
|
|
172
|
+
// On a fresh page the opener is <body>, and "restoring" to it left a screen
|
|
173
|
+
// reader at the top of the document (NVDA run, 2026-10-15). Land on the
|
|
174
|
+
// chosen page's heading instead.
|
|
175
|
+
if (previousFocus && previousFocus !== document.body)
|
|
176
|
+
restoreFocus(previousFocus);
|
|
177
|
+
else
|
|
178
|
+
focusPageHeading();
|
|
86
179
|
});
|
|
87
180
|
queueMicrotask(() => body.querySelector('.choice')?.focus());
|
|
88
181
|
return body;
|
|
89
182
|
}
|
|
90
183
|
function topbar() {
|
|
91
|
-
return h('header', { class: 'topbar' }, h('a', { class: 'brand', href: '#/spend', onclick: () => go('spend') }, brandMark(), h('span', { class: 'brand-text' }, h('span', { class: 'brand-name', text: 'Segreant' }), h('span', { class: 'brand-tag', text: 'AI Financial Ops' }))),
|
|
184
|
+
return h('header', { class: 'topbar' }, h('a', { class: 'brand', href: '#/spend', onclick: () => go('spend') }, brandMark(), h('span', { class: 'brand-text' }, h('span', { class: 'brand-name', text: 'Segreant' }), h('span', { class: 'brand-tag', text: 'AI Financial Ops' }))), () => (sampleData()
|
|
185
|
+
? h('span', {
|
|
186
|
+
class: 'pill pill-demo topbar-demo',
|
|
187
|
+
role: 'status',
|
|
188
|
+
title: 'Sample data. None of this was measured from your machine.',
|
|
189
|
+
text: 'sample data',
|
|
190
|
+
})
|
|
191
|
+
: null), h('nav', { class: 'ops', 'aria-label': 'Jobs' }, ...JOBS.map((op) => h('button', {
|
|
92
192
|
class: 'op',
|
|
193
|
+
// A job the reader hid stays out of the bar unless it is the one open.
|
|
194
|
+
hidden: () => !jobShown(op.id) && current() !== op.id,
|
|
195
|
+
'aria-current': () => (current() === op.id ? 'page' : false),
|
|
196
|
+
title: op.plain,
|
|
197
|
+
onclick: () => go(op.id),
|
|
198
|
+
text: op.label,
|
|
199
|
+
}))), h('nav', { class: 'ops ops-util', 'aria-label': 'Setup' }, ...UTILITIES.map((op) => h('button', {
|
|
200
|
+
class: 'op op-util',
|
|
93
201
|
'aria-current': () => (current() === op.id ? 'page' : false),
|
|
94
202
|
title: op.plain,
|
|
95
203
|
onclick: () => go(op.id),
|
|
@@ -102,6 +210,7 @@ function topbar() {
|
|
|
102
210
|
}
|
|
103
211
|
function viewFor(route) {
|
|
104
212
|
switch (route) {
|
|
213
|
+
case 'statement': return statementView();
|
|
105
214
|
case 'spend': return spendView();
|
|
106
215
|
case 'evidence': return evidenceView();
|
|
107
216
|
case 'allocation': return allocationView();
|
|
@@ -109,9 +218,12 @@ function viewFor(route) {
|
|
|
109
218
|
case 'control': return controlView();
|
|
110
219
|
case 'data': return dataView();
|
|
111
220
|
case 'system': return systemView();
|
|
221
|
+
case 'privacy': return privacyView();
|
|
112
222
|
}
|
|
113
223
|
}
|
|
114
224
|
function boot() {
|
|
225
|
+
initTheme();
|
|
226
|
+
initView();
|
|
115
227
|
const root = document.getElementById('app');
|
|
116
228
|
if (!root)
|
|
117
229
|
throw new Error('missing #app');
|
|
@@ -130,7 +242,65 @@ function boot() {
|
|
|
130
242
|
// wording register, and re-reading them on a plain/precise click re-issued
|
|
131
243
|
// every endpoint behind the spine — including `/api/value`, which correlates
|
|
132
244
|
// against the repository and is the slowest read this product has.
|
|
133
|
-
|
|
245
|
+
//
|
|
246
|
+
// Each claim draws as soon as its own payload lands. They used to wait for all
|
|
247
|
+
// four, and `/api/value` (which reads git history) took 17-21 s on a real
|
|
248
|
+
// ledger, so the whole header said "Reading the ledger…" for that long.
|
|
249
|
+
// Billing, allocation and value do not depend on the period, so they are read
|
|
250
|
+
// once; the overview is re-read whenever the period changes.
|
|
251
|
+
//
|
|
252
|
+
// Order matters as much as independence: fired together, the value read
|
|
253
|
+
// pushed the overview from 0.5 s alone to 10 s. (Other reads are not queued
|
|
254
|
+
// behind it in general: settings answered in 6-16 ms during a 25 s value
|
|
255
|
+
// read; the overview shares the work the value read is doing.) So the cheap reads go first and the
|
|
256
|
+
// value read starts only once the overview has answered.
|
|
257
|
+
let restStarted = false;
|
|
258
|
+
const readTheRest = () => {
|
|
259
|
+
if (restStarted)
|
|
260
|
+
return;
|
|
261
|
+
restStarted = true;
|
|
262
|
+
void Promise.allSettled([
|
|
263
|
+
api.billing().then((b) => { billingIn.set(b); lastReadAt.set(Date.now()); }, (e) => { billingIn.set(null); if (unreachable(e))
|
|
264
|
+
figuresStale.set(true); }),
|
|
265
|
+
api.allocation().then((a) => { allocationIn.set(a); lastReadAt.set(Date.now()); }, (e) => { allocationIn.set(null); if (unreachable(e))
|
|
266
|
+
figuresStale.set(true); }),
|
|
267
|
+
]).then(() => api.value().then((v) => { valueIn.set(v); lastReadAt.set(Date.now()); }, (e) => { valueIn.set(null); if (unreachable(e))
|
|
268
|
+
figuresStale.set(true); }));
|
|
269
|
+
};
|
|
270
|
+
effect(() => {
|
|
271
|
+
const r = period();
|
|
272
|
+
const controller = new AbortController();
|
|
273
|
+
overviewIn.set(undefined);
|
|
274
|
+
void api.overview(r, controller.signal).then((o) => { if (!controller.signal.aborted) {
|
|
275
|
+
overviewIn.set(o);
|
|
276
|
+
lastReadAt.set(Date.now());
|
|
277
|
+
} }, (e) => { if (!controller.signal.aborted) {
|
|
278
|
+
overviewIn.set(null);
|
|
279
|
+
if (unreachable(e))
|
|
280
|
+
figuresStale.set(true);
|
|
281
|
+
} }).finally(readTheRest);
|
|
282
|
+
onCleanup(() => controller.abort());
|
|
283
|
+
});
|
|
284
|
+
// A screen that changed the ledger says so, and the claim it touched is
|
|
285
|
+
// re-read, so the spine never contradicts the page beneath it.
|
|
286
|
+
window.addEventListener('segreant:ledger-changed', (event) => {
|
|
287
|
+
const claim = event.detail?.claim;
|
|
288
|
+
if (claim === 'allocation') {
|
|
289
|
+
void api.allocation().then((a) => { allocationIn.set(a); lastReadAt.set(Date.now()); }, () => undefined);
|
|
290
|
+
}
|
|
291
|
+
if (claim === 'billing') {
|
|
292
|
+
void api.billing().then((b) => { billingIn.set(b); lastReadAt.set(Date.now()); }, () => undefined);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
// A stopped server is noticed without a click. While any figure is on screen the
|
|
296
|
+
// shell re-checks the server; a dead one marks what is shown as not current.
|
|
297
|
+
setInterval(() => {
|
|
298
|
+
clock.set(Date.now());
|
|
299
|
+
if (lastReadAt() === null || figuresStale())
|
|
300
|
+
return;
|
|
301
|
+
void api.health().then(() => undefined, (e) => { if (unreachable(e))
|
|
302
|
+
figuresStale.set(true); });
|
|
303
|
+
}, STALE_RECHECK_MS);
|
|
134
304
|
effect(() => {
|
|
135
305
|
// `registerChosen()`, never `register()`. Reading the register itself made
|
|
136
306
|
// this effect — which renders the ENTIRE application — a subscriber to the
|
|
@@ -164,17 +334,29 @@ function boot() {
|
|
|
164
334
|
event.preventDefault();
|
|
165
335
|
document.getElementById('main')?.focus();
|
|
166
336
|
},
|
|
167
|
-
}), topbar(),
|
|
337
|
+
}), topbar(), () => (figuresStale() && lastReadAt() !== null ? staleBanner() : null),
|
|
338
|
+
// The banner alone was easy to miss: the figures under it looked exactly
|
|
339
|
+
// as current as before. While stale, the sheet is dimmed as well.
|
|
340
|
+
h('div', { class: () => (figuresStale() && lastReadAt() !== null ? 'sheet is-stale' : 'sheet') }, () => {
|
|
341
|
+
// The four claims head the four money views only. On Protect, Data
|
|
342
|
+
// and Settings they pushed the screen's own content below the fold
|
|
343
|
+
// and asserted nothing about it.
|
|
344
|
+
if (!ROUTE_LAYER[current()])
|
|
345
|
+
return null;
|
|
346
|
+
// The reader can fold the strip away in Settings; every figure is still on its own page.
|
|
347
|
+
if (!viewPrefs().strip)
|
|
348
|
+
return null;
|
|
168
349
|
const layers = chain();
|
|
169
350
|
if (!layers)
|
|
170
351
|
return h('div', { class: 'spine spine-loading' }, h('p', { class: 'spine-read', text: 'Reading the ledger…' }));
|
|
171
352
|
return spine({
|
|
172
353
|
layers,
|
|
354
|
+
pending: pendingClaims(),
|
|
173
355
|
active: ROUTE_LAYER[current()] ?? null,
|
|
174
356
|
onSelect: (id) => go(LAYER_ROUTE[id]),
|
|
175
357
|
onInspect: openClaimInspector,
|
|
176
358
|
});
|
|
177
|
-
}, h('main', { class: 'main', id: 'main', tabindex: '-1' }, () => viewFor(current()))), h('footer', { class: 'shellfoot' }, h('span', { text: '
|
|
359
|
+
}, h('main', { class: 'main', id: 'main', tabindex: '-1' }, () => viewFor(current()))), h('footer', { class: 'shellfoot' }, h('span', { text: 'Runs on this machine. Outbound traffic goes only to the configured provider and the other routes you set up.' }), h('span', { class: 'shellfoot-links' }, h('a', { href: '#/privacy', text: 'Privacy' }), h('a', { href: '/classic', text: 'Classic dashboard' }))));
|
|
178
360
|
});
|
|
179
361
|
}
|
|
180
362
|
boot();
|