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
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Workbench: the Spend page's day-to-day view of where the metered money
|
|
3
|
+
* went, session by session.
|
|
4
|
+
*
|
|
5
|
+
* Every figure here is metered list cost, the same claim as the Metered band
|
|
6
|
+
* above. Outcomes come from the last value read and are filed by the
|
|
7
|
+
* Statement's rule; a session reaches a commit only through its own log.
|
|
8
|
+
* Where the evidence stops (traffic with no session id, a commit never
|
|
9
|
+
* measured, usage a provider never reported) the view says so beside the
|
|
10
|
+
* figure instead of filling it in.
|
|
11
|
+
*/
|
|
12
|
+
import { h } from '../core/dom.js';
|
|
13
|
+
import { signal, scopedEffect, onCleanup } from '../core/signal.js';
|
|
14
|
+
import { api } from '../core/api.js';
|
|
15
|
+
import { period } from '../core/period.js';
|
|
16
|
+
import { usd, count, share, isPrecise } from '../core/fmt.js';
|
|
17
|
+
/** Rows shown before "Show all". */
|
|
18
|
+
const FIRST_ROWS = 12;
|
|
19
|
+
const OUTCOME_WORDS = {
|
|
20
|
+
kept: { label: 'Kept', plain: 'passed every check you track', cls: 'oc-kept' },
|
|
21
|
+
notKept: { label: 'Not kept', plain: 'a check failed, or a revert names it', cls: 'oc-notkept' },
|
|
22
|
+
contested: { label: 'Results disagree', plain: 'one check both passed and failed', cls: 'oc-contested' },
|
|
23
|
+
unknown: { label: 'No signal', plain: 'old enough, but survival was not measured', cls: 'oc-unknown' },
|
|
24
|
+
maturing: { label: 'Too early to tell', plain: 'the survival check has not come round yet', cls: 'oc-maturing' },
|
|
25
|
+
unmeasured: { label: 'Not matched', plain: 'no measured commit, or more than one, matches its sha', cls: 'oc-unmeasured' },
|
|
26
|
+
noCommit: { label: 'No commit linked', plain: 'the session log records no commit', cls: 'oc-none' },
|
|
27
|
+
};
|
|
28
|
+
/** The table's narrow column takes the short word; the full one is in the title and the detail panel. */
|
|
29
|
+
const SHORT_WORDS = {
|
|
30
|
+
kept: 'Kept', notKept: 'Not kept', contested: 'Disputed', unknown: 'No signal', maturing: 'Too early', unmeasured: 'Not matched',
|
|
31
|
+
};
|
|
32
|
+
const OUTCOME_ORDER = ['kept', 'notKept', 'contested', 'unknown', 'maturing', 'unmeasured', 'noCommit'];
|
|
33
|
+
export function workbench() {
|
|
34
|
+
const data = signal(null);
|
|
35
|
+
const error = signal(null);
|
|
36
|
+
const selected = signal(null);
|
|
37
|
+
const focusBucket = signal(null);
|
|
38
|
+
const showAll = signal(false);
|
|
39
|
+
// Choose a session and make sure its row is on the page: a bar can point at
|
|
40
|
+
// a session past the first rows, and the costliest session need not be in
|
|
41
|
+
// them either. Without this the detail opened with no row selected and
|
|
42
|
+
// nothing to scroll to.
|
|
43
|
+
const reveal = (sessions, id) => {
|
|
44
|
+
if (id !== null && sessions.findIndex((s) => s.sessionId === id) >= FIRST_ROWS)
|
|
45
|
+
showAll.set(true);
|
|
46
|
+
selected.set(id);
|
|
47
|
+
};
|
|
48
|
+
scopedEffect(() => {
|
|
49
|
+
const r = period();
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
let current = true;
|
|
52
|
+
error.set(null);
|
|
53
|
+
data.set(null);
|
|
54
|
+
showAll.set(false);
|
|
55
|
+
focusBucket.set(null);
|
|
56
|
+
void api.sessions(r, controller.signal)
|
|
57
|
+
.then((payload) => {
|
|
58
|
+
if (!current || controller.signal.aborted)
|
|
59
|
+
return;
|
|
60
|
+
data.set(payload);
|
|
61
|
+
// Open on the session that cost the most: the first question on this page.
|
|
62
|
+
const top = [...payload.sessions].sort((a, b) => b.costUsd - a.costUsd)[0];
|
|
63
|
+
reveal(payload.sessions, top ? top.sessionId : null);
|
|
64
|
+
})
|
|
65
|
+
.catch((e) => {
|
|
66
|
+
if (!current || controller.signal.aborted)
|
|
67
|
+
return;
|
|
68
|
+
error.set(e instanceof Error ? e.message : String(e));
|
|
69
|
+
});
|
|
70
|
+
onCleanup(() => { current = false; controller.abort(); });
|
|
71
|
+
});
|
|
72
|
+
return h('section', { class: 'wb', 'aria-label': 'Workbench' }, () => {
|
|
73
|
+
const err = error();
|
|
74
|
+
if (err)
|
|
75
|
+
return h('div', { class: 'card' }, h('p', { class: 'drawer-error', role: 'alert', text: `The session read failed: ${err}` }));
|
|
76
|
+
const d = data();
|
|
77
|
+
if (!d)
|
|
78
|
+
return h('div', { class: 'card' }, h('p', { class: 'drawer-muted', role: 'status', 'aria-busy': 'true', text: 'Reading sessions…' }));
|
|
79
|
+
if (d.totals.requests === 0)
|
|
80
|
+
return h('div', { class: 'card' }, h('p', { class: 'drawer-muted', role: 'status', text: 'Nothing was metered in this range.' }));
|
|
81
|
+
const colour = modelColours(d.models);
|
|
82
|
+
return h('div', { class: 'wb-body' }, tiles(d), h('div', { class: 'wb-pair' }, chart(d, colour, focusBucket, (id) => reveal(d.sessions, id)), outcomes(d)), h('div', { class: 'wb-pair wb-pair-sessions' }, sessionTable(d, colour, selected, showAll), () => {
|
|
83
|
+
const s = d.sessions.find((x) => x.sessionId === selected());
|
|
84
|
+
return s ? detail(s, d) : h('aside', { class: 'card wb-detail' }, h('p', { class: 'drawer-muted', text: 'Choose a session to see inside it.' }));
|
|
85
|
+
}));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/* ---------- Figures across the top ------------------------------------- */
|
|
89
|
+
function tiles(d) {
|
|
90
|
+
const t = d.totals;
|
|
91
|
+
const sent = t.inputTokens + t.cacheReadTokens + t.cacheWriteTokens;
|
|
92
|
+
const cacheShare = sent > 0 ? t.cacheReadTokens / sent : null;
|
|
93
|
+
const tile = (label, value, sub, cls = '') => h('div', { class: `wb-tile ${cls}` }, h('span', { class: 'wb-tile-label', text: label }), h('span', { class: 'wb-tile-value', text: value }), h('span', { class: 'wb-tile-sub', text: sub }));
|
|
94
|
+
return h('div', { class: 'wb-tiles', role: 'group', 'aria-label': 'This range at a glance' }, tile('List cost', usd(t.costUsd), 'metered, not billed'), tile('Requests', count(t.requests), `${count(t.sessions)} session${t.sessions === 1 ? '' : 's'} · ${count(t.tools)} tool${t.tools === 1 ? '' : 's'}`), tile('Context sent', tokens(sent), `${tokens(t.outputTokens)} written back`), tile('Read from cache', cacheShare === null ? '—' : share(cacheShare), 'of the context sent'), tile('Refused or failed', count(t.refusedReplies + t.failedReplies), `${count(t.refusedReplies)} refused (429) · ${count(t.failedReplies)} failed (5xx)`, t.refusedReplies + t.failedReplies > 0 ? 'wb-tile-caution' : ''), tile('Outside any session', usd(t.unsessionedCostUsd), `${count(t.unsessionedRequests)} requests carry no session id`));
|
|
95
|
+
}
|
|
96
|
+
/* ---------- Cost over time, by model ----------------------------------- */
|
|
97
|
+
/** A named model's series colour; any model outside the five takes the "other" colour. */
|
|
98
|
+
function modelColours(models) {
|
|
99
|
+
return (model) => {
|
|
100
|
+
const i = models.indexOf(model);
|
|
101
|
+
return i < 0 ? 'var(--series-other)' : `var(--series-${i + 1})`;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const OTHER_COLOUR = 'var(--series-other)';
|
|
105
|
+
/** A round axis top and its steps, so every gridline names a value the scale really reaches. */
|
|
106
|
+
export function niceScale(max) {
|
|
107
|
+
if (!(max > 0))
|
|
108
|
+
return { top: 1, ticks: [0, 1] };
|
|
109
|
+
// Every round step (1, 2, 2.5, 5 times a power of ten) that gives three to
|
|
110
|
+
// six intervals; the lowest top wins, so a $40.20 peak gets a $50 axis, not
|
|
111
|
+
// the $60 a fixed four-step rule gave it (half the plot left empty).
|
|
112
|
+
const mag = 10 ** Math.floor(Math.log10(max));
|
|
113
|
+
let best = null;
|
|
114
|
+
for (const m of [0.1, 0.2, 0.25, 0.5, 1, 2, 2.5, 5]) {
|
|
115
|
+
const step = m * mag;
|
|
116
|
+
const top = Math.ceil(max / step - 1e-9) * step;
|
|
117
|
+
const intervals = Math.round(top / step);
|
|
118
|
+
if (intervals < 3 || intervals > 6)
|
|
119
|
+
continue;
|
|
120
|
+
if (!best || top < best.top - 1e-9 || (Math.abs(top - best.top) < 1e-9 && step > best.step))
|
|
121
|
+
best = { top, step };
|
|
122
|
+
}
|
|
123
|
+
const { top, step } = best ?? { top: Math.ceil(max / mag) * mag, step: mag };
|
|
124
|
+
const ticks = [];
|
|
125
|
+
for (let v = 0; v <= top + step / 2; v += step)
|
|
126
|
+
ticks.push(Number(v.toFixed(10)));
|
|
127
|
+
return { top: Number(top.toFixed(10)), ticks };
|
|
128
|
+
}
|
|
129
|
+
function bucketLabel(ms, bucketMs) {
|
|
130
|
+
const d = new Date(ms);
|
|
131
|
+
if (bucketMs <= 60 * 60 * 1000) {
|
|
132
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
133
|
+
return { short: hh, long: `${hh}:00 to ${String((d.getHours() + 1) % 24).padStart(2, '0')}:00` };
|
|
134
|
+
}
|
|
135
|
+
const day = d.toLocaleDateString(undefined, { weekday: 'short', day: 'numeric', month: 'short' });
|
|
136
|
+
return bucketMs > 24 * 60 * 60 * 1000
|
|
137
|
+
? { short: d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' }), long: `Week from ${day}` }
|
|
138
|
+
: { short: String(d.getDate()), long: day };
|
|
139
|
+
}
|
|
140
|
+
function chart(d, colour, focusBucket, select) {
|
|
141
|
+
const max = d.buckets.reduce((m, b) => Math.max(m, b.costUsd), 0);
|
|
142
|
+
const { top, ticks } = niceScale(max);
|
|
143
|
+
const every = d.buckets.length > 16 ? Math.ceil(d.buckets.length / 10) : 1;
|
|
144
|
+
const unit = d.window.bucketMs <= 60 * 60 * 1000 ? 'hour' : d.window.bucketMs > 24 * 60 * 60 * 1000 ? 'week' : 'day';
|
|
145
|
+
const modelTotal = (m) => d.buckets.reduce((s, b) => s + (b.byModel.find((x) => x.model === m)?.costUsd ?? 0), 0);
|
|
146
|
+
const otherTotal = d.buckets.reduce((s, b) => s + b.otherUsd, 0);
|
|
147
|
+
const legend = [
|
|
148
|
+
...d.models.map((m) => ({ name: m, colour: colour(m), total: modelTotal(m) })),
|
|
149
|
+
...(otherTotal > 0 ? [{ name: 'other models', colour: OTHER_COLOUR, total: otherTotal }] : []),
|
|
150
|
+
];
|
|
151
|
+
// The most expensive session that ran in a bucket: what clicking the bar opens.
|
|
152
|
+
const sessionIn = (i) => {
|
|
153
|
+
const start = d.buckets[i].startMs;
|
|
154
|
+
const end = d.buckets[i + 1]?.startMs ?? d.window.endMs;
|
|
155
|
+
return d.sessions.filter((s) => s.startMs < end && s.endMs >= start).sort((a, b) => b.costUsd - a.costUsd)[0];
|
|
156
|
+
};
|
|
157
|
+
const readout = () => {
|
|
158
|
+
const i = focusBucket();
|
|
159
|
+
if (i === null)
|
|
160
|
+
return `${usd(d.totals.costUsd)} over the range. Point at a ${unit} for its split; choose one to open its costliest session.`;
|
|
161
|
+
const b = d.buckets[i];
|
|
162
|
+
const parts = [...b.byModel.map((m) => `${m.model} ${usd(m.costUsd)}`), ...(b.otherUsd > 0 ? [`other models ${usd(b.otherUsd)}`] : [])].join(' · ');
|
|
163
|
+
return `${bucketLabel(b.startMs, d.window.bucketMs).long}: ${usd(b.costUsd)}, ${count(b.requests)} requests${parts ? ` (${parts})` : ''}`;
|
|
164
|
+
};
|
|
165
|
+
return h('div', { class: 'card wb-chart' }, h('div', { class: 'card-head' }, h('h2', { class: 'card-title', text: `List cost by ${unit}, by model` })), h('div', { class: 'wb-legend' }, ...legend.map((m) => h('span', { class: 'wb-legend-item' }, h('span', { class: 'wb-swatch', style: `background:${m.colour}`, 'aria-hidden': 'true' }), h('span', { class: 'wb-legend-name', text: m.name }), h('span', { class: 'wb-legend-value num', text: usd(m.total) })))), h('p', { class: 'wb-readout', role: 'status', text: readout }), h('div', { class: 'wb-plot' }, h('div', { class: 'wb-yaxis', 'aria-hidden': 'true' }, ...ticks.map((v) => h('span', { class: 'wb-ytick', style: `bottom:${(v / top) * 100}%`, text: usd(v) }))), h('div', { class: 'wb-bars', style: `--n:${d.buckets.length}` }, ...ticks.map((v) => h('span', { class: 'wb-grid', style: `bottom:${(v / top) * 100}%`, 'aria-hidden': 'true' })), ...d.buckets.map((b, i) => {
|
|
166
|
+
const label = bucketLabel(b.startMs, d.window.bucketMs);
|
|
167
|
+
const target = sessionIn(i);
|
|
168
|
+
return h('button', {
|
|
169
|
+
class: () => `wb-bar${focusBucket() === i ? ' is-focus' : ''}`,
|
|
170
|
+
'aria-label': `${label.long}: ${usd(b.costUsd)}${target ? '. Opens its costliest session' : ''}`,
|
|
171
|
+
disabled: b.costUsd <= 0 && !target ? true : undefined,
|
|
172
|
+
onmouseenter: () => focusBucket.set(i),
|
|
173
|
+
onmouseleave: () => focusBucket.set(null),
|
|
174
|
+
onfocus: () => focusBucket.set(i),
|
|
175
|
+
onblur: () => focusBucket.set(null),
|
|
176
|
+
onclick: () => { if (target) {
|
|
177
|
+
select(target.sessionId);
|
|
178
|
+
document.getElementById(rowId(target.sessionId))?.scrollIntoView({ block: 'nearest' });
|
|
179
|
+
} },
|
|
180
|
+
}, h('span', { class: 'wb-stack', style: `height:${top > 0 ? (b.costUsd / top) * 100 : 0}%` },
|
|
181
|
+
// Top to bottom: other models, then the named ones from cheapest to costliest.
|
|
182
|
+
...[
|
|
183
|
+
...(b.otherUsd > 0 ? [{ grow: b.otherUsd, fill: OTHER_COLOUR }] : []),
|
|
184
|
+
...[...b.byModel].reverse().map((m) => ({ grow: m.costUsd, fill: colour(m.model) })),
|
|
185
|
+
].map((seg) => h('span', { class: 'wb-seg', style: `flex-grow:${seg.grow};background:${seg.fill}` }))), h('span', { class: 'wb-xtick', 'aria-hidden': 'true', text: i % every === 0 ? label.short : '' }));
|
|
186
|
+
}))), d.totals.unsessionedRequests > 0
|
|
187
|
+
? h('p', { class: 'basis', text: `The bars include all ${count(d.totals.requests)} requests. ${count(d.totals.unsessionedRequests)} of them (${usd(d.totals.unsessionedCostUsd)}) carry no session id, so they appear in no session below.` })
|
|
188
|
+
: h('p', { class: 'basis', text: 'Local list prices from the rate card. Billed is where this is checked against the invoice.' }));
|
|
189
|
+
}
|
|
190
|
+
/* ---------- What the session spend came to ----------------------------- */
|
|
191
|
+
function outcomes(d) {
|
|
192
|
+
const o = d.outcomes;
|
|
193
|
+
const parts = OUTCOME_ORDER.map((k) => ({
|
|
194
|
+
k,
|
|
195
|
+
cost: k === 'noCommit' ? o.noCommit.costUsd : o[k].costUsd,
|
|
196
|
+
n: k === 'noCommit' ? o.noCommit.sessions : o[k].commits,
|
|
197
|
+
}));
|
|
198
|
+
const total = parts.reduce((s, p) => s + p.cost, 0);
|
|
199
|
+
return h('div', { class: 'card wb-outcomes' }, h('div', { class: 'card-head' }, h('h2', { class: 'card-title', text: 'What the session spend came to' })), total > 0
|
|
200
|
+
? h('div', { class: 'wb-split', 'aria-hidden': 'true' }, ...parts.filter((p) => p.cost > 0).map((p) => h('span', { class: `wb-split-seg ${OUTCOME_WORDS[p.k].cls}`, style: `flex-grow:${p.cost}` })))
|
|
201
|
+
: null, h('div', { class: 'wb-oc-rows' }, ...parts.filter((p) => p.cost > 0 || p.k === 'kept' || p.k === 'notKept').map((p) => h('div', { class: 'wb-oc-row' }, h('span', { class: `wb-oc-key ${OUTCOME_WORDS[p.k].cls}` }, h('span', { class: 'wb-oc-dot', 'aria-hidden': 'true' }), h('span', { text: OUTCOME_WORDS[p.k].label })), h('span', { class: 'wb-oc-n num', text: p.k === 'noCommit' ? `${count(p.n)} session${p.n === 1 ? '' : 's'}` : `${count(p.n)} commit${p.n === 1 ? '' : 's'}` }), h('span', { class: 'wb-oc-cost num', text: usd(p.cost) }), h('span', { class: 'wb-oc-share num', text: total > 0 ? share(p.cost / total) : '—' })))), h('p', { class: 'basis', text: () => (isPrecise() ? o.basis : `Only sessions are counted here. A session's cost is split evenly across the commits its own log recorded${o.measuredAt === null ? '; no value read has run yet' : ''}. This is cost, not value.`) }));
|
|
202
|
+
}
|
|
203
|
+
/* ---------- Sessions ---------------------------------------------------- */
|
|
204
|
+
function rowId(sessionId) {
|
|
205
|
+
return `wb-row-${sessionId.replace(/[^A-Za-z0-9_-]/g, '_')}`;
|
|
206
|
+
}
|
|
207
|
+
function tokens(n) {
|
|
208
|
+
if (n >= 1_000_000)
|
|
209
|
+
return `${(n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 1)}M`;
|
|
210
|
+
if (n >= 1_000)
|
|
211
|
+
return `${Math.round(n / 1_000)}k`;
|
|
212
|
+
return String(n);
|
|
213
|
+
}
|
|
214
|
+
function when(ms, bucketMs) {
|
|
215
|
+
const d = new Date(ms);
|
|
216
|
+
const time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
217
|
+
return bucketMs <= 60 * 60 * 1000 ? time : `${d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })} ${time}`;
|
|
218
|
+
}
|
|
219
|
+
function duration(ms) {
|
|
220
|
+
const m = Math.round(ms / 60_000);
|
|
221
|
+
if (m < 1)
|
|
222
|
+
return 'under a minute';
|
|
223
|
+
if (m < 60)
|
|
224
|
+
return `${m} min`;
|
|
225
|
+
return `${Math.floor(m / 60)} h ${String(m % 60).padStart(2, '0')} min`;
|
|
226
|
+
}
|
|
227
|
+
function sessionTable(d, colour, selected, showAll) {
|
|
228
|
+
// One scale for every row's context, so a long session reads as long beside a short one.
|
|
229
|
+
const scaleMax = d.sessions.reduce((m, s) => Math.max(m, s.contextPeakTokens), 0);
|
|
230
|
+
const at = (v) => (scaleMax > 0 ? (v / scaleMax) * 100 : 0);
|
|
231
|
+
const row = (s) => {
|
|
232
|
+
const lo = Math.min(s.contextStartTokens, s.contextEndTokens);
|
|
233
|
+
const hi = Math.max(s.contextStartTokens, s.contextEndTokens);
|
|
234
|
+
const measuredContext = s.profile.length > 0;
|
|
235
|
+
return h('tr', {
|
|
236
|
+
id: rowId(s.sessionId),
|
|
237
|
+
class: () => `wb-row${selected() === s.sessionId ? ' is-selected' : ''}`,
|
|
238
|
+
'aria-selected': () => (selected() === s.sessionId ? 'true' : 'false'),
|
|
239
|
+
onclick: () => selected.set(s.sessionId),
|
|
240
|
+
}, h('td', { class: 'wb-when num' }, h('button', {
|
|
241
|
+
class: 'link-btn wb-open',
|
|
242
|
+
'aria-label': `Open the session that started ${when(s.startMs, d.window.bucketMs)}`,
|
|
243
|
+
onclick: (e) => { e.stopPropagation(); selected.set(s.sessionId); },
|
|
244
|
+
}, h('span', { class: 'wb-when-day', text: new Date(s.startMs).toLocaleDateString(undefined, { day: 'numeric', month: 'short' }) }), h('span', { class: 'wb-when-time', text: new Date(s.startMs).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }) }))), h('td', { class: 'wb-who' }, h('span', { class: 'wb-tool', text: s.source ?? 'direct' }), h('span', { class: 'wb-model' }, h('span', { class: 'wb-swatch', style: `background:${colour(s.model)}`, 'aria-hidden': 'true' }), h('span', { text: s.models > 1 ? `${s.model} +${s.models - 1}` : s.model }))), h('td', { class: 'num' }, h('span', { class: 'wb-req', text: count(s.requests) }), s.refusedReplies + s.failedReplies > 0
|
|
245
|
+
? h('span', { class: 'wb-req-note', text: [s.refusedReplies > 0 ? `${count(s.refusedReplies)} refused` : '', s.failedReplies > 0 ? `${count(s.failedReplies)} failed` : ''].filter(Boolean).join(', ') })
|
|
246
|
+
: null), h('td', { class: 'wb-ctx' }, measuredContext
|
|
247
|
+
? h('span', { class: 'wb-dumbbell', 'aria-hidden': 'true' }, h('span', { class: 'wb-db-track' }), s.contextPeakTokens > hi ? h('span', { class: 'wb-db-peak', style: `left:${at(s.contextPeakTokens)}%` }) : null, h('span', { class: `wb-db-span${s.contextEndTokens < s.contextStartTokens ? ' is-down' : ''}`, style: `left:${at(lo)}%;width:${Math.max(0, at(hi) - at(lo))}%` }), h('span', { class: 'wb-db-start', style: `left:${at(s.contextStartTokens)}%` }), h('span', { class: 'wb-db-end', style: `left:${at(s.contextEndTokens)}%` }))
|
|
248
|
+
: null, h('span', { class: 'wb-ctx-text num', text: measuredContext ? `${tokens(s.contextStartTokens)} → ${tokens(s.contextEndTokens)}` : 'not reported' })), h('td', { class: 'num wb-cost', text: usd(s.costUsd) }), h('td', { class: 'wb-commits' }, s.commits.length === 0
|
|
249
|
+
? h('span', { class: 'wb-none', text: 'no commit' })
|
|
250
|
+
: h('span', { class: 'wb-chips' }, ...s.commits.map((c) => h('span', {
|
|
251
|
+
class: `wb-chip ${OUTCOME_WORDS[c.outcome].cls}`,
|
|
252
|
+
title: `${c.sha.slice(0, 12)} ${c.subject}: ${OUTCOME_WORDS[c.outcome].label}`,
|
|
253
|
+
text: SHORT_WORDS[c.outcome],
|
|
254
|
+
})))));
|
|
255
|
+
};
|
|
256
|
+
return h('div', { class: 'card wb-sessions' }, h('div', { class: 'card-head' }, h('h2', { class: 'card-title', text: 'Sessions, newest first' }), h('span', { class: 'wb-head-note', text: `${count(d.totals.sessions)} in this range` })), h('div', { class: 'table-wrap' }, h('table', { class: 'wb-table' }, h('thead', null, h('tr', null, h('th', { text: 'Started' }), h('th', { text: 'Tool · model' }), h('th', { class: 'num', text: 'Requests' }), h('th', { text: 'Context, first → last' }), h('th', { class: 'num', text: 'List cost' }), h('th', { text: 'Outcome' }))), h('tbody', null, () => (showAll() ? d.sessions : d.sessions.slice(0, FIRST_ROWS)).map(row)))), () => (d.sessions.length > FIRST_ROWS && !showAll()
|
|
257
|
+
? h('button', { class: 'link-btn wb-more', onclick: () => showAll.set(true), text: `Show all ${count(d.sessions.length)} sessions` })
|
|
258
|
+
: null), d.sessionsOmitted > 0
|
|
259
|
+
? h('p', { class: 'basis', text: `The ${count(d.sessionLimit)} newest are listed; ${count(d.sessionsOmitted)} older sessions are in the totals but not the list.` })
|
|
260
|
+
: null, h('p', { class: 'basis', text: 'Context is what each request sent: new input plus cached context read and written. The line runs from the first request to the last, on one scale for every row; a tick marks the peak when it came between.' }));
|
|
261
|
+
}
|
|
262
|
+
/* ---------- Inside one session ------------------------------------------ */
|
|
263
|
+
function detail(s, d) {
|
|
264
|
+
const sent = s.inputTokens + s.cacheReadTokens + s.cacheWriteTokens;
|
|
265
|
+
const peak = s.profile.reduce((m, v) => Math.max(m, v), 0);
|
|
266
|
+
const marksAt = new Map();
|
|
267
|
+
for (const m of s.marks)
|
|
268
|
+
marksAt.set(m.at, marksAt.has(m.at) && marksAt.get(m.at) !== m.kind ? 'both' : m.kind);
|
|
269
|
+
const fact = (k, v) => h('div', { class: 'wb-fact' }, h('dt', { text: k }), h('dd', { class: 'num', text: v }));
|
|
270
|
+
return h('aside', { class: 'card wb-detail', 'aria-label': 'Inside this session' }, h('div', { class: 'card-head' }, h('h2', { class: 'card-title', text: 'Inside this session' }), h('span', { class: 'wb-head-note num', text: usd(s.costUsd) })), h('p', { class: 'wb-detail-who' }, h('strong', { text: s.source ?? 'Direct traffic' }), ` on ${s.project}, ${when(s.startMs, 0)} for ${duration(s.endMs - s.startMs)}.`), s.profile.length > 0
|
|
271
|
+
? h('div', { class: 'wb-profile-wrap' }, h('div', { class: 'wb-profile', role: 'img', 'aria-label': `Context per request, rising from ${tokens(s.contextStartTokens)} to ${tokens(s.contextEndTokens)}, peak ${tokens(peak)}` }, ...s.profile.map((v, i) => h('span', {
|
|
272
|
+
class: `wb-pbar${marksAt.has(i) ? ` has-mark mark-${marksAt.get(i)}` : ''}`,
|
|
273
|
+
style: `height:${peak > 0 ? Math.max(2, (v / peak) * 100) : 0}%`,
|
|
274
|
+
title: `${tokens(v)} tokens of context`,
|
|
275
|
+
}))), h('div', { class: 'wb-profile-axis' }, h('span', { text: `first: ${tokens(s.contextStartTokens)}` }), h('span', { text: `peak: ${tokens(peak)}` }), h('span', { text: `last: ${tokens(s.contextEndTokens)}` })), s.marks.length > 0
|
|
276
|
+
? h('p', { class: 'wb-profile-key' }, h('span', { class: 'wb-mark-key mark-refused', 'aria-hidden': 'true' }), ' refused (429) ', h('span', { class: 'wb-mark-key mark-failed', 'aria-hidden': 'true' }), ' failed (5xx), at the request before it')
|
|
277
|
+
: null)
|
|
278
|
+
: h('p', { class: 'drawer-muted', text: 'The tool reported no usage for this session, so its context is not known.' }), h('dl', { class: 'wb-facts' }, fact('Requests', count(s.requests)), fact('Context sent', tokens(sent)), fact('Read from cache', sent > 0 ? share(s.cacheReadTokens / sent) : '—'), fact('Written back', tokens(s.outputTokens)), fact('Refused or failed', `${count(s.refusedReplies)} · ${count(s.failedReplies)}`), fact('Models', s.models > 1 ? `${count(s.models)}, mostly ${s.model}` : s.model)), h('h3', { class: 'wb-sub', text: 'Commits this session made' }), s.commits.length === 0
|
|
279
|
+
? h('p', { class: 'drawer-muted', text: 'None linked. A session is linked to a commit only when its own log records making it.' })
|
|
280
|
+
: h('ul', { class: 'wb-commit-list' }, ...s.commits.map((c) => h('li', { class: 'wb-commit' }, h('span', { class: 'wb-sha', text: c.sha.slice(0, 10) }), h('span', { class: 'wb-subject', text: c.subject }), h('span', { class: `wb-chip ${OUTCOME_WORDS[c.outcome].cls}`, text: OUTCOME_WORDS[c.outcome].label }), c.checkMs !== null
|
|
281
|
+
? h('span', { class: 'wb-check', text: `checked ${new Date(c.checkMs).toLocaleDateString(undefined, { day: 'numeric', month: 'short' })}` })
|
|
282
|
+
: null))), h('h3', { class: 'wb-sub', text: 'Worth a look next time' }), h('ul', { class: 'wb-hints' }, ...hints(s, d).map((t) => h('li', { text: t }))));
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Plain observations drawn only from this session's own figures. Each names the
|
|
286
|
+
* fact it rests on; none promises a saving the ledger cannot show.
|
|
287
|
+
*/
|
|
288
|
+
export function hints(s, d) {
|
|
289
|
+
const out = [];
|
|
290
|
+
const sent = s.inputTokens + s.cacheReadTokens + s.cacheWriteTokens;
|
|
291
|
+
if (s.profile.length > 1 && s.contextEndTokens >= 2 * Math.max(1, s.contextStartTokens) && s.contextEndTokens >= 40_000) {
|
|
292
|
+
out.push(`Context grew from ${tokens(s.contextStartTokens)} to ${tokens(s.contextEndTokens)} a request. Every later request paid to resend it; a fresh session for the next task starts small again.`);
|
|
293
|
+
}
|
|
294
|
+
if (sent > 0 && s.requests >= 4 && s.cacheReadTokens / sent < 0.3) {
|
|
295
|
+
out.push(`Only ${share(s.cacheReadTokens / sent)} of the context came from cache. Cached reads are priced well below new input, and a stable opening prompt is what lets a tool reuse them.`);
|
|
296
|
+
}
|
|
297
|
+
if (s.refusedReplies > 0)
|
|
298
|
+
out.push(`${count(s.refusedReplies)} request${s.refusedReplies === 1 ? ' was' : 's were'} refused with a 429. If a Segreant cap refused ${s.refusedReplies === 1 ? 'it' : 'them'}, Protect shows which one.`);
|
|
299
|
+
if (s.failedReplies > 0)
|
|
300
|
+
out.push(`${count(s.failedReplies)} request${s.failedReplies === 1 ? '' : 's'} failed (5xx). A failed request that is retried is paid for twice.`);
|
|
301
|
+
if (s.usageUnknownRequests > 0)
|
|
302
|
+
out.push(`${count(s.usageUnknownRequests)} request${s.usageUnknownRequests === 1 ? '' : 's'} never reported usage, so this session's cost is a floor.`);
|
|
303
|
+
if (s.commits.length === 0)
|
|
304
|
+
out.push('No commit is linked. If this session produced work, its tool did not log the commit, so the outcome stays unknown rather than guessed.');
|
|
305
|
+
if (s.commits.some((c) => c.outcome === 'notKept'))
|
|
306
|
+
out.push('A commit from this session was not kept. Its cost is on the Value page under work that did not last.');
|
|
307
|
+
if (s.commits.some((c) => c.outcome === 'unmeasured') && d.outcomes.measuredAt !== null)
|
|
308
|
+
out.push('A commit here matches no single measured commit: it is newer than the last value read, or its short sha fits more than one. A value read from the Value page files a new one.');
|
|
309
|
+
if (out.length === 0)
|
|
310
|
+
out.push('Nothing stands out: context stayed level, the cache did its work, and nothing was refused.');
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>Segreant — classic dashboard</title>
|
|
7
|
-
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+
|
|
7
|
+
<link rel="icon" type="image/png" href="data:image/png;base64,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" />
|
|
8
8
|
<!-- No web fonts. The dashboard makes ZERO external browser requests; this is
|
|
9
9
|
a browser-app property, not a claim that proxy-routed provider traffic is
|
|
10
10
|
offline.
|
|
@@ -1052,8 +1052,11 @@ $('#viewnav').addEventListener('click', (e) => {
|
|
|
1052
1052
|
// an operator-imported provider report, not verified provider API data or a
|
|
1053
1053
|
// reconciled mapping to the local request ledger.
|
|
1054
1054
|
const billingTime = (ms) => {
|
|
1055
|
+
// An absent instant is null/undefined/'' and Number() turns each of them into 0,
|
|
1056
|
+
// which is a finite number: the 1970 epoch. Only a positive instant is a time.
|
|
1057
|
+
if (ms === null || ms === undefined || ms === '') return '—';
|
|
1055
1058
|
const n = Number(ms);
|
|
1056
|
-
return Number.isFinite(n) ? new Date(n).toLocaleString() : '—';
|
|
1059
|
+
return Number.isFinite(n) && n > 0 ? new Date(n).toLocaleString() : '—';
|
|
1057
1060
|
};
|
|
1058
1061
|
const billingUsd = (micros) => {
|
|
1059
1062
|
const n = Number(micros);
|
|
@@ -1761,7 +1764,7 @@ function renderValue(d) {
|
|
|
1761
1764
|
if (body.error === 'no-sessions-in-window') {
|
|
1762
1765
|
out.textContent = `no sessions in the last ${body.windowDays} days for "${body.project}" — run some real traffic through the proxy first`;
|
|
1763
1766
|
} else if (!res.ok) {
|
|
1764
|
-
out.textContent = `judge failed: ${body
|
|
1767
|
+
out.textContent = `judge failed: ${serverErrorText(body, String(res.status))}`;
|
|
1765
1768
|
} else {
|
|
1766
1769
|
const j = body.judgment; const t = body.tier;
|
|
1767
1770
|
const warn = t && t.sendsContentOffDevice ? '<span style="color:var(--coral)">⚠ sent a transcript excerpt off-device</span> · ' : '';
|
|
@@ -2060,10 +2063,19 @@ function showSettingsError(error) {
|
|
|
2060
2063
|
});
|
|
2061
2064
|
}
|
|
2062
2065
|
|
|
2066
|
+
// Routes answer either { error: 'text' } or { error: { code, message } }.
|
|
2067
|
+
// Wrapping the object form in an Error printed "[object Object]".
|
|
2068
|
+
function serverErrorText(data, fallback) {
|
|
2069
|
+
const e = data && data.error;
|
|
2070
|
+
if (typeof e === 'string' && e) return e;
|
|
2071
|
+
if (e && typeof e.message === 'string' && e.message) return e.message;
|
|
2072
|
+
return fallback;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2063
2075
|
async function settingsJson(res) {
|
|
2064
2076
|
let data = null;
|
|
2065
2077
|
try { data = await res.json(); } catch (_) { /* use the status below */ }
|
|
2066
|
-
if (!res.ok) throw new Error(data
|
|
2078
|
+
if (!res.ok) throw new Error(serverErrorText(data, `Settings request failed (${res.status})`));
|
|
2067
2079
|
return data;
|
|
2068
2080
|
}
|
|
2069
2081
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>Segreant</title>
|
|
7
|
-
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+
|
|
7
|
+
<link rel="icon" type="image/png" href="data:image/png;base64,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" />
|
|
8
8
|
<!--
|
|
9
9
|
DIRECTION CONTRACT
|
|
10
10
|
|