@chrysb/alphaclaw 0.4.3 → 0.4.4
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/lib/public/js/components/file-tree.js +37 -8
- package/lib/public/js/components/gateway.js +74 -42
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/usage-tab/overview-section.js +100 -26
- package/lib/public/js/lib/api.js +31 -0
- package/lib/server/db/usage/index.js +35 -0
- package/lib/server/db/usage/pricing.js +82 -0
- package/lib/server/db/usage/schema.js +87 -0
- package/lib/server/db/usage/sessions.js +217 -0
- package/lib/server/db/usage/shared.js +139 -0
- package/lib/server/db/usage/summary.js +280 -0
- package/lib/server/db/usage/timeseries.js +64 -0
- package/lib/server/{watchdog-db.js → db/watchdog/index.js} +1 -18
- package/lib/server/db/watchdog/schema.js +21 -0
- package/lib/server/{webhooks-db.js → db/webhooks/index.js} +1 -22
- package/lib/server/db/webhooks/schema.js +25 -0
- package/lib/server/routes/browse/index.js +29 -0
- package/lib/server.js +3 -3
- package/package.json +1 -1
- package/lib/server/usage-db.js +0 -838
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
createBrowseFile,
|
|
14
14
|
createBrowseFolder,
|
|
15
15
|
moveBrowsePath,
|
|
16
|
+
downloadBrowseFile,
|
|
16
17
|
} from "../lib/api.js";
|
|
17
18
|
import {
|
|
18
19
|
kDraftIndexChangedEventName,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
FileAddLineIcon,
|
|
41
42
|
FolderAddLineIcon,
|
|
42
43
|
DeleteBinLineIcon,
|
|
44
|
+
DownloadLineIcon,
|
|
43
45
|
} from "./icons.js";
|
|
44
46
|
import { LoadingSpinner } from "./loading-spinner.js";
|
|
45
47
|
import { ConfirmDialog } from "./confirm-dialog.js";
|
|
@@ -224,6 +226,7 @@ const TreeContextMenu = ({
|
|
|
224
226
|
isLocked,
|
|
225
227
|
onNewFile,
|
|
226
228
|
onNewFolder,
|
|
229
|
+
onDownload,
|
|
227
230
|
onDelete,
|
|
228
231
|
onClose,
|
|
229
232
|
}) => {
|
|
@@ -252,6 +255,7 @@ const TreeContextMenu = ({
|
|
|
252
255
|
const isRoot = targetType === "root";
|
|
253
256
|
const contextFolder = isFolder ? targetPath : "";
|
|
254
257
|
const canCreate = !isLocked && (isFolder || isRoot);
|
|
258
|
+
const canDownload = isFile && targetPath;
|
|
255
259
|
const canDelete = !isLocked && (isFolder || isFile) && targetPath;
|
|
256
260
|
|
|
257
261
|
return html`
|
|
@@ -278,18 +282,33 @@ const TreeContextMenu = ({
|
|
|
278
282
|
</button>
|
|
279
283
|
`
|
|
280
284
|
: null}
|
|
281
|
-
${canDelete
|
|
285
|
+
${canDownload || canDelete
|
|
282
286
|
? html`
|
|
283
287
|
${canCreate
|
|
284
288
|
? html`<div class="tree-context-menu-sep"></div>`
|
|
285
289
|
: null}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
${canDownload
|
|
291
|
+
? html`
|
|
292
|
+
<button
|
|
293
|
+
class="tree-context-menu-item"
|
|
294
|
+
onclick=${() => { onDownload(targetPath); onClose(); }}
|
|
295
|
+
>
|
|
296
|
+
<${DownloadLineIcon} className="tree-context-menu-icon" />
|
|
297
|
+
<span>Download</span>
|
|
298
|
+
</button>
|
|
299
|
+
`
|
|
300
|
+
: null}
|
|
301
|
+
${canDelete
|
|
302
|
+
? html`
|
|
303
|
+
<button
|
|
304
|
+
class="tree-context-menu-item"
|
|
305
|
+
onclick=${() => { onDelete(targetPath); onClose(); }}
|
|
306
|
+
>
|
|
307
|
+
<${DeleteBinLineIcon} className="tree-context-menu-icon" />
|
|
308
|
+
<span>Delete</span>
|
|
309
|
+
</button>
|
|
310
|
+
`
|
|
311
|
+
: null}
|
|
293
312
|
`
|
|
294
313
|
: null}
|
|
295
314
|
${isLocked
|
|
@@ -942,6 +961,15 @@ export const FileTree = ({
|
|
|
942
961
|
setContextMenu(null);
|
|
943
962
|
};
|
|
944
963
|
|
|
964
|
+
const requestDownload = async (targetPath) => {
|
|
965
|
+
try {
|
|
966
|
+
await downloadBrowseFile(targetPath);
|
|
967
|
+
showToast("Download started", "success");
|
|
968
|
+
} catch (downloadError) {
|
|
969
|
+
showToast(downloadError.message || "Could not download file", "error");
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
|
|
945
973
|
const handleDragDrop = async (action, sourcePath, targetFolder) => {
|
|
946
974
|
if (action === "start") {
|
|
947
975
|
setDragSourcePath(sourcePath);
|
|
@@ -1208,6 +1236,7 @@ export const FileTree = ({
|
|
|
1208
1236
|
isLocked=${!!contextMenu.isLocked}
|
|
1209
1237
|
onNewFile=${(folder) => requestCreate(folder, "file")}
|
|
1210
1238
|
onNewFolder=${(folder) => requestCreate(folder, "folder")}
|
|
1239
|
+
onDownload=${requestDownload}
|
|
1211
1240
|
onDelete=${requestDelete}
|
|
1212
1241
|
onClose=${closeContextMenu}
|
|
1213
1242
|
/>
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
|
-
import {
|
|
5
|
-
fetchOpenclawVersion,
|
|
6
|
-
updateOpenclaw,
|
|
7
|
-
} from "../lib/api.js";
|
|
4
|
+
import { fetchOpenclawVersion, updateOpenclaw } from "../lib/api.js";
|
|
8
5
|
import { UpdateActionButton } from "./update-action-button.js";
|
|
9
6
|
import { ConfirmDialog } from "./confirm-dialog.js";
|
|
10
7
|
import { showToast } from "./toast.js";
|
|
@@ -38,7 +35,8 @@ const VersionRow = ({
|
|
|
38
35
|
const [hasUpdate, setHasUpdate] = useState(false);
|
|
39
36
|
const [error, setError] = useState("");
|
|
40
37
|
const [hasViewedChangelog, setHasViewedChangelog] = useState(false);
|
|
41
|
-
const [confirmWithoutChangelogOpen, setConfirmWithoutChangelogOpen] =
|
|
38
|
+
const [confirmWithoutChangelogOpen, setConfirmWithoutChangelogOpen] =
|
|
39
|
+
useState(false);
|
|
42
40
|
const simulateUpdate = (() => {
|
|
43
41
|
try {
|
|
44
42
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -124,7 +122,9 @@ const VersionRow = ({
|
|
|
124
122
|
setChecking(true);
|
|
125
123
|
setError("");
|
|
126
124
|
try {
|
|
127
|
-
const data = isUpdateAction
|
|
125
|
+
const data = isUpdateAction
|
|
126
|
+
? await applyUpdate()
|
|
127
|
+
: await fetchVersion(true);
|
|
128
128
|
setVersion(data.currentVersion || version);
|
|
129
129
|
setLatestVersion(data.latestVersion || null);
|
|
130
130
|
setHasUpdate(!!data.hasUpdate);
|
|
@@ -158,10 +158,14 @@ const VersionRow = ({
|
|
|
158
158
|
} catch (err) {
|
|
159
159
|
setError(
|
|
160
160
|
err.message ||
|
|
161
|
-
(isUpdateAction
|
|
161
|
+
(isUpdateAction
|
|
162
|
+
? `Could not update ${label}`
|
|
163
|
+
: "Could not check updates"),
|
|
162
164
|
);
|
|
163
165
|
showToast(
|
|
164
|
-
isUpdateAction
|
|
166
|
+
isUpdateAction
|
|
167
|
+
? `Could not update ${label}`
|
|
168
|
+
: "Could not check updates",
|
|
165
169
|
"error",
|
|
166
170
|
);
|
|
167
171
|
await onActionComplete({
|
|
@@ -201,7 +205,8 @@ const VersionRow = ({
|
|
|
201
205
|
? `${version}`
|
|
202
206
|
: "..."}
|
|
203
207
|
</p>
|
|
204
|
-
${error &&
|
|
208
|
+
${error &&
|
|
209
|
+
effectiveHasUpdate &&
|
|
205
210
|
html`<div
|
|
206
211
|
class="mt-1 text-xs text-red-300 bg-red-900/30 border border-red-800 rounded-lg px-2 py-1"
|
|
207
212
|
>
|
|
@@ -209,7 +214,9 @@ const VersionRow = ({
|
|
|
209
214
|
</div>`}
|
|
210
215
|
</div>
|
|
211
216
|
<div class="flex items-center gap-3 shrink-0">
|
|
212
|
-
${effectiveHasUpdate &&
|
|
217
|
+
${effectiveHasUpdate &&
|
|
218
|
+
effectiveLatestVersion &&
|
|
219
|
+
html`
|
|
213
220
|
<a
|
|
214
221
|
href=${changelogUrl}
|
|
215
222
|
target="_blank"
|
|
@@ -228,7 +235,9 @@ const VersionRow = ({
|
|
|
228
235
|
idleLabel=${isUpdateActionActive
|
|
229
236
|
? updateIdleLabel
|
|
230
237
|
: "Check updates"}
|
|
231
|
-
loadingLabel=${isUpdateActionActive
|
|
238
|
+
loadingLabel=${isUpdateActionActive
|
|
239
|
+
? "Updating..."
|
|
240
|
+
: "Checking..."}
|
|
232
241
|
className="hidden md:inline-flex"
|
|
233
242
|
/>
|
|
234
243
|
`
|
|
@@ -240,12 +249,15 @@ const VersionRow = ({
|
|
|
240
249
|
idleLabel=${isUpdateActionActive
|
|
241
250
|
? updateIdleLabel
|
|
242
251
|
: "Check updates"}
|
|
243
|
-
loadingLabel=${isUpdateActionActive
|
|
252
|
+
loadingLabel=${isUpdateActionActive
|
|
253
|
+
? "Updating..."
|
|
254
|
+
: "Checking..."}
|
|
244
255
|
/>
|
|
245
256
|
`}
|
|
246
257
|
</div>
|
|
247
258
|
</div>
|
|
248
|
-
${showMobileUpdateRow &&
|
|
259
|
+
${showMobileUpdateRow &&
|
|
260
|
+
html`
|
|
249
261
|
<div class="mt-2 md:hidden flex items-center gap-2">
|
|
250
262
|
<a
|
|
251
263
|
href=${changelogUrl}
|
|
@@ -296,24 +308,25 @@ export const Gateway = ({
|
|
|
296
308
|
const dotClass = isRunning
|
|
297
309
|
? "ac-status-dot ac-status-dot--healthy"
|
|
298
310
|
: "w-2 h-2 rounded-full bg-yellow-500 animate-pulse";
|
|
299
|
-
const watchdogHealth =
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
? "
|
|
306
|
-
: watchdogHealth === "
|
|
307
|
-
? "bg-
|
|
308
|
-
: "
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
311
|
+
const watchdogHealth =
|
|
312
|
+
watchdogStatus?.lifecycle === "crash_loop"
|
|
313
|
+
? "crash_loop"
|
|
314
|
+
: watchdogStatus?.health;
|
|
315
|
+
const watchdogDotClass =
|
|
316
|
+
watchdogHealth === "healthy"
|
|
317
|
+
? "ac-status-dot ac-status-dot--healthy ac-status-dot--healthy-offset"
|
|
318
|
+
: watchdogHealth === "degraded"
|
|
319
|
+
? "bg-yellow-500"
|
|
320
|
+
: watchdogHealth === "unhealthy" || watchdogHealth === "crash_loop"
|
|
321
|
+
? "bg-red-500"
|
|
322
|
+
: "bg-gray-500";
|
|
323
|
+
const watchdogLabel =
|
|
324
|
+
watchdogHealth === "unknown" ? "initializing" : watchdogHealth || "unknown";
|
|
312
325
|
const isRepairInProgress = repairing || !!watchdogStatus?.operationInProgress;
|
|
326
|
+
const showInspectButton = watchdogHealth === "degraded" && !!onOpenWatchdog;
|
|
313
327
|
const showRepairButton =
|
|
314
328
|
isRepairInProgress ||
|
|
315
329
|
watchdogStatus?.lifecycle === "crash_loop" ||
|
|
316
|
-
watchdogStatus?.health === "degraded" ||
|
|
317
330
|
watchdogStatus?.health === "unhealthy" ||
|
|
318
331
|
watchdogStatus?.health === "crashed";
|
|
319
332
|
const liveUptimeMs = useMemo(() => {
|
|
@@ -339,7 +352,9 @@ export const Gateway = ({
|
|
|
339
352
|
<div class="min-w-0 flex items-center gap-2 text-sm">
|
|
340
353
|
<span class=${dotClass}></span>
|
|
341
354
|
<span class="font-semibold">Gateway:</span>
|
|
342
|
-
<span class="text-gray-400"
|
|
355
|
+
<span class="text-gray-400"
|
|
356
|
+
>${restarting ? "restarting..." : status || "checking..."}</span
|
|
357
|
+
>
|
|
343
358
|
</div>
|
|
344
359
|
<div class="flex items-center gap-3 shrink-0">
|
|
345
360
|
${!restarting && isRunning
|
|
@@ -367,18 +382,22 @@ export const Gateway = ({
|
|
|
367
382
|
onclick=${onOpenWatchdog}
|
|
368
383
|
title="Open Watchdog tab"
|
|
369
384
|
>
|
|
370
|
-
<span
|
|
371
|
-
|
|
372
|
-
|
|
385
|
+
<span
|
|
386
|
+
class=${watchdogDotClass.startsWith("ac-status-dot")
|
|
387
|
+
? watchdogDotClass
|
|
388
|
+
: `w-2 h-2 rounded-full ${watchdogDotClass}`}
|
|
389
|
+
></span>
|
|
373
390
|
<span class="font-semibold">Watchdog:</span>
|
|
374
391
|
<span class="text-gray-400">${watchdogLabel}</span>
|
|
375
392
|
</button>
|
|
376
393
|
`
|
|
377
394
|
: html`
|
|
378
395
|
<div class="inline-flex items-center gap-2 text-sm">
|
|
379
|
-
<span
|
|
380
|
-
|
|
381
|
-
|
|
396
|
+
<span
|
|
397
|
+
class=${watchdogDotClass.startsWith("ac-status-dot")
|
|
398
|
+
? watchdogDotClass
|
|
399
|
+
: `w-2 h-2 rounded-full ${watchdogDotClass}`}
|
|
400
|
+
></span>
|
|
382
401
|
<span class="font-semibold">Watchdog:</span>
|
|
383
402
|
<span class="text-gray-400">${watchdogLabel}</span>
|
|
384
403
|
</div>
|
|
@@ -386,24 +405,37 @@ export const Gateway = ({
|
|
|
386
405
|
${onRepair
|
|
387
406
|
? html`
|
|
388
407
|
<div class="shrink-0 w-32 flex justify-end">
|
|
389
|
-
${
|
|
408
|
+
${showInspectButton
|
|
390
409
|
? html`
|
|
391
410
|
<${UpdateActionButton}
|
|
392
|
-
onClick=${
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
loadingLabel="Repairing..."
|
|
411
|
+
onClick=${onOpenWatchdog}
|
|
412
|
+
warning=${false}
|
|
413
|
+
idleLabel="Inspect"
|
|
414
|
+
loadingLabel="Inspect"
|
|
397
415
|
className="w-full justify-center"
|
|
398
416
|
/>
|
|
399
417
|
`
|
|
400
|
-
:
|
|
418
|
+
: showRepairButton
|
|
419
|
+
? html`
|
|
420
|
+
<${UpdateActionButton}
|
|
421
|
+
onClick=${onRepair}
|
|
422
|
+
loading=${isRepairInProgress}
|
|
423
|
+
warning=${true}
|
|
424
|
+
idleLabel="Repair"
|
|
425
|
+
loadingLabel="Repairing..."
|
|
426
|
+
className="w-full justify-center"
|
|
427
|
+
/>
|
|
428
|
+
`
|
|
429
|
+
: html`<span
|
|
430
|
+
class="inline-flex h-7 w-full"
|
|
431
|
+
aria-hidden="true"
|
|
432
|
+
></span>`}
|
|
401
433
|
</div>
|
|
402
434
|
`
|
|
403
435
|
: null}
|
|
404
436
|
</div>
|
|
405
437
|
</div>
|
|
406
|
-
<div class="mt-3
|
|
438
|
+
<div class="mt-3">
|
|
407
439
|
<${VersionRow}
|
|
408
440
|
label="OpenClaw"
|
|
409
441
|
currentVersion=${openclawVersion}
|
|
@@ -288,6 +288,19 @@ export const FolderAddLineIcon = ({ className = "" }) => html`
|
|
|
288
288
|
</svg>
|
|
289
289
|
`;
|
|
290
290
|
|
|
291
|
+
export const DownloadLineIcon = ({ className = "" }) => html`
|
|
292
|
+
<svg
|
|
293
|
+
class=${className}
|
|
294
|
+
viewBox="0 0 24 24"
|
|
295
|
+
fill="currentColor"
|
|
296
|
+
aria-hidden="true"
|
|
297
|
+
>
|
|
298
|
+
<path
|
|
299
|
+
d="M3 19H21V21H3V19ZM13 13.1716L19.0711 7.1005L20.4853 8.51472L12 17L3.51472 8.51472L4.92893 7.1005L11 13.1716V2H13V13.1716Z"
|
|
300
|
+
/>
|
|
301
|
+
</svg>
|
|
302
|
+
`;
|
|
303
|
+
|
|
291
304
|
export const RestartLineIcon = ({ className = "" }) => html`
|
|
292
305
|
<svg
|
|
293
306
|
class=${className}
|
|
@@ -26,40 +26,78 @@ const SummaryCard = ({ title, tokens, cost }) => html`
|
|
|
26
26
|
`;
|
|
27
27
|
|
|
28
28
|
const AgentCostDistribution = ({ summary }) => {
|
|
29
|
-
const agents = Array.isArray(summary?.costByAgent?.agents)
|
|
30
|
-
|
|
29
|
+
const agents = Array.isArray(summary?.costByAgent?.agents)
|
|
30
|
+
? summary.costByAgent.agents
|
|
31
|
+
: [];
|
|
32
|
+
const missingPricingModels = Array.from(
|
|
33
|
+
new Set(
|
|
34
|
+
(summary?.daily || [])
|
|
35
|
+
.flatMap((dayRow) => dayRow?.models || [])
|
|
36
|
+
.filter(
|
|
37
|
+
(modelRow) =>
|
|
38
|
+
!modelRow?.pricingFound && Number(modelRow?.totalTokens || 0) > 0,
|
|
39
|
+
)
|
|
40
|
+
.map(
|
|
41
|
+
(modelRow) =>
|
|
42
|
+
String(modelRow?.model || "unknown").trim() || "unknown",
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
).sort((leftValue, rightValue) => leftValue.localeCompare(rightValue));
|
|
46
|
+
const missingPricingPreview = missingPricingModels.slice(0, 3).join(", ");
|
|
47
|
+
const hasMoreMissingPricingModels = missingPricingModels.length > 3;
|
|
48
|
+
const missingPricingLabel = missingPricingModels.length
|
|
49
|
+
? hasMoreMissingPricingModels
|
|
50
|
+
? `${missingPricingPreview}, +${missingPricingModels.length - 3} more`
|
|
51
|
+
: missingPricingPreview
|
|
52
|
+
: "";
|
|
53
|
+
const [selectedAgent, setSelectedAgent] = useState(() =>
|
|
54
|
+
String(agents[0]?.agent || ""),
|
|
55
|
+
);
|
|
31
56
|
useEffect(() => {
|
|
32
57
|
if (agents.length === 0) {
|
|
33
58
|
if (selectedAgent) setSelectedAgent("");
|
|
34
59
|
return;
|
|
35
60
|
}
|
|
36
|
-
const hasSelectedAgent = agents.some(
|
|
61
|
+
const hasSelectedAgent = agents.some(
|
|
62
|
+
(row) => String(row.agent || "") === selectedAgent,
|
|
63
|
+
);
|
|
37
64
|
if (!hasSelectedAgent) setSelectedAgent(String(agents[0]?.agent || ""));
|
|
38
65
|
}, [agents, selectedAgent]);
|
|
39
66
|
const selectedAgentRow =
|
|
40
|
-
agents.find((row) => String(row.agent || "") === selectedAgent) ||
|
|
67
|
+
agents.find((row) => String(row.agent || "") === selectedAgent) ||
|
|
68
|
+
agents[0] ||
|
|
69
|
+
null;
|
|
41
70
|
|
|
42
71
|
return html`
|
|
43
72
|
<div class="bg-surface border border-border rounded-xl p-4">
|
|
44
73
|
${agents.length === 0
|
|
45
74
|
? html`
|
|
46
|
-
<div
|
|
47
|
-
|
|
75
|
+
<div
|
|
76
|
+
class="flex flex-wrap items-start sm:items-center justify-between gap-3 mb-3"
|
|
77
|
+
>
|
|
78
|
+
<h2 class="card-label text-xs">Estimated cost breakdown</h2>
|
|
48
79
|
</div>
|
|
49
|
-
<p class="text-xs text-gray-500">
|
|
80
|
+
<p class="text-xs text-gray-500">
|
|
81
|
+
No agent usage recorded for this range.
|
|
82
|
+
</p>
|
|
50
83
|
`
|
|
51
84
|
: html`
|
|
52
85
|
<div class="space-y-3">
|
|
53
|
-
<div
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
86
|
+
<div
|
|
87
|
+
class="flex flex-wrap items-start sm:items-center justify-between gap-3"
|
|
88
|
+
>
|
|
89
|
+
<h2 class="card-label text-xs">Estimated cost breakdown</h2>
|
|
90
|
+
<div
|
|
91
|
+
class="inline-flex flex-wrap items-center gap-3 text-xs text-gray-500"
|
|
92
|
+
>
|
|
93
|
+
<label
|
|
94
|
+
class="inline-flex items-center gap-2 text-xs text-gray-500"
|
|
95
|
+
>
|
|
59
96
|
<select
|
|
60
97
|
class="bg-black/30 border border-border rounded-lg text-xs px-2.5 py-1.5 text-gray-200 focus:border-gray-500"
|
|
61
98
|
value=${String(selectedAgentRow?.agent || "")}
|
|
62
|
-
onChange=${(e) =>
|
|
99
|
+
onChange=${(e) =>
|
|
100
|
+
setSelectedAgent(String(e.currentTarget?.value || ""))}
|
|
63
101
|
>
|
|
64
102
|
${agents.map(
|
|
65
103
|
(agentRow) => html`
|
|
@@ -74,17 +112,29 @@ const AgentCostDistribution = ({ summary }) => {
|
|
|
74
112
|
</div>
|
|
75
113
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
76
114
|
${kUsageSourceOrder.map((sourceName) => {
|
|
77
|
-
const sourceRow = (
|
|
78
|
-
|
|
79
|
-
)
|
|
115
|
+
const sourceRow = (
|
|
116
|
+
selectedAgentRow?.sourceBreakdown || []
|
|
117
|
+
).find((row) => String(row.source || "") === sourceName) || {
|
|
118
|
+
source: sourceName,
|
|
119
|
+
totalCost: 0,
|
|
120
|
+
totalTokens: 0,
|
|
121
|
+
turnCount: 0,
|
|
122
|
+
};
|
|
80
123
|
return html`
|
|
81
124
|
<div class="ac-surface-inset px-2.5 py-2">
|
|
82
|
-
<p class="text-[11px] text-gray-500"
|
|
83
|
-
|
|
125
|
+
<p class="text-[11px] text-gray-500">
|
|
126
|
+
${renderSourceLabel(sourceRow.source)}
|
|
127
|
+
</p>
|
|
128
|
+
<p class="text-xs text-gray-300 mt-0.5">
|
|
129
|
+
${formatUsd(sourceRow.totalCost)}
|
|
130
|
+
</p>
|
|
84
131
|
<p class="text-[11px] text-gray-500 mt-0.5">
|
|
85
|
-
${formatInteger(sourceRow.totalTokens)} tok
|
|
86
|
-
|
|
87
|
-
|
|
132
|
+
${formatInteger(sourceRow.totalTokens)} tok ·
|
|
133
|
+
${formatCountLabel(
|
|
134
|
+
sourceRow.turnCount,
|
|
135
|
+
"turn",
|
|
136
|
+
"turns",
|
|
137
|
+
)}
|
|
88
138
|
</p>
|
|
89
139
|
</div>
|
|
90
140
|
`;
|
|
@@ -92,6 +142,19 @@ const AgentCostDistribution = ({ summary }) => {
|
|
|
92
142
|
</div>
|
|
93
143
|
</div>
|
|
94
144
|
`}
|
|
145
|
+
${missingPricingModels.length
|
|
146
|
+
? html`
|
|
147
|
+
<div class="mt-3">
|
|
148
|
+
<p class="text-[11px] text-gray-500">
|
|
149
|
+
<span>
|
|
150
|
+
. Missing model pricing for ${missingPricingModels.length}
|
|
151
|
+
${missingPricingModels.length === 1 ? "model" : "models"}:
|
|
152
|
+
${missingPricingLabel}.
|
|
153
|
+
</span>
|
|
154
|
+
</p>
|
|
155
|
+
</div>
|
|
156
|
+
`
|
|
157
|
+
: null}
|
|
95
158
|
</div>
|
|
96
159
|
`;
|
|
97
160
|
};
|
|
@@ -107,7 +170,11 @@ export const OverviewSection = ({
|
|
|
107
170
|
}) => html`
|
|
108
171
|
<div class="space-y-4">
|
|
109
172
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
110
|
-
<${SummaryCard}
|
|
173
|
+
<${SummaryCard}
|
|
174
|
+
title="Today"
|
|
175
|
+
tokens=${periodSummary.today.tokens}
|
|
176
|
+
cost=${periodSummary.today.cost}
|
|
177
|
+
/>
|
|
111
178
|
<${SummaryCard}
|
|
112
179
|
title="Last 7 days"
|
|
113
180
|
tokens=${periodSummary.week.tokens}
|
|
@@ -120,11 +187,18 @@ export const OverviewSection = ({
|
|
|
120
187
|
/>
|
|
121
188
|
</div>
|
|
122
189
|
<div class="bg-surface border border-border rounded-xl p-4">
|
|
123
|
-
<div
|
|
124
|
-
|
|
190
|
+
<div
|
|
191
|
+
class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3"
|
|
192
|
+
>
|
|
193
|
+
<h2 class="card-label text-xs">
|
|
194
|
+
Daily ${metric === "tokens" ? "tokens" : "cost"} by model
|
|
195
|
+
</h2>
|
|
125
196
|
<div class="flex items-center gap-2">
|
|
126
197
|
<${SegmentedControl}
|
|
127
|
-
options=${kRangeOptions.map((option) => ({
|
|
198
|
+
options=${kRangeOptions.map((option) => ({
|
|
199
|
+
label: option.label,
|
|
200
|
+
value: option.value,
|
|
201
|
+
}))}
|
|
128
202
|
value=${days}
|
|
129
203
|
onChange=${onDaysChange}
|
|
130
204
|
/>
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -576,6 +576,37 @@ export const deleteBrowseFile = async (filePath) => {
|
|
|
576
576
|
return parseJsonOrThrow(res, 'Could not delete file');
|
|
577
577
|
};
|
|
578
578
|
|
|
579
|
+
export const downloadBrowseFile = async (filePath) => {
|
|
580
|
+
const params = new URLSearchParams({ path: String(filePath || "") });
|
|
581
|
+
const res = await authFetch(`/api/browse/download?${params.toString()}`);
|
|
582
|
+
if (!res.ok) {
|
|
583
|
+
const errorText = await res.text();
|
|
584
|
+
throw new Error(errorText || "Could not download file");
|
|
585
|
+
}
|
|
586
|
+
const fileBlob = await res.blob();
|
|
587
|
+
const urlApi = window?.URL || URL;
|
|
588
|
+
if (!urlApi?.createObjectURL || !urlApi?.revokeObjectURL) {
|
|
589
|
+
throw new Error("Download is not supported in this browser");
|
|
590
|
+
}
|
|
591
|
+
const downloadUrl = urlApi.createObjectURL(fileBlob);
|
|
592
|
+
const fileName =
|
|
593
|
+
String(filePath || "")
|
|
594
|
+
.split("/")
|
|
595
|
+
.filter(Boolean)
|
|
596
|
+
.pop() || "download";
|
|
597
|
+
try {
|
|
598
|
+
const downloadLink = document.createElement("a");
|
|
599
|
+
downloadLink.href = downloadUrl;
|
|
600
|
+
downloadLink.download = fileName;
|
|
601
|
+
document.body?.appendChild(downloadLink);
|
|
602
|
+
downloadLink.click();
|
|
603
|
+
downloadLink.remove();
|
|
604
|
+
} finally {
|
|
605
|
+
urlApi.revokeObjectURL(downloadUrl);
|
|
606
|
+
}
|
|
607
|
+
return { ok: true };
|
|
608
|
+
};
|
|
609
|
+
|
|
579
610
|
export const restoreBrowseFile = async (filePath) => {
|
|
580
611
|
const res = await authFetch('/api/browse/restore', {
|
|
581
612
|
method: 'POST',
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
4
|
+
const { ensureSchema } = require("./schema");
|
|
5
|
+
const { getDailySummary } = require("./summary");
|
|
6
|
+
const { getSessionsList, getSessionDetail } = require("./sessions");
|
|
7
|
+
const { getSessionTimeSeries } = require("./timeseries");
|
|
8
|
+
const { kGlobalModelPricing } = require("./pricing");
|
|
9
|
+
|
|
10
|
+
let db = null;
|
|
11
|
+
let usageDbPath = "";
|
|
12
|
+
|
|
13
|
+
const ensureDb = () => {
|
|
14
|
+
if (!db) throw new Error("Usage DB not initialized");
|
|
15
|
+
return db;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const initUsageDb = ({ rootDir }) => {
|
|
19
|
+
const dbDir = path.join(rootDir, "db");
|
|
20
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
21
|
+
usageDbPath = path.join(dbDir, "usage.db");
|
|
22
|
+
db = new DatabaseSync(usageDbPath);
|
|
23
|
+
ensureSchema(db);
|
|
24
|
+
return { path: usageDbPath };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
initUsageDb,
|
|
29
|
+
getDailySummary: (options = {}) => getDailySummary({ database: ensureDb(), ...options }),
|
|
30
|
+
getSessionsList: (options = {}) => getSessionsList({ database: ensureDb(), ...options }),
|
|
31
|
+
getSessionDetail: (options = {}) => getSessionDetail({ database: ensureDb(), ...options }),
|
|
32
|
+
getSessionTimeSeries: (options = {}) =>
|
|
33
|
+
getSessionTimeSeries({ database: ensureDb(), ...options }),
|
|
34
|
+
kGlobalModelPricing,
|
|
35
|
+
};
|