agent-mockingbird 0.0.1
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/.agents/skills/btca-cli/SKILL.md +64 -0
- package/.agents/skills/btca-cli/agents/openai.yaml +3 -0
- package/.agents/skills/frontend-design/SKILL.md +42 -0
- package/.agents/skills/frontend-design/agents/openai.yaml +3 -0
- package/.env.example +36 -0
- package/.githooks/pre-commit +33 -0
- package/.github/workflows/ci.yml +309 -0
- package/.opencode/bun.lock +18 -0
- package/.opencode/package.json +5 -0
- package/.opencode/tools/agent_type_manager.ts +100 -0
- package/.opencode/tools/config_manager.ts +87 -0
- package/.opencode/tools/cron_manager.ts +145 -0
- package/.opencode/tools/memory_get.ts +43 -0
- package/.opencode/tools/memory_remember.ts +53 -0
- package/.opencode/tools/memory_search.ts +48 -0
- package/AGENTS.md +126 -0
- package/MEMORY.md +2 -0
- package/README.md +451 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/agent-mockingbird.config.example.json +135 -0
- package/apps/server/package.json +32 -0
- package/apps/server/src/backend/agents/bootstrapContext.ts +362 -0
- package/apps/server/src/backend/agents/openclawImport.test.ts +133 -0
- package/apps/server/src/backend/agents/openclawImport.ts +797 -0
- package/apps/server/src/backend/agents/opencodeConfig.ts +428 -0
- package/apps/server/src/backend/agents/service.ts +10 -0
- package/apps/server/src/backend/config/example-config.test.ts +20 -0
- package/apps/server/src/backend/config/orchestration.ts +243 -0
- package/apps/server/src/backend/config/policy.ts +158 -0
- package/apps/server/src/backend/config/schema.test.ts +15 -0
- package/apps/server/src/backend/config/schema.ts +391 -0
- package/apps/server/src/backend/config/semantic.test.ts +34 -0
- package/apps/server/src/backend/config/semantic.ts +149 -0
- package/apps/server/src/backend/config/service.test.ts +75 -0
- package/apps/server/src/backend/config/service.ts +207 -0
- package/apps/server/src/backend/config/smoke.ts +77 -0
- package/apps/server/src/backend/config/store.test.ts +123 -0
- package/apps/server/src/backend/config/store.ts +581 -0
- package/apps/server/src/backend/config/testFixtures.ts +5 -0
- package/apps/server/src/backend/config/types.ts +56 -0
- package/apps/server/src/backend/contracts/events.ts +320 -0
- package/apps/server/src/backend/contracts/runtime.ts +111 -0
- package/apps/server/src/backend/cron/executor.ts +435 -0
- package/apps/server/src/backend/cron/repository.ts +170 -0
- package/apps/server/src/backend/cron/service.ts +660 -0
- package/apps/server/src/backend/cron/storage.ts +92 -0
- package/apps/server/src/backend/cron/types.ts +138 -0
- package/apps/server/src/backend/cron/utils.ts +351 -0
- package/apps/server/src/backend/db/client.ts +20 -0
- package/apps/server/src/backend/db/migrate.ts +40 -0
- package/apps/server/src/backend/db/repository.ts +1762 -0
- package/apps/server/src/backend/db/schema.ts +113 -0
- package/apps/server/src/backend/db/usageDashboard.test.ts +102 -0
- package/apps/server/src/backend/db/wipe.ts +13 -0
- package/apps/server/src/backend/defaults.ts +32 -0
- package/apps/server/src/backend/env.ts +48 -0
- package/apps/server/src/backend/heartbeat/activeHours.ts +45 -0
- package/apps/server/src/backend/heartbeat/defaultJob.ts +88 -0
- package/apps/server/src/backend/heartbeat/heartbeat.test.ts +110 -0
- package/apps/server/src/backend/heartbeat/runtimeService.ts +190 -0
- package/apps/server/src/backend/heartbeat/service.ts +176 -0
- package/apps/server/src/backend/heartbeat/state.test.ts +63 -0
- package/apps/server/src/backend/heartbeat/state.ts +167 -0
- package/apps/server/src/backend/heartbeat/types.ts +54 -0
- package/apps/server/src/backend/http/boundedQueue.test.ts +49 -0
- package/apps/server/src/backend/http/boundedQueue.ts +92 -0
- package/apps/server/src/backend/http/parsers.ts +40 -0
- package/apps/server/src/backend/http/router.ts +61 -0
- package/apps/server/src/backend/http/routes/agentRoutes.ts +67 -0
- package/apps/server/src/backend/http/routes/backgroundRoutes.ts +203 -0
- package/apps/server/src/backend/http/routes/chatRoutes.ts +107 -0
- package/apps/server/src/backend/http/routes/configRoutes.ts +602 -0
- package/apps/server/src/backend/http/routes/cronRoutes.ts +221 -0
- package/apps/server/src/backend/http/routes/dashboardRoutes.ts +308 -0
- package/apps/server/src/backend/http/routes/eventRoutes.ts +7 -0
- package/apps/server/src/backend/http/routes/heartbeatRoutes.test.ts +41 -0
- package/apps/server/src/backend/http/routes/heartbeatRoutes.ts +28 -0
- package/apps/server/src/backend/http/routes/index.ts +101 -0
- package/apps/server/src/backend/http/routes/mcpRoutes.ts +213 -0
- package/apps/server/src/backend/http/routes/memoryRoutes.ts +154 -0
- package/apps/server/src/backend/http/routes/runRoutes.ts +310 -0
- package/apps/server/src/backend/http/routes/runtimeRoutes.ts +197 -0
- package/apps/server/src/backend/http/routes/skillRoutes.ts +112 -0
- package/apps/server/src/backend/http/routes/uiRoutes.test.ts +161 -0
- package/apps/server/src/backend/http/routes/uiRoutes.ts +177 -0
- package/apps/server/src/backend/http/routes/usageRoutes.test.ts +104 -0
- package/apps/server/src/backend/http/routes/usageRoutes.ts +767 -0
- package/apps/server/src/backend/http/schemas.ts +64 -0
- package/apps/server/src/backend/http/sse.ts +144 -0
- package/apps/server/src/backend/integration/backend-core.test.ts +2316 -0
- package/apps/server/src/backend/logging/logger.ts +64 -0
- package/apps/server/src/backend/mcp/service.ts +326 -0
- package/apps/server/src/backend/memory/cli.ts +170 -0
- package/apps/server/src/backend/memory/conceptExpansion.test.ts +28 -0
- package/apps/server/src/backend/memory/conceptExpansion.ts +80 -0
- package/apps/server/src/backend/memory/qmdPort.test.ts +54 -0
- package/apps/server/src/backend/memory/qmdPort.ts +61 -0
- package/apps/server/src/backend/memory/records.test.ts +66 -0
- package/apps/server/src/backend/memory/records.ts +229 -0
- package/apps/server/src/backend/memory/service.ts +2012 -0
- package/apps/server/src/backend/memory/sqliteVec.ts +58 -0
- package/apps/server/src/backend/memory/types.ts +104 -0
- package/apps/server/src/backend/opencode/agentMockingbirdPlugin.test.ts +396 -0
- package/apps/server/src/backend/opencode/client.ts +98 -0
- package/apps/server/src/backend/opencode/models.ts +41 -0
- package/apps/server/src/backend/opencode/systemPrompt.test.ts +146 -0
- package/apps/server/src/backend/opencode/systemPrompt.ts +284 -0
- package/apps/server/src/backend/paths.ts +57 -0
- package/apps/server/src/backend/prompts/service.ts +100 -0
- package/apps/server/src/backend/queue/queue.test.ts +189 -0
- package/apps/server/src/backend/queue/service.ts +177 -0
- package/apps/server/src/backend/queue/types.ts +39 -0
- package/apps/server/src/backend/run/service.ts +576 -0
- package/apps/server/src/backend/run/storage.ts +47 -0
- package/apps/server/src/backend/run/types.ts +44 -0
- package/apps/server/src/backend/runtime/errors.ts +61 -0
- package/apps/server/src/backend/runtime/index.ts +72 -0
- package/apps/server/src/backend/runtime/memoryPromptDedup.test.ts +153 -0
- package/apps/server/src/backend/runtime/memoryPromptDedup.ts +76 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/backgroundMethods.ts +765 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/coreMethods.ts +705 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/eventMethods.ts +503 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/memoryMethods.ts +462 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/promptMethods.ts +1167 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/shared.ts +254 -0
- package/apps/server/src/backend/runtime/opencodeRuntime.test.ts +2899 -0
- package/apps/server/src/backend/runtime/opencodeRuntime.ts +135 -0
- package/apps/server/src/backend/runtime/sessionScope.ts +45 -0
- package/apps/server/src/backend/skills/service.ts +442 -0
- package/apps/server/src/backend/workspace/resolve.ts +27 -0
- package/apps/server/src/cli/agent-mockingbird.mjs +2522 -0
- package/apps/server/src/cli/agent-mockingbird.test.ts +68 -0
- package/apps/server/src/cli/runtime-assets.mjs +269 -0
- package/apps/server/src/cli/runtime-assets.test.ts +52 -0
- package/apps/server/src/cli/runtime-layout.mjs +75 -0
- package/apps/server/src/cli/standaloneBuild.test.ts +19 -0
- package/apps/server/src/cli/standaloneBuild.ts +19 -0
- package/apps/server/src/cli/standaloneCronBinary.test.ts +187 -0
- package/apps/server/src/index.ts +178 -0
- package/apps/server/tsconfig.json +12 -0
- package/backlog.md +5 -0
- package/bin/agent-mockingbird +2522 -0
- package/bin/runtime-layout.mjs +75 -0
- package/build-bin.ts +34 -0
- package/build-cli.mjs +37 -0
- package/build.ts +40 -0
- package/bun-env.d.ts +11 -0
- package/bun.lock +888 -0
- package/bunfig.toml +2 -0
- package/components.json +21 -0
- package/config.json +130 -0
- package/deploy/RELEASE_INSTALL.md +112 -0
- package/deploy/docker-compose.yml +42 -0
- package/deploy/systemd/README.md +46 -0
- package/deploy/systemd/agent-mockingbird.service +28 -0
- package/deploy/systemd/opencode.service +25 -0
- package/docs/legacy-config-ui-reference.md +51 -0
- package/docs/memory-e2e-trace-2026-03-04.md +63 -0
- package/docs/memory-ops.md +96 -0
- package/docs/memory-runtime-contract.md +42 -0
- package/docs/memory-tuning-remote-2026-03-04.md +59 -0
- package/docs/opencode-rebase-workflow-plan.md +614 -0
- package/docs/opencode-startup-sync-plan.md +94 -0
- package/docs/vendor-opencode.md +41 -0
- package/drizzle/0000_famous_turbo.sql +49 -0
- package/drizzle/0001_cron_memory_aux.sql +160 -0
- package/drizzle/0002_runtime_session_bindings.sql +28 -0
- package/drizzle/0003_background_runs.sql +27 -0
- package/drizzle/0004_memory_open_write.sql +63 -0
- package/drizzle/0005_signal_channel.sql +47 -0
- package/drizzle/0006_usage_event_dimensions.sql +7 -0
- package/drizzle/meta/0000_snapshot.json +341 -0
- package/drizzle/meta/_journal.json +55 -0
- package/drizzle.config.ts +14 -0
- package/eslint.config.mjs +77 -0
- package/knip.json +18 -0
- package/memory/2026-03-04.md +4 -0
- package/opencode.lock.json +16 -0
- package/package.json +67 -0
- package/packages/agent-mockingbird-installer/README.md +31 -0
- package/packages/agent-mockingbird-installer/bin/agent-mockingbird-installer.mjs +44 -0
- package/packages/agent-mockingbird-installer/opencode.lock.json +16 -0
- package/packages/agent-mockingbird-installer/package.json +23 -0
- package/packages/contracts/package.json +19 -0
- package/packages/contracts/src/agentTypes.ts +122 -0
- package/packages/contracts/src/cron.ts +146 -0
- package/packages/contracts/src/dashboard.ts +378 -0
- package/packages/contracts/src/index.ts +3 -0
- package/packages/contracts/tsconfig.json +4 -0
- package/patches/opencode/0001-Wafflebot-OpenCode-baseline.patch +2341 -0
- package/patches/opencode/0002-Fix-OpenCode-web-entry-and-settings-icons.patch +104 -0
- package/patches/opencode/0003-fix-app-remove-duplicate-sidebar-mount.patch +32 -0
- package/patches/opencode/0004-Add-heartbeat-settings-and-usage-nav.patch +506 -0
- package/patches/opencode/0005-Use-chart-icon-for-usage-nav.patch +38 -0
- package/patches/opencode/0006-Modernize-cron-settings.patch +399 -0
- package/patches/opencode/0007-Rename-waffle-namespaces-to-mockingbird.patch +1110 -0
- package/patches/opencode/0008-Remove-cron-contract-section.patch +178 -0
- package/patches/opencode/0009-Rework-cron-tab-as-operations-console.patch +414 -0
- package/patches/opencode/0010-Refine-heartbeat-settings-controls.patch +208 -0
- package/runtime-assets/opencode-config/opencode.jsonc +25 -0
- package/runtime-assets/opencode-config/package.json +5 -0
- package/runtime-assets/opencode-config/plugins/agent-mockingbird.ts +715 -0
- package/runtime-assets/workspace/.agents/skills/config-auditor/SKILL.md +25 -0
- package/runtime-assets/workspace/.agents/skills/config-editor/SKILL.md +24 -0
- package/runtime-assets/workspace/.agents/skills/cron-manager/SKILL.md +57 -0
- package/runtime-assets/workspace/.agents/skills/memory-ops/SKILL.md +120 -0
- package/runtime-assets/workspace/.agents/skills/runtime-diagnose/SKILL.md +25 -0
- package/runtime-assets/workspace/AGENTS.md +56 -0
- package/runtime-assets/workspace/MEMORY.md +4 -0
- package/scripts/build-release-bundle.sh +66 -0
- package/scripts/check-ship.ts +383 -0
- package/scripts/dev-opencode.sh +17 -0
- package/scripts/dev-stack-opencode.sh +15 -0
- package/scripts/dev-stack.sh +61 -0
- package/scripts/install-systemd.sh +87 -0
- package/scripts/memory-e2e.sh +76 -0
- package/scripts/memory-trace-e2e.sh +141 -0
- package/scripts/migrate-opencode-env.ts +108 -0
- package/scripts/onboard/bootstrap.sh +32 -0
- package/scripts/opencode-swap.ts +78 -0
- package/scripts/opencode-sync.ts +715 -0
- package/scripts/runtime-assets-sync.mjs +83 -0
- package/scripts/setup-git-hooks.ts +39 -0
- package/tsconfig.json +45 -0
- package/tui.json +98 -0
- package/turbo.json +36 -0
- package/vendor/OPENCODE_VENDOR.md +13 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getUsageDashboardSnapshot } from "../../db/repository";
|
|
5
|
+
import { resolveAppDistDir } from "../../paths";
|
|
6
|
+
|
|
7
|
+
interface UsageDashboardRangeQuery {
|
|
8
|
+
startAt: number | null;
|
|
9
|
+
endAtExclusive: number | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseOptionalMillis(value: string | null): number | null {
|
|
13
|
+
if (value === null || value === "") return null;
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) return null;
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseRangeQuery(url: URL): UsageDashboardRangeQuery | Response {
|
|
20
|
+
const startAt = parseOptionalMillis(url.searchParams.get("startAt"));
|
|
21
|
+
const endAtExclusive = parseOptionalMillis(url.searchParams.get("endAtExclusive"));
|
|
22
|
+
|
|
23
|
+
if (startAt !== null && endAtExclusive !== null && startAt >= endAtExclusive) {
|
|
24
|
+
return Response.json(
|
|
25
|
+
{ error: "startAt must be earlier than endAtExclusive" },
|
|
26
|
+
{ status: 400 },
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { startAt, endAtExclusive };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getOpenCodeStylesheetLinks() {
|
|
34
|
+
const appDistDir = resolveAppDistDir();
|
|
35
|
+
if (!appDistDir) return "";
|
|
36
|
+
const indexPath = path.join(appDistDir, "index.html");
|
|
37
|
+
if (!existsSync(indexPath)) return "";
|
|
38
|
+
const html = readFileSync(indexPath, "utf8");
|
|
39
|
+
const hrefs = [...html.matchAll(/<link\s+rel="stylesheet"[^>]*href="([^"]+)"/g)].map(match => match[1]);
|
|
40
|
+
return hrefs.map(href => `<link rel="stylesheet" crossorigin href="${href}">`).join("\n ");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function usagePageHtml() {
|
|
44
|
+
const stylesheetLinks = getOpenCodeStylesheetLinks();
|
|
45
|
+
return `<!doctype html>
|
|
46
|
+
<html lang="en" style="background-color: var(--background-base)">
|
|
47
|
+
<head>
|
|
48
|
+
<meta charset="utf-8" />
|
|
49
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
50
|
+
<title>Usage</title>
|
|
51
|
+
<link rel="icon" type="image/png" href="/favicon-96x96-v3.png" sizes="96x96" />
|
|
52
|
+
<link rel="icon" type="image/svg+xml" href="/favicon-v3.svg" />
|
|
53
|
+
<link rel="shortcut icon" href="/favicon-v3.ico" />
|
|
54
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-v3.png" />
|
|
55
|
+
<meta name="theme-color" content="#F8F7F7" />
|
|
56
|
+
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
|
57
|
+
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
|
|
58
|
+
${stylesheetLinks}
|
|
59
|
+
<style>
|
|
60
|
+
:root {
|
|
61
|
+
color-scheme: light dark;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
body {
|
|
65
|
+
margin: 0;
|
|
66
|
+
background: var(--background-base);
|
|
67
|
+
color: var(--text-base);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.usage-shell {
|
|
71
|
+
min-height: 100dvh;
|
|
72
|
+
background:
|
|
73
|
+
linear-gradient(180deg, color-mix(in srgb, var(--background-base) 82%, transparent), var(--background-base)),
|
|
74
|
+
radial-gradient(circle at top left, color-mix(in srgb, var(--surface-interactive-base) 70%, transparent), transparent 32rem);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.usage-main {
|
|
78
|
+
min-width: 0;
|
|
79
|
+
padding: 1rem;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.usage-content {
|
|
83
|
+
max-width: min(1680px, calc(100vw - 2rem));
|
|
84
|
+
margin: 0 auto;
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
gap: 1rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.usage-card {
|
|
91
|
+
border: 1px solid var(--border-weak-base);
|
|
92
|
+
border-radius: 0.875rem;
|
|
93
|
+
background: var(--surface-raised-base);
|
|
94
|
+
box-shadow: var(--shadow-xs);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.usage-header {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: space-between;
|
|
101
|
+
gap: 1rem;
|
|
102
|
+
padding: 1rem 1.1rem;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.usage-header-copy {
|
|
106
|
+
min-width: 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.usage-back-link {
|
|
110
|
+
display: inline-flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
gap: 0.45rem;
|
|
113
|
+
text-decoration: none;
|
|
114
|
+
color: var(--text-weak);
|
|
115
|
+
border: 1px solid transparent;
|
|
116
|
+
border-radius: 999px;
|
|
117
|
+
padding: 0.45rem 0.7rem;
|
|
118
|
+
transition: color 120ms ease, background-color 120ms ease, border-color 120ms ease;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.usage-back-link:hover {
|
|
122
|
+
color: var(--text-strong);
|
|
123
|
+
background: var(--surface-raised-base-hover);
|
|
124
|
+
border-color: var(--border-weak-base);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.usage-toolbar-card {
|
|
128
|
+
padding: 1rem 1.1rem;
|
|
129
|
+
display: flex;
|
|
130
|
+
flex-direction: column;
|
|
131
|
+
gap: 0.9rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.usage-filter-form {
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: end;
|
|
137
|
+
justify-content: space-between;
|
|
138
|
+
gap: 1rem;
|
|
139
|
+
flex-wrap: wrap;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.usage-filter-fields {
|
|
143
|
+
display: flex;
|
|
144
|
+
gap: 0.75rem;
|
|
145
|
+
flex-wrap: wrap;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.usage-filter-field {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
gap: 0.4rem;
|
|
152
|
+
min-width: min(100%, 13rem);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.usage-date-input {
|
|
156
|
+
appearance: none;
|
|
157
|
+
min-height: 2.75rem;
|
|
158
|
+
border: 1px solid var(--border-weak-base);
|
|
159
|
+
border-radius: 0.75rem;
|
|
160
|
+
background: var(--surface-base);
|
|
161
|
+
color: var(--text-strong);
|
|
162
|
+
padding: 0.7rem 0.8rem;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.usage-date-input:focus {
|
|
166
|
+
outline: 2px solid color-mix(in srgb, var(--border-weak-selected) 45%, transparent);
|
|
167
|
+
outline-offset: 1px;
|
|
168
|
+
border-color: var(--border-weak-selected);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.usage-filter-actions {
|
|
172
|
+
display: flex;
|
|
173
|
+
gap: 0.55rem;
|
|
174
|
+
flex-wrap: wrap;
|
|
175
|
+
align-items: center;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.usage-filter-button {
|
|
179
|
+
border: 1px solid var(--border-weak-base);
|
|
180
|
+
border-radius: 999px;
|
|
181
|
+
background: var(--surface-base);
|
|
182
|
+
color: var(--text-base);
|
|
183
|
+
padding: 0.7rem 0.95rem;
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.usage-filter-button:hover {
|
|
189
|
+
background: var(--surface-raised-base-hover);
|
|
190
|
+
border-color: var(--border-weak-selected);
|
|
191
|
+
color: var(--text-strong);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.usage-filter-button--primary {
|
|
195
|
+
background: var(--surface-base-active);
|
|
196
|
+
border-color: var(--border-weak-selected);
|
|
197
|
+
color: var(--text-strong);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.usage-filter-summary {
|
|
201
|
+
display: flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
justify-content: space-between;
|
|
204
|
+
gap: 0.75rem;
|
|
205
|
+
flex-wrap: wrap;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.usage-filter-status {
|
|
209
|
+
min-height: 1rem;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.usage-metrics {
|
|
213
|
+
display: grid;
|
|
214
|
+
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
215
|
+
gap: 0.8rem;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.usage-metric {
|
|
219
|
+
padding: 1rem 1.1rem;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.usage-metric-value {
|
|
223
|
+
margin-top: 0.35rem;
|
|
224
|
+
color: var(--text-strong);
|
|
225
|
+
font-size: 1.4rem;
|
|
226
|
+
letter-spacing: -0.03em;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.usage-panel {
|
|
230
|
+
padding: 1rem 1.1rem;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.usage-table-wrap {
|
|
234
|
+
overflow-x: auto;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.usage-table {
|
|
238
|
+
width: 100%;
|
|
239
|
+
min-width: 42rem;
|
|
240
|
+
border-collapse: collapse;
|
|
241
|
+
table-layout: auto;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.usage-table th,
|
|
245
|
+
.usage-table td {
|
|
246
|
+
padding: 0.7rem 0.75rem;
|
|
247
|
+
border-bottom: 1px solid var(--border-weak-base);
|
|
248
|
+
text-align: left;
|
|
249
|
+
vertical-align: top;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.usage-table th:last-child,
|
|
253
|
+
.usage-table td:last-child {
|
|
254
|
+
text-align: right;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.usage-table th {
|
|
258
|
+
white-space: nowrap;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.usage-table td {
|
|
262
|
+
overflow-wrap: anywhere;
|
|
263
|
+
word-break: break-word;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.usage-table__cell--numeric,
|
|
267
|
+
.usage-table__cell--compact {
|
|
268
|
+
white-space: nowrap;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.usage-table__cell--numeric {
|
|
272
|
+
text-align: right;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.usage-table__cell--session {
|
|
276
|
+
min-width: 20rem;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.usage-table__cell--model {
|
|
280
|
+
min-width: 24rem;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.usage-table__cell--when {
|
|
284
|
+
min-width: 12rem;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.usage-table--provider th:first-child,
|
|
288
|
+
.usage-table--provider td:first-child {
|
|
289
|
+
width: 100%;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.usage-table--model th:first-child,
|
|
293
|
+
.usage-table--model td:first-child {
|
|
294
|
+
width: 100%;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.usage-table--recent {
|
|
298
|
+
min-width: 78rem;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.usage-table--recent th:first-child,
|
|
302
|
+
.usage-table--recent td:first-child {
|
|
303
|
+
width: 30%;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.usage-table--recent th:nth-child(2),
|
|
307
|
+
.usage-table--recent td:nth-child(2) {
|
|
308
|
+
width: 32%;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.usage-table--recent th:nth-child(3),
|
|
312
|
+
.usage-table--recent td:nth-child(3) {
|
|
313
|
+
width: 18%;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.usage-table tbody tr:hover {
|
|
317
|
+
background: var(--surface-raised-base-hover);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.usage-empty {
|
|
321
|
+
padding: 0.75rem 0 0.25rem;
|
|
322
|
+
color: var(--text-weak);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
@media (max-width: 1080px) {
|
|
326
|
+
.usage-metrics {
|
|
327
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@media (max-width: 720px) {
|
|
332
|
+
.usage-main {
|
|
333
|
+
padding: 0.75rem;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.usage-content {
|
|
337
|
+
max-width: none;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.usage-header {
|
|
341
|
+
flex-direction: column;
|
|
342
|
+
align-items: flex-start;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.usage-filter-form {
|
|
346
|
+
align-items: stretch;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.usage-filter-fields,
|
|
350
|
+
.usage-filter-field,
|
|
351
|
+
.usage-filter-actions {
|
|
352
|
+
width: 100%;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.usage-filter-button {
|
|
356
|
+
flex: 1 1 10rem;
|
|
357
|
+
justify-content: center;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.usage-metrics {
|
|
361
|
+
grid-template-columns: 1fr;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
</style>
|
|
365
|
+
</head>
|
|
366
|
+
<body class="antialiased overscroll-none text-12-regular bg-background-base text-text-base">
|
|
367
|
+
<div class="usage-shell">
|
|
368
|
+
<main class="usage-main">
|
|
369
|
+
<div class="usage-content">
|
|
370
|
+
<section class="usage-card usage-header">
|
|
371
|
+
<div class="usage-header-copy flex flex-col gap-1.5">
|
|
372
|
+
<div class="text-16-medium text-text-strong">Runtime Usage Dashboard</div>
|
|
373
|
+
<div class="text-12-regular text-text-weak">Calendar-based usage slices, grouped totals, and the latest deltas in the selected range.</div>
|
|
374
|
+
</div>
|
|
375
|
+
<a class="usage-back-link text-12-medium" href="/" data-action="usage-back-link" aria-label="Back to app">
|
|
376
|
+
<span aria-hidden="true">←</span>
|
|
377
|
+
<span>Back to app</span>
|
|
378
|
+
</a>
|
|
379
|
+
</section>
|
|
380
|
+
|
|
381
|
+
<section class="usage-card usage-toolbar-card">
|
|
382
|
+
<form class="usage-filter-form" id="usage-filter-form">
|
|
383
|
+
<div class="usage-filter-fields">
|
|
384
|
+
<label class="usage-filter-field">
|
|
385
|
+
<span class="text-12-medium text-text-weak">Start date</span>
|
|
386
|
+
<input class="usage-date-input text-13-regular" id="usage-start-date" type="date" />
|
|
387
|
+
</label>
|
|
388
|
+
<label class="usage-filter-field">
|
|
389
|
+
<span class="text-12-medium text-text-weak">End date</span>
|
|
390
|
+
<input class="usage-date-input text-13-regular" id="usage-end-date" type="date" />
|
|
391
|
+
</label>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<div class="usage-filter-actions">
|
|
395
|
+
<button class="usage-filter-button usage-filter-button--primary text-13-medium" type="submit">Apply range</button>
|
|
396
|
+
<button class="usage-filter-button text-13-medium" type="button" id="usage-month-to-date">Month to date</button>
|
|
397
|
+
<button class="usage-filter-button text-13-medium" type="button" id="usage-all-time">All time</button>
|
|
398
|
+
</div>
|
|
399
|
+
</form>
|
|
400
|
+
|
|
401
|
+
<div class="usage-filter-summary">
|
|
402
|
+
<div>
|
|
403
|
+
<div class="text-12-medium text-text-weak">Selection</div>
|
|
404
|
+
<div class="text-13-regular text-text-strong" id="usage-range-summary"></div>
|
|
405
|
+
</div>
|
|
406
|
+
<div class="usage-filter-status text-12-regular text-text-danger" id="usage-filter-status" role="status" aria-live="polite"></div>
|
|
407
|
+
</div>
|
|
408
|
+
</section>
|
|
409
|
+
|
|
410
|
+
<section class="usage-metrics" id="metrics"></section>
|
|
411
|
+
|
|
412
|
+
<section class="usage-card usage-panel">
|
|
413
|
+
<div class="flex items-center justify-between gap-3 mb-3">
|
|
414
|
+
<div>
|
|
415
|
+
<div class="text-14-medium text-text-strong">By Provider</div>
|
|
416
|
+
<div class="text-12-regular text-text-weak">Requests, input, output, total tokens, and estimated cost.</div>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
<div id="providers"></div>
|
|
420
|
+
</section>
|
|
421
|
+
|
|
422
|
+
<section class="usage-card usage-panel">
|
|
423
|
+
<div class="flex items-center justify-between gap-3 mb-3">
|
|
424
|
+
<div>
|
|
425
|
+
<div class="text-14-medium text-text-strong">By Model</div>
|
|
426
|
+
<div class="text-12-regular text-text-weak">Qualified model references tracked per event when available.</div>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
<div id="models"></div>
|
|
430
|
+
</section>
|
|
431
|
+
|
|
432
|
+
<section class="usage-card usage-panel">
|
|
433
|
+
<div class="flex items-center justify-between gap-3 mb-3">
|
|
434
|
+
<div>
|
|
435
|
+
<div class="text-14-medium text-text-strong">Recent Activity</div>
|
|
436
|
+
<div class="text-12-regular text-text-weak">The 50 latest usage deltas for the selected date range.</div>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
<div id="recent"></div>
|
|
440
|
+
</section>
|
|
441
|
+
</div>
|
|
442
|
+
</main>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<script type="module">
|
|
446
|
+
const metricsEl = document.getElementById("metrics");
|
|
447
|
+
const providersEl = document.getElementById("providers");
|
|
448
|
+
const modelsEl = document.getElementById("models");
|
|
449
|
+
const recentEl = document.getElementById("recent");
|
|
450
|
+
const backLink = document.querySelector("[data-action='usage-back-link']");
|
|
451
|
+
const filterForm = document.getElementById("usage-filter-form");
|
|
452
|
+
const startDateInput = document.getElementById("usage-start-date");
|
|
453
|
+
const endDateInput = document.getElementById("usage-end-date");
|
|
454
|
+
const monthToDateButton = document.getElementById("usage-month-to-date");
|
|
455
|
+
const allTimeButton = document.getElementById("usage-all-time");
|
|
456
|
+
const rangeSummaryEl = document.getElementById("usage-range-summary");
|
|
457
|
+
const filterStatusEl = document.getElementById("usage-filter-status");
|
|
458
|
+
|
|
459
|
+
function formatNumber(value) {
|
|
460
|
+
return new Intl.NumberFormat("en-US").format(Number(value || 0));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function formatUsd(value) {
|
|
464
|
+
return new Intl.NumberFormat("en-US", {
|
|
465
|
+
style: "currency",
|
|
466
|
+
currency: "USD",
|
|
467
|
+
minimumFractionDigits: 4,
|
|
468
|
+
maximumFractionDigits: 4,
|
|
469
|
+
}).format(Number(value || 0));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function escapeHtml(value) {
|
|
473
|
+
return String(value ?? "")
|
|
474
|
+
.replaceAll("&", "&")
|
|
475
|
+
.replaceAll("<", "<")
|
|
476
|
+
.replaceAll(">", ">")
|
|
477
|
+
.replaceAll('"', """)
|
|
478
|
+
.replaceAll("'", "'");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function formatDateInput(date) {
|
|
482
|
+
const year = date.getFullYear();
|
|
483
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
484
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
485
|
+
return year + "-" + month + "-" + day;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function parseDateValue(value) {
|
|
489
|
+
if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value || "")) return null;
|
|
490
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
491
|
+
const start = new Date(year, month - 1, day);
|
|
492
|
+
if (
|
|
493
|
+
start.getFullYear() !== year ||
|
|
494
|
+
start.getMonth() !== month - 1 ||
|
|
495
|
+
start.getDate() !== day
|
|
496
|
+
) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
startAt: start.getTime(),
|
|
502
|
+
endAtExclusive: new Date(year, month - 1, day + 1).getTime(),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function getMonthToDateSelection() {
|
|
507
|
+
const now = new Date();
|
|
508
|
+
return {
|
|
509
|
+
startDate: formatDateInput(new Date(now.getFullYear(), now.getMonth(), 1)),
|
|
510
|
+
endDate: formatDateInput(now),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function readSelectionFromUrl() {
|
|
515
|
+
const url = new URL(window.location.href);
|
|
516
|
+
const startDate = url.searchParams.get("start");
|
|
517
|
+
const endDate = url.searchParams.get("end");
|
|
518
|
+
|
|
519
|
+
if (!startDate && !endDate) return getMonthToDateSelection();
|
|
520
|
+
if (!startDate || !endDate) return getMonthToDateSelection();
|
|
521
|
+
if (!parseDateValue(startDate) || !parseDateValue(endDate) || startDate > endDate) {
|
|
522
|
+
return getMonthToDateSelection();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return { startDate, endDate };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
let currentSelection = readSelectionFromUrl();
|
|
529
|
+
|
|
530
|
+
function syncInputsFromSelection() {
|
|
531
|
+
startDateInput.value = currentSelection.startDate || "";
|
|
532
|
+
endDateInput.value = currentSelection.endDate || "";
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function syncUrlFromSelection() {
|
|
536
|
+
const url = new URL(window.location.href);
|
|
537
|
+
if (currentSelection.startDate && currentSelection.endDate) {
|
|
538
|
+
url.searchParams.set("start", currentSelection.startDate);
|
|
539
|
+
url.searchParams.set("end", currentSelection.endDate);
|
|
540
|
+
} else {
|
|
541
|
+
url.searchParams.delete("start");
|
|
542
|
+
url.searchParams.delete("end");
|
|
543
|
+
}
|
|
544
|
+
window.history.replaceState({}, "", url);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function formatHumanDate(value) {
|
|
548
|
+
const parsed = parseDateValue(value);
|
|
549
|
+
if (!parsed) return value;
|
|
550
|
+
return new Date(parsed.startAt).toLocaleDateString(undefined, {
|
|
551
|
+
year: "numeric",
|
|
552
|
+
month: "short",
|
|
553
|
+
day: "numeric",
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function renderSelectionSummary() {
|
|
558
|
+
if (!currentSelection.startDate || !currentSelection.endDate) {
|
|
559
|
+
rangeSummaryEl.textContent = "All recorded usage";
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (currentSelection.startDate === currentSelection.endDate) {
|
|
564
|
+
rangeSummaryEl.textContent = formatHumanDate(currentSelection.startDate);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
rangeSummaryEl.textContent = formatHumanDate(currentSelection.startDate) + " to " + formatHumanDate(currentSelection.endDate);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function setFilterStatus(message) {
|
|
572
|
+
filterStatusEl.textContent = message || "";
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function buildRequestUrl() {
|
|
576
|
+
const url = new URL("/api/usage/dashboard", window.location.origin);
|
|
577
|
+
if (currentSelection.startDate && currentSelection.endDate) {
|
|
578
|
+
const start = parseDateValue(currentSelection.startDate);
|
|
579
|
+
const end = parseDateValue(currentSelection.endDate);
|
|
580
|
+
if (start && end) {
|
|
581
|
+
url.searchParams.set("startAt", String(start.startAt));
|
|
582
|
+
url.searchParams.set("endAtExclusive", String(end.endAtExclusive));
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return url.toString();
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function renderMetrics(payload) {
|
|
589
|
+
const metrics = [
|
|
590
|
+
["Requests", formatNumber(payload.totals.requestCount)],
|
|
591
|
+
["Input Tokens", formatNumber(payload.totals.inputTokens)],
|
|
592
|
+
["Output Tokens", formatNumber(payload.totals.outputTokens)],
|
|
593
|
+
["Total Tokens", formatNumber(payload.totals.totalTokens)],
|
|
594
|
+
["Estimated Cost", formatUsd(payload.totals.estimatedCostUsd)],
|
|
595
|
+
];
|
|
596
|
+
metricsEl.innerHTML = metrics.map(([label, value]) => \`
|
|
597
|
+
<article class="usage-card usage-metric">
|
|
598
|
+
<div class="text-12-medium text-text-weak">\${escapeHtml(label)}</div>
|
|
599
|
+
<div class="usage-metric-value text-20-medium">\${escapeHtml(value)}</div>
|
|
600
|
+
</article>
|
|
601
|
+
\`).join("");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function renderTable(target, rows, columns, className = "") {
|
|
605
|
+
if (!rows.length) {
|
|
606
|
+
target.innerHTML = '<div class="usage-empty text-13-regular">No usage recorded for this range.</div>';
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const head = columns
|
|
611
|
+
.map(column => \`<th class="text-12-medium text-text-weak \${escapeHtml(column.headerClassName || "")}">\${escapeHtml(column.label)}</th>\`)
|
|
612
|
+
.join("");
|
|
613
|
+
const body = rows
|
|
614
|
+
.map(row => \`<tr>\${columns.map(column => \`<td class="text-13-regular \${escapeHtml(column.className || "")}">\${escapeHtml(column.render(row))}</td>\`).join("")}</tr>\`)
|
|
615
|
+
.join("");
|
|
616
|
+
|
|
617
|
+
target.innerHTML = \`<div class="usage-table-wrap"><table class="usage-table \${escapeHtml(className)}"><thead><tr>\${head}</tr></thead><tbody>\${body}</tbody></table></div>\`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function renderRecent(rows) {
|
|
621
|
+
renderTable(
|
|
622
|
+
recentEl,
|
|
623
|
+
rows,
|
|
624
|
+
[
|
|
625
|
+
{
|
|
626
|
+
label: "Session",
|
|
627
|
+
render: row => row.sessionTitle || row.sessionId || "Unbound usage event",
|
|
628
|
+
className: "text-text-strong usage-table__cell--session",
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
label: "Model",
|
|
632
|
+
render: row => (row.providerId || "unknown") + "/" + (row.modelId || "unknown"),
|
|
633
|
+
className: "font-mono text-text-weak usage-table__cell--model",
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
label: "When",
|
|
637
|
+
render: row => new Date(row.createdAt).toLocaleString(),
|
|
638
|
+
className: "text-text-weak usage-table__cell--when",
|
|
639
|
+
},
|
|
640
|
+
{ label: "Req", render: row => formatNumber(row.requestCount), className: "usage-table__cell--numeric" },
|
|
641
|
+
{ label: "In", render: row => formatNumber(row.inputTokens), className: "usage-table__cell--numeric" },
|
|
642
|
+
{ label: "Out", render: row => formatNumber(row.outputTokens), className: "usage-table__cell--numeric" },
|
|
643
|
+
{ label: "Total", render: row => formatNumber(row.totalTokens), className: "usage-table__cell--numeric" },
|
|
644
|
+
{
|
|
645
|
+
label: "Cost",
|
|
646
|
+
render: row => formatUsd(row.estimatedCostUsd),
|
|
647
|
+
className: "text-text-strong usage-table__cell--numeric",
|
|
648
|
+
},
|
|
649
|
+
],
|
|
650
|
+
"usage-table--recent",
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function loadUsage() {
|
|
655
|
+
const response = await fetch(buildRequestUrl());
|
|
656
|
+
if (!response.ok) {
|
|
657
|
+
const payload = await response.json().catch(() => null);
|
|
658
|
+
throw new Error(payload?.error || "Failed to load usage.");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const payload = await response.json();
|
|
662
|
+
renderMetrics(payload);
|
|
663
|
+
renderTable(providersEl, payload.providers, [
|
|
664
|
+
{ label: "Provider", render: row => row.providerId, className: "font-mono text-text-strong" },
|
|
665
|
+
{ label: "Req", render: row => formatNumber(row.requestCount), className: "usage-table__cell--numeric" },
|
|
666
|
+
{ label: "In", render: row => formatNumber(row.inputTokens), className: "usage-table__cell--numeric" },
|
|
667
|
+
{ label: "Out", render: row => formatNumber(row.outputTokens), className: "usage-table__cell--numeric" },
|
|
668
|
+
{ label: "Total", render: row => formatNumber(row.totalTokens), className: "usage-table__cell--numeric" },
|
|
669
|
+
{ label: "Cost", render: row => formatUsd(row.estimatedCostUsd), className: "text-text-strong usage-table__cell--numeric" },
|
|
670
|
+
], "usage-table--provider");
|
|
671
|
+
renderTable(modelsEl, payload.models, [
|
|
672
|
+
{ label: "Model", render: row => row.providerId + "/" + row.modelId, className: "font-mono text-text-strong usage-table__cell--model" },
|
|
673
|
+
{ label: "Req", render: row => formatNumber(row.requestCount), className: "usage-table__cell--numeric" },
|
|
674
|
+
{ label: "In", render: row => formatNumber(row.inputTokens), className: "usage-table__cell--numeric" },
|
|
675
|
+
{ label: "Out", render: row => formatNumber(row.outputTokens), className: "usage-table__cell--numeric" },
|
|
676
|
+
{ label: "Total", render: row => formatNumber(row.totalTokens), className: "usage-table__cell--numeric" },
|
|
677
|
+
{ label: "Cost", render: row => formatUsd(row.estimatedCostUsd), className: "text-text-strong usage-table__cell--numeric" },
|
|
678
|
+
], "usage-table--model");
|
|
679
|
+
renderRecent(payload.recent);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function refreshUsage() {
|
|
683
|
+
setFilterStatus("");
|
|
684
|
+
renderSelectionSummary();
|
|
685
|
+
syncUrlFromSelection();
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
await loadUsage();
|
|
689
|
+
} catch (error) {
|
|
690
|
+
setFilterStatus(error instanceof Error ? error.message : "Failed to load usage.");
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
filterForm?.addEventListener("submit", event => {
|
|
695
|
+
event.preventDefault();
|
|
696
|
+
const startDate = startDateInput.value;
|
|
697
|
+
const endDate = endDateInput.value;
|
|
698
|
+
|
|
699
|
+
if (!startDate || !endDate) {
|
|
700
|
+
setFilterStatus("Choose both a start date and an end date.");
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (!parseDateValue(startDate) || !parseDateValue(endDate)) {
|
|
705
|
+
setFilterStatus("Enter valid calendar dates.");
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (startDate > endDate) {
|
|
710
|
+
setFilterStatus("Start date must be on or before the end date.");
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
currentSelection = { startDate, endDate };
|
|
715
|
+
void refreshUsage();
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
monthToDateButton?.addEventListener("click", () => {
|
|
719
|
+
currentSelection = getMonthToDateSelection();
|
|
720
|
+
syncInputsFromSelection();
|
|
721
|
+
void refreshUsage();
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
allTimeButton?.addEventListener("click", () => {
|
|
725
|
+
currentSelection = { startDate: null, endDate: null };
|
|
726
|
+
syncInputsFromSelection();
|
|
727
|
+
void refreshUsage();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
backLink?.addEventListener("click", event => {
|
|
731
|
+
const referrer = document.referrer ? new URL(document.referrer) : null;
|
|
732
|
+
const sameOriginReferrer = referrer && referrer.origin === window.location.origin;
|
|
733
|
+
if (sameOriginReferrer && window.history.length > 1) {
|
|
734
|
+
event.preventDefault();
|
|
735
|
+
window.history.back();
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
syncInputsFromSelection();
|
|
740
|
+
void refreshUsage();
|
|
741
|
+
window.setInterval(() => { void loadUsage().catch(error => setFilterStatus(error instanceof Error ? error.message : "Failed to load usage.")); }, 15000);
|
|
742
|
+
</script>
|
|
743
|
+
</body>
|
|
744
|
+
</html>`;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export function createUsageRoutes() {
|
|
748
|
+
return {
|
|
749
|
+
"/api/usage/dashboard": {
|
|
750
|
+
GET: (req: Request) => {
|
|
751
|
+
const url = new URL(req.url);
|
|
752
|
+
const query = parseRangeQuery(url);
|
|
753
|
+
if (query instanceof Response) return query;
|
|
754
|
+
return Response.json(getUsageDashboardSnapshot(query));
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
|
|
758
|
+
"/usage": {
|
|
759
|
+
GET: () =>
|
|
760
|
+
new Response(usagePageHtml(), {
|
|
761
|
+
headers: {
|
|
762
|
+
"content-type": "text/html; charset=utf-8",
|
|
763
|
+
},
|
|
764
|
+
}),
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
}
|