@oharness/server 0.9.0 → 0.9.2
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/dist/account-usage.d.ts +29 -0
- package/dist/account-usage.js +56 -0
- package/dist/account-usage.js.map +1 -0
- package/dist/i18n.js +66 -2
- package/dist/i18n.js.map +1 -1
- package/dist/page/client/panels.js +5 -1
- package/dist/page/client/panels.js.map +1 -1
- package/dist/page/client/shell.js +2 -0
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/usage.js +450 -88
- package/dist/page/client/usage.js.map +1 -1
- package/dist/page/style.js +43 -0
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +1 -0
- package/dist/page.js.map +1 -1
- package/dist/server.js +94 -35
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/page/style.js
CHANGED
|
@@ -1453,6 +1453,49 @@ details.setting>summary:focus-visible{outline:none;background:var(--hover)}
|
|
|
1453
1453
|
.status-dot.ok{background:var(--ok);opacity:1}
|
|
1454
1454
|
/* The presets and the calendar on one line, on the same baseline. */
|
|
1455
1455
|
.usage-window{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
1456
|
+
.usage-kind-title{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
|
|
1457
|
+
.usage-head-actions{display:inline-flex;align-items:center;gap:8px;margin-left:auto}
|
|
1458
|
+
.usage-head-actions .seg{margin:0}
|
|
1459
|
+
.usage-head-actions .seg-item{padding:3px 10px;font-size:12px}
|
|
1460
|
+
/*
|
|
1461
|
+
* The kinds, as figures that are also the filter. Filled, never outlined: the
|
|
1462
|
+
* chosen one takes the accent fill the model chips use for "this one".
|
|
1463
|
+
*/
|
|
1464
|
+
.usage-kinds{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin:4px 0 10px}
|
|
1465
|
+
.usage-kind{display:flex;flex-direction:column;align-items:flex-start;gap:2px;min-width:0;padding:12px 14px;border:0;border-radius:10px;background:var(--panel);color:var(--fg);font:inherit;text-align:left;cursor:pointer}
|
|
1466
|
+
.usage-kind:hover{box-shadow:inset 0 0 0 999px var(--hover)}
|
|
1467
|
+
.usage-kind:focus-visible{outline:none;box-shadow:inset 0 0 0 999px var(--sel)}
|
|
1468
|
+
.usage-kind-label{font-size:11.5px;color:var(--dim)}
|
|
1469
|
+
.usage-kind-value{font-size:20px;font-weight:600;letter-spacing:-.02em;font-variant-numeric:tabular-nums;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1470
|
+
.usage-kind-sub{font-size:12px;color:var(--dim)}
|
|
1471
|
+
.usage-kind.empty .usage-kind-value{color:var(--dim)}
|
|
1472
|
+
.usage-kind.on{background:var(--accent);color:var(--bg);box-shadow:none}
|
|
1473
|
+
.usage-kind.on .usage-kind-label,.usage-kind.on .usage-kind-sub,.usage-kind.on.empty .usage-kind-value{color:var(--bg);opacity:.72}
|
|
1474
|
+
.usage-counted{margin:0 0 10px}
|
|
1475
|
+
/* A model list: its own height, its own scroll, no frame drawn round it. */
|
|
1476
|
+
.usage-list{max-height:440px;overflow-y:auto;overscroll-behavior:contain}
|
|
1477
|
+
.usage-list>.row-card:last-of-type{margin-bottom:0}
|
|
1478
|
+
/* The share bar: a filled track, no outline, thick enough to carry its facts. */
|
|
1479
|
+
.usage-model{padding:12px 14px}
|
|
1480
|
+
.usage-model .row-head{margin-bottom:8px}
|
|
1481
|
+
.usage-share{position:relative;height:28px;border-radius:6px;background:var(--field);overflow:hidden}
|
|
1482
|
+
.usage-share-fill{position:absolute;inset:0 auto 0 0;border-radius:6px;background:color-mix(in srgb,var(--brand) 34%,transparent)}
|
|
1483
|
+
.usage-share-label{position:relative;display:flex;align-items:center;gap:12px;height:100%;padding:0 10px;min-width:0;font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
1484
|
+
.usage-model-pct{flex:none;color:var(--fg);font-weight:600}
|
|
1485
|
+
.usage-share-facts{min-width:0;overflow:hidden;text-overflow:ellipsis;color:var(--fg);opacity:.72}
|
|
1486
|
+
/* Amounts in one column at the right edge, so a list is compared by glancing down it. */
|
|
1487
|
+
.usage .row-head .usage-amount{margin-left:auto;flex:none;font-variant-numeric:tabular-nums}
|
|
1488
|
+
.usage .row-head .row-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1489
|
+
@media (max-width:640px){.usage-kinds{grid-template-columns:repeat(2,minmax(0,1fr))}.usage-kind:first-child{grid-column:1/-1}}
|
|
1490
|
+
.usage-section-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:22px 0 8px}
|
|
1491
|
+
.usage-section-head .section-title{margin:0}
|
|
1492
|
+
.usage-session{cursor:pointer}
|
|
1493
|
+
.usage-session:hover{background:var(--hover)}
|
|
1494
|
+
.usage-session:focus-visible{outline:none;background:var(--hover)}
|
|
1495
|
+
.usage-sessions-count{margin:10px 0 8px}
|
|
1496
|
+
.usage-sessions-foot{padding:10px 0 24px;text-align:center}
|
|
1497
|
+
.usage-sessions-sentinel{height:1px}
|
|
1498
|
+
.usage-kind-title .muted{font-weight:400;text-transform:none;letter-spacing:0}
|
|
1456
1499
|
.perms-card{display:flex;flex-direction:column;gap:10px}
|
|
1457
1500
|
.perms-card .setting-list{margin:0}
|
|
1458
1501
|
/* The mode, as the one fact the rest of the page qualifies. */
|
package/dist/page/style.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.js","sourceRoot":"","sources":["../../src/page/style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;EAC7B,SAAS
|
|
1
|
+
{"version":3,"file":"style.js","sourceRoot":"","sources":["../../src/page/style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;EAC7B,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8+FV,CAAC"}
|
package/dist/page.js
CHANGED
|
@@ -82,6 +82,7 @@ ${ANALYTICS_STUB}
|
|
|
82
82
|
configuration: this row answers "where did that chart go", which is a
|
|
83
83
|
question asked mid-task, while the three below it are settings. -->
|
|
84
84
|
<button class="nav-item" id="nav-artifacts" type="button"><svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.67" width="8" height="6" rx="1"/><rect x="6" y="7.33" width="8" height="6" rx="1"/></svg><span data-t="nav.artifacts"></span><span class="count" id="artifact-count"></span></button>
|
|
85
|
+
<button class="nav-item" id="nav-sessions" type="button"><svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13.33 10a1.33 1.33 0 0 1-1.33 1.33H4.67L2 14V3.33A1.33 1.33 0 0 1 3.33 2H12a1.33 1.33 0 0 1 1.33 1.33Z"/><path d="M5.33 5.33h5.34"/><path d="M5.33 8h3.34"/></svg><span data-t="nav.sessions"></span></button>
|
|
85
86
|
<button class="nav-item" id="nav-models" type="button"><svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 5.33a1.33 1.33 0 0 0-.67-1.15L8.67 1.51a1.33 1.33 0 0 0-1.33 0L2.67 4.18A1.33 1.33 0 0 0 2 5.33v5.33a1.33 1.33 0 0 0 .67 1.15l4.67 2.67a1.33 1.33 0 0 0 1.33 0l4.67-2.67A1.33 1.33 0 0 0 14 10.67Z"/><path d="m2.2 4.67 5.8 3.33 5.8-3.33"/><path d="M8 14.67V8"/></svg><span data-t="nav.models"></span><span class="count" id="model-count"></span></button>
|
|
86
87
|
<button class="nav-item" id="nav-routines" type="button"><svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6.67"/><path d="M8 4v4l2.67 1.33"/></svg><span data-t="nav.routines"></span><span class="count" id="routine-count"></span></button>
|
|
87
88
|
<button class="nav-item" id="nav-settings" type="button"><svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.67 4.67h-6"/><path d="M9.33 11.33H3.33"/><circle cx="4.67" cy="4.67" r="2"/><circle cx="11.33" cy="11.33" r="2"/></svg><span data-t="nav.settings"></span></button>
|
package/dist/page.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.js","sourceRoot":"","sources":["../src/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEpD;;;;;;;;;;;;;;GAcG;AAGH;;;;;;;GAOG;AACH,MAAM,OAAO,GAAG;;;;;yCAKyB,CAAC;AAE1C,MAAM,CAAC,MAAM,IAAI,GAAG;;;;;;;4CAOwB,kBAAkB,CAAC,OAAO,CAAC;SAC9D,KAAK;EACZ,cAAc
|
|
1
|
+
{"version":3,"file":"page.js","sourceRoot":"","sources":["../src/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEpD;;;;;;;;;;;;;;GAcG;AAGH;;;;;;;GAOG;AACH,MAAM,OAAO,GAAG;;;;;yCAKyB,CAAC;AAE1C,MAAM,CAAC,MAAM,IAAI,GAAG;;;;;;;4CAOwB,kBAAkB,CAAC,OAAO,CAAC;SAC9D,KAAK;EACZ,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA2YK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;qBACvB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;EAC1C,aAAa;EACb,UAAU;EACV,gBAAgB;;;QAGV,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -18,7 +18,7 @@ import { visibleText } from "@oharness/engine/core/types.ts";
|
|
|
18
18
|
import { errorMessage } from "@oharness/engine/core/errors.ts";
|
|
19
19
|
import { officialBaseUrl, officialTeam } from "@oharness/engine/auth/providers/official.ts";
|
|
20
20
|
import { loadUserConfig, updateUserConfig } from "@oharness/engine/config.ts";
|
|
21
|
-
import {
|
|
21
|
+
import { EXTENSION_IDS, EXTENSION_INSTALL_URL } from "@oharness/engine/browser/host-manifest.ts";
|
|
22
22
|
import { announcePresence } from "@oharness/engine/browser/presence.ts";
|
|
23
23
|
import { extensionHostReachable } from "@oharness/engine/browser/socket.ts";
|
|
24
24
|
import { RoutineRunner } from "@oharness/engine/schedule/runner.ts";
|
|
@@ -38,6 +38,7 @@ import { artifactDocument, sendArtifact } from "./artifacts.js";
|
|
|
38
38
|
import { sendProjectExport } from "./project-export.js";
|
|
39
39
|
import { ArtifactStore, getArtifactStore, isCanvas, isValidArtifactId, restoreArtifacts, } from "@oharness/engine/artifacts/store.ts";
|
|
40
40
|
import { accountTeams, publishArtifact, unpublishArtifact } from "@oharness/engine/artifacts/publish.ts";
|
|
41
|
+
import { accountUsage } from "./account-usage.js";
|
|
41
42
|
/**
|
|
42
43
|
* How many stored sessions the usage page counts.
|
|
43
44
|
*
|
|
@@ -47,6 +48,15 @@ import { accountTeams, publishArtifact, unpublishArtifact } from "@oharness/engi
|
|
|
47
48
|
* stops short says so rather than reading as the whole history.
|
|
48
49
|
*/
|
|
49
50
|
const USAGE_SESSION_SCAN = 200;
|
|
51
|
+
/**
|
|
52
|
+
* How far back the sessions page reads, and how many rows it sends at a time.
|
|
53
|
+
*
|
|
54
|
+
* The page is "every session that spent something", so it reads further back
|
|
55
|
+
* than the usage summary does — but still a bounded number, and it says when
|
|
56
|
+
* it stopped. A page at a time because the list is drawn as it is scrolled.
|
|
57
|
+
*/
|
|
58
|
+
const USAGE_SESSIONS_SCAN = 5000;
|
|
59
|
+
const USAGE_SESSIONS_PAGE = 50;
|
|
50
60
|
/**
|
|
51
61
|
* How many trail entries one read of every session may return.
|
|
52
62
|
*
|
|
@@ -452,7 +462,7 @@ export async function startWebServer(opts) {
|
|
|
452
462
|
"content-type": "text/html; charset=utf-8",
|
|
453
463
|
// Framed by itself and by the OHarness extension's side panel, and by
|
|
454
464
|
// nothing else: a page that drives a shell is not one to embed.
|
|
455
|
-
"content-security-policy": `frame-ancestors 'self' chrome-extension://${
|
|
465
|
+
"content-security-policy": `frame-ancestors 'self' ${EXTENSION_IDS.map((id) => `chrome-extension://${id}`).join(" ")}`,
|
|
456
466
|
});
|
|
457
467
|
res.end(PAGE);
|
|
458
468
|
return;
|
|
@@ -2124,6 +2134,22 @@ export async function startWebServer(opts) {
|
|
|
2124
2134
|
})),
|
|
2125
2135
|
});
|
|
2126
2136
|
}
|
|
2137
|
+
/**
|
|
2138
|
+
* The account's spend, from the site's ledger rather than this machine.
|
|
2139
|
+
*
|
|
2140
|
+
* What `GET /usage` cannot see: chat from other devices, and every image,
|
|
2141
|
+
* video and clip of audio, which are billed by the gateway and settled
|
|
2142
|
+
* after the call that started them. Answers `signedIn: false` for a
|
|
2143
|
+
* machine with no account, so the page falls back to its own figures.
|
|
2144
|
+
*/
|
|
2145
|
+
case "GET /usage/account": {
|
|
2146
|
+
return send(res, 200, await accountUsage({
|
|
2147
|
+
token: await current.harness.accountToken(),
|
|
2148
|
+
team: officialTeam(),
|
|
2149
|
+
site: siteUrl(),
|
|
2150
|
+
query: url.searchParams,
|
|
2151
|
+
}));
|
|
2152
|
+
}
|
|
2127
2153
|
/**
|
|
2128
2154
|
* What has been spent on this machine, and on what.
|
|
2129
2155
|
*
|
|
@@ -2137,6 +2163,33 @@ export async function startWebServer(opts) {
|
|
|
2137
2163
|
* mid-turn has already spent money its metadata will not know about
|
|
2138
2164
|
* until the turn lands, and the page asking is the page that spent it.
|
|
2139
2165
|
*/
|
|
2166
|
+
/**
|
|
2167
|
+
* Every session that spent something, newest first, a page at a time.
|
|
2168
|
+
*
|
|
2169
|
+
* The usage page lists the latest few; this is the rest of them, with a
|
|
2170
|
+
* search over the title and the model. Filtered here rather than in the
|
|
2171
|
+
* page so a search reaches sessions the page has not loaded yet.
|
|
2172
|
+
*/
|
|
2173
|
+
case "GET /usage/sessions": {
|
|
2174
|
+
const { stored, rows } = await usageRows(USAGE_SESSIONS_SCAN);
|
|
2175
|
+
const query = (url.searchParams.get("q") || "").trim().toLowerCase();
|
|
2176
|
+
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
2177
|
+
const limit = Math.min(200, Math.max(1, Number(url.searchParams.get("limit")) || USAGE_SESSIONS_PAGE));
|
|
2178
|
+
const list = [...rows.values()]
|
|
2179
|
+
.filter((row) => row.costUsd > 0 || row.input || row.output || row.cacheRead || row.cacheWrite)
|
|
2180
|
+
.filter((row) => !query || (row.title || "").toLowerCase().includes(query) || row.model.toLowerCase().includes(query))
|
|
2181
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
2182
|
+
return send(res, 200, {
|
|
2183
|
+
sessions: list.slice(offset, offset + limit).map((row) => ({
|
|
2184
|
+
...row,
|
|
2185
|
+
current: row.id === current.session.id,
|
|
2186
|
+
})),
|
|
2187
|
+
total: list.length,
|
|
2188
|
+
offset,
|
|
2189
|
+
more: offset + limit < list.length,
|
|
2190
|
+
truncated: stored.length >= USAGE_SESSIONS_SCAN,
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2140
2193
|
case "GET /usage": {
|
|
2141
2194
|
// How far back the page is asking about. Everything below — the
|
|
2142
2195
|
// totals, the models, the sessions and the curve — is that window and
|
|
@@ -2144,33 +2197,7 @@ export async function startWebServer(opts) {
|
|
|
2144
2197
|
// from the chart under it is worse than no figure.
|
|
2145
2198
|
const window = usageWindow(url.searchParams.get("range"), url.searchParams.get("from"), url.searchParams.get("to"));
|
|
2146
2199
|
const { since, until } = window;
|
|
2147
|
-
const stored = await
|
|
2148
|
-
const rows = new Map();
|
|
2149
|
-
for (const meta of stored) {
|
|
2150
|
-
rows.set(meta.id, {
|
|
2151
|
-
id: meta.id,
|
|
2152
|
-
title: meta.title,
|
|
2153
|
-
model: meta.model,
|
|
2154
|
-
updatedAt: meta.updatedAt,
|
|
2155
|
-
turns: meta.turns,
|
|
2156
|
-
costUsd: meta.costUsd || 0,
|
|
2157
|
-
...tokensOf(meta.usage),
|
|
2158
|
-
});
|
|
2159
|
-
}
|
|
2160
|
-
for (const runner of runners.values()) {
|
|
2161
|
-
const sess = runner.session;
|
|
2162
|
-
rows.set(sess.id, {
|
|
2163
|
-
id: sess.id,
|
|
2164
|
-
title: sess.title,
|
|
2165
|
-
model: sess.model.id,
|
|
2166
|
-
// A session opened but not yet spoken to has no stored copy and no
|
|
2167
|
-
// last-updated time of its own; it is here, so it is now.
|
|
2168
|
-
updatedAt: rows.get(sess.id)?.updatedAt ?? Date.now(),
|
|
2169
|
-
turns: sess.turns,
|
|
2170
|
-
costUsd: sess.costUsd,
|
|
2171
|
-
...tokensOf(sess.usage),
|
|
2172
|
-
});
|
|
2173
|
-
}
|
|
2200
|
+
const { stored, rows } = await usageRows(USAGE_SESSION_SCAN);
|
|
2174
2201
|
// Sessions that never sent anything are dropped: they contribute zero
|
|
2175
2202
|
// to every total here, and a page about what was spent that opens with
|
|
2176
2203
|
// a screen of "$0.0000 · 0 tokens" buries the ones that cost money.
|
|
@@ -2214,12 +2241,6 @@ export async function startWebServer(opts) {
|
|
|
2214
2241
|
// Sorted by what was spent, not by name: the row worth seeing first
|
|
2215
2242
|
// is the expensive one.
|
|
2216
2243
|
models: [...byModel.values()].sort((a, b) => b.costUsd - a.costUsd),
|
|
2217
|
-
// A page of them is a page nobody reads. The rest are counted in the
|
|
2218
|
-
// totals above, which is where a number that large belongs.
|
|
2219
|
-
sessions: list.slice(0, 20).map((row) => ({
|
|
2220
|
-
...row,
|
|
2221
|
-
current: row.id === current.session.id,
|
|
2222
|
-
})),
|
|
2223
2244
|
scanned: rows.size,
|
|
2224
2245
|
// Said out loud so a total that stops short is not read as final.
|
|
2225
2246
|
truncated: stored.length >= USAGE_SESSION_SCAN,
|
|
@@ -3571,6 +3592,44 @@ export async function startWebServer(opts) {
|
|
|
3571
3592
|
* log carries. Everything else is folded from the revision logs, which is
|
|
3572
3593
|
* what makes the panel's answer independent of which tabs are open.
|
|
3573
3594
|
*/
|
|
3595
|
+
/**
|
|
3596
|
+
* One row per session: the stored ones, with the open ones laid over their
|
|
3597
|
+
* stored copy. A session mid-turn has already spent money its metadata will
|
|
3598
|
+
* not know about until the turn lands, and the page asking is the page that
|
|
3599
|
+
* spent it.
|
|
3600
|
+
*/
|
|
3601
|
+
async function usageRows(scan) {
|
|
3602
|
+
const stored = await current.harness.listSessions(scan);
|
|
3603
|
+
const rows = new Map();
|
|
3604
|
+
for (const meta of stored) {
|
|
3605
|
+
rows.set(meta.id, {
|
|
3606
|
+
id: meta.id,
|
|
3607
|
+
cwd: meta.cwd,
|
|
3608
|
+
title: meta.title,
|
|
3609
|
+
model: meta.model,
|
|
3610
|
+
updatedAt: meta.updatedAt,
|
|
3611
|
+
turns: meta.turns,
|
|
3612
|
+
costUsd: meta.costUsd || 0,
|
|
3613
|
+
...tokensOf(meta.usage),
|
|
3614
|
+
});
|
|
3615
|
+
}
|
|
3616
|
+
for (const runner of runners.values()) {
|
|
3617
|
+
const sess = runner.session;
|
|
3618
|
+
rows.set(sess.id, {
|
|
3619
|
+
id: sess.id,
|
|
3620
|
+
cwd: rows.get(sess.id)?.cwd ?? runner.harness.cwd,
|
|
3621
|
+
title: sess.title,
|
|
3622
|
+
model: sess.model.id,
|
|
3623
|
+
// A session opened but not yet spoken to has no stored copy and no
|
|
3624
|
+
// last-updated time of its own; it is here, so it is now.
|
|
3625
|
+
updatedAt: rows.get(sess.id)?.updatedAt ?? Date.now(),
|
|
3626
|
+
turns: sess.turns,
|
|
3627
|
+
costUsd: sess.costUsd,
|
|
3628
|
+
...tokensOf(sess.usage),
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3631
|
+
return { stored, rows };
|
|
3632
|
+
}
|
|
3574
3633
|
async function artifactCatalogue() {
|
|
3575
3634
|
const groups = [];
|
|
3576
3635
|
for (const runner of runners.values()) {
|