@agent-native/dispatch 0.2.6 → 0.2.7

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.
@@ -2,7 +2,7 @@ import { defineAction } from "@agent-native/core";
2
2
  import { z } from "zod";
3
3
  import { listDispatchUsageMetrics } from "../server/lib/usage-metrics-store.js";
4
4
  export default defineAction({
5
- description: "Get workspace-level LLM usage, estimated cost, users, app access, and recent activity metrics for Dispatch admins.",
5
+ description: "Get workspace-level LLM usage, spend or Builder.io credit spend, users, app access, and recent activity metrics for Dispatch admins.",
6
6
  schema: z.object({
7
7
  sinceDays: z.coerce
8
8
  .number()
@@ -1 +1 @@
1
- {"version":3,"file":"list-dispatch-usage-metrics.js","sourceRoot":"","sources":["../../src/actions/list-dispatch-usage-metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,wBAAwB,EAAE,MAAM,sCAAsC,CAAC;AAEhF,eAAe,YAAY,CAAC;IAC1B,WAAW,EACT,oHAAoH;IACtH,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,SAAS,EAAE,CAAC,CAAC,MAAM;aAChB,MAAM,EAAE;aACR,GAAG,EAAE;aACL,GAAG,CAAC,CAAC,CAAC;aACN,GAAG,CAAC,GAAG,CAAC;aACR,OAAO,CAAC,EAAE,CAAC;aACX,QAAQ,CAAC,0CAA0C,CAAC;KACxD,CAAC;IACF,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;IACvB,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,wBAAwB,CAAC,IAAI,CAAC;CACpD,CAAC,CAAC","sourcesContent":["import { defineAction } from \"@agent-native/core\";\nimport { z } from \"zod\";\nimport { listDispatchUsageMetrics } from \"../server/lib/usage-metrics-store.js\";\n\nexport default defineAction({\n description:\n \"Get workspace-level LLM usage, estimated cost, users, app access, and recent activity metrics for Dispatch admins.\",\n schema: z.object({\n sinceDays: z.coerce\n .number()\n .int()\n .min(1)\n .max(365)\n .default(30)\n .describe(\"Lookback window in days. Defaults to 30.\"),\n }),\n http: { method: \"GET\" },\n run: async (args) => listDispatchUsageMetrics(args),\n});\n"]}
1
+ {"version":3,"file":"list-dispatch-usage-metrics.js","sourceRoot":"","sources":["../../src/actions/list-dispatch-usage-metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,wBAAwB,EAAE,MAAM,sCAAsC,CAAC;AAEhF,eAAe,YAAY,CAAC;IAC1B,WAAW,EACT,sIAAsI;IACxI,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,SAAS,EAAE,CAAC,CAAC,MAAM;aAChB,MAAM,EAAE;aACR,GAAG,EAAE;aACL,GAAG,CAAC,CAAC,CAAC;aACN,GAAG,CAAC,GAAG,CAAC;aACR,OAAO,CAAC,EAAE,CAAC;aACX,QAAQ,CAAC,0CAA0C,CAAC;KACxD,CAAC;IACF,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;IACvB,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,wBAAwB,CAAC,IAAI,CAAC;CACpD,CAAC,CAAC","sourcesContent":["import { defineAction } from \"@agent-native/core\";\nimport { z } from \"zod\";\nimport { listDispatchUsageMetrics } from \"../server/lib/usage-metrics-store.js\";\n\nexport default defineAction({\n description:\n \"Get workspace-level LLM usage, spend or Builder.io credit spend, users, app access, and recent activity metrics for Dispatch admins.\",\n schema: z.object({\n sinceDays: z.coerce\n .number()\n .int()\n .min(1)\n .max(365)\n .default(30)\n .describe(\"Lookback window in days. Defaults to 30.\"),\n }),\n http: { method: \"GET\" },\n run: async (args) => listDispatchUsageMetrics(args),\n});\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"view-screen.d.ts","sourceRoot":"","sources":["../../src/actions/view-screen.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;;AAeH,wBAyEG"}
1
+ {"version":3,"file":"view-screen.d.ts","sourceRoot":"","sources":["../../src/actions/view-screen.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;;AAeH,wBA0EG"}
@@ -48,6 +48,7 @@ export default defineAction({
48
48
  try {
49
49
  const metrics = await listDispatchUsageMetrics({ sinceDays: 30 });
50
50
  screen.usageMetrics = {
51
+ billing: metrics.billing,
51
52
  totals: metrics.totals,
52
53
  byApp: metrics.byApp.slice(0, 8),
53
54
  byUser: metrics.byUser.slice(0, 8),
@@ -1 +1 @@
1
- {"version":3,"file":"view-screen.js","sourceRoot":"","sources":["../../src/actions/view-screen.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * See what the user is currently looking at on screen.\n *\n * Reads and returns the current navigation state from application state.\n *\n * Usage:\n * pnpm action view-screen\n */\n\nimport { defineAction } from \"@agent-native/core\";\nimport { readAppState } from \"@agent-native/core/application-state\";\nimport { z } from \"zod\";\nimport { listOverview } from \"../server/lib/dispatch-store.js\";\nimport {\n listVaultOverview,\n listSecrets,\n listGrants,\n listRequests,\n} from \"../server/lib/vault-store.js\";\nimport { listWorkspaceApps } from \"../server/lib/app-creation-store.js\";\nimport { listDispatchUsageMetrics } from \"../server/lib/usage-metrics-store.js\";\n\nexport default defineAction({\n description:\n \"See what the user is currently looking at in the dispatch UI, including navigation state and a compact operational summary.\",\n schema: z.object({}),\n http: false,\n run: async () => {\n const [navigation, overview, vaultOverview] = await Promise.all([\n readAppState(\"navigation\"),\n listOverview(),\n listVaultOverview(),\n ]);\n\n const screen: Record<string, unknown> = {\n counts: { ...overview.counts, ...vaultOverview },\n approvalPolicy: overview.settings,\n };\n if (navigation) screen.navigation = navigation;\n if (navigation?.view === \"overview\") {\n screen.recentAudit = overview.recentAudit.slice(0, 5);\n screen.recentApprovals = overview.recentApprovals.slice(0, 5);\n }\n if (navigation?.view === \"destinations\") {\n screen.recentDestinations = overview.recentDestinations;\n }\n if (\n navigation?.view === \"overview\" ||\n navigation?.view === \"metrics\" ||\n navigation?.view === \"apps\" ||\n navigation?.view === \"new-app\"\n ) {\n screen.workspaceApps = await listWorkspaceApps({\n includeAgentCards: true,\n });\n }\n if (navigation?.view === \"metrics\") {\n try {\n const metrics = await listDispatchUsageMetrics({ sinceDays: 30 });\n screen.usageMetrics = {\n totals: metrics.totals,\n byApp: metrics.byApp.slice(0, 8),\n byUser: metrics.byUser.slice(0, 8),\n appAccess: metrics.appAccess\n .filter((app) => !app.isDispatch)\n .slice(0, 8),\n };\n } catch (error) {\n screen.usageMetricsError =\n error instanceof Error ? error.message : String(error);\n }\n }\n if (navigation?.view === \"vault\" || navigation?.view === \"new-app\") {\n const [secrets, grants, requests] = await Promise.all([\n listSecrets(),\n listGrants(),\n listRequests({ status: \"pending\" }),\n ]);\n screen.vaultSecrets = secrets.map((s) => ({\n id: s.id,\n name: s.name,\n credentialKey: s.credentialKey,\n provider: s.provider,\n }));\n screen.vaultActiveGrants = grants\n .filter((g) => g.status === \"active\")\n .map((g) => ({ secretId: g.secretId, appId: g.appId }));\n screen.vaultPendingRequests = requests;\n }\n\n if (Object.keys(screen).length === 0) {\n return \"No application state found. Is the app running?\";\n }\n return JSON.stringify(screen, null, 2);\n },\n});\n"]}
1
+ {"version":3,"file":"view-screen.js","sourceRoot":"","sources":["../../src/actions/view-screen.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * See what the user is currently looking at on screen.\n *\n * Reads and returns the current navigation state from application state.\n *\n * Usage:\n * pnpm action view-screen\n */\n\nimport { defineAction } from \"@agent-native/core\";\nimport { readAppState } from \"@agent-native/core/application-state\";\nimport { z } from \"zod\";\nimport { listOverview } from \"../server/lib/dispatch-store.js\";\nimport {\n listVaultOverview,\n listSecrets,\n listGrants,\n listRequests,\n} from \"../server/lib/vault-store.js\";\nimport { listWorkspaceApps } from \"../server/lib/app-creation-store.js\";\nimport { listDispatchUsageMetrics } from \"../server/lib/usage-metrics-store.js\";\n\nexport default defineAction({\n description:\n \"See what the user is currently looking at in the dispatch UI, including navigation state and a compact operational summary.\",\n schema: z.object({}),\n http: false,\n run: async () => {\n const [navigation, overview, vaultOverview] = await Promise.all([\n readAppState(\"navigation\"),\n listOverview(),\n listVaultOverview(),\n ]);\n\n const screen: Record<string, unknown> = {\n counts: { ...overview.counts, ...vaultOverview },\n approvalPolicy: overview.settings,\n };\n if (navigation) screen.navigation = navigation;\n if (navigation?.view === \"overview\") {\n screen.recentAudit = overview.recentAudit.slice(0, 5);\n screen.recentApprovals = overview.recentApprovals.slice(0, 5);\n }\n if (navigation?.view === \"destinations\") {\n screen.recentDestinations = overview.recentDestinations;\n }\n if (\n navigation?.view === \"overview\" ||\n navigation?.view === \"metrics\" ||\n navigation?.view === \"apps\" ||\n navigation?.view === \"new-app\"\n ) {\n screen.workspaceApps = await listWorkspaceApps({\n includeAgentCards: true,\n });\n }\n if (navigation?.view === \"metrics\") {\n try {\n const metrics = await listDispatchUsageMetrics({ sinceDays: 30 });\n screen.usageMetrics = {\n billing: metrics.billing,\n totals: metrics.totals,\n byApp: metrics.byApp.slice(0, 8),\n byUser: metrics.byUser.slice(0, 8),\n appAccess: metrics.appAccess\n .filter((app) => !app.isDispatch)\n .slice(0, 8),\n };\n } catch (error) {\n screen.usageMetricsError =\n error instanceof Error ? error.message : String(error);\n }\n }\n if (navigation?.view === \"vault\" || navigation?.view === \"new-app\") {\n const [secrets, grants, requests] = await Promise.all([\n listSecrets(),\n listGrants(),\n listRequests({ status: \"pending\" }),\n ]);\n screen.vaultSecrets = secrets.map((s) => ({\n id: s.id,\n name: s.name,\n credentialKey: s.credentialKey,\n provider: s.provider,\n }));\n screen.vaultActiveGrants = grants\n .filter((g) => g.status === \"active\")\n .map((g) => ({ secretId: g.secretId, appId: g.appId }));\n screen.vaultPendingRequests = requests;\n }\n\n if (Object.keys(screen).length === 0) {\n return \"No application state found. Is the app running?\";\n }\n return JSON.stringify(screen, null, 2);\n },\n});\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/metrics.tsx"],"names":[],"mappings":"AAmBA,wBAAgB,IAAI;;IAEnB;AA6gBD,MAAM,CAAC,OAAO,UAAU,YAAY,4CAwHnC"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/metrics.tsx"],"names":[],"mappings":"AAmBA,wBAAgB,IAAI;;IAEnB;AAsmBD,MAAM,CAAC,OAAO,UAAU,YAAY,4CAsInC"}
@@ -12,7 +12,33 @@ export function meta() {
12
12
  return [{ title: "Metrics — Dispatch" }];
13
13
  }
14
14
  const RANGES = [7, 30, 90];
15
- function formatCost(cents) {
15
+ const USD_BILLING = {
16
+ unit: "usd",
17
+ label: "Estimated spend",
18
+ shortLabel: "Cost",
19
+ source: "estimated-provider-cost",
20
+ };
21
+ function displayAmountFromCostCents(cents, billing) {
22
+ if (billing.unit !== "builder-credits")
23
+ return cents;
24
+ const margin = billing.hardCostMarginMultiplier ?? 1.25;
25
+ const creditsPerUsd = billing.creditsPerUsd ?? 20;
26
+ const credits = (cents / 100) * margin * creditsPerUsd;
27
+ return credits <= 0 ? 0 : Math.ceil(credits * 1000) / 1000;
28
+ }
29
+ function formatCredits(credits) {
30
+ if (!Number.isFinite(credits) || credits === 0)
31
+ return "0 credits";
32
+ const maximumFractionDigits = credits < 1 ? 3 : credits < 10 ? 2 : 1;
33
+ const value = credits.toLocaleString(undefined, {
34
+ maximumFractionDigits,
35
+ });
36
+ return `${value} ${credits === 1 ? "credit" : "credits"}`;
37
+ }
38
+ function formatSpend(cents, billing) {
39
+ if (billing.unit === "builder-credits") {
40
+ return formatCredits(displayAmountFromCostCents(cents, billing));
41
+ }
16
42
  if (!Number.isFinite(cents) || cents === 0)
17
43
  return "$0.00";
18
44
  if (Math.abs(cents) < 1)
@@ -55,8 +81,8 @@ function displayApp(value) {
55
81
  return "Unattributed";
56
82
  return trimmed;
57
83
  }
58
- function maxCost(rows) {
59
- return rows.reduce((max, row) => Math.max(max, row.costCents), 0);
84
+ function maxSpend(rows, billing) {
85
+ return rows.reduce((max, row) => Math.max(max, displayAmountFromCostCents(row.costCents, billing)), 0);
60
86
  }
61
87
  function barWidth(value, max) {
62
88
  if (max <= 0 || value <= 0)
@@ -75,12 +101,14 @@ function Panel({ title, icon, children, action, }) {
75
101
  function LoadingMetrics() {
76
102
  return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "grid gap-3 md:grid-cols-2 xl:grid-cols-5", children: Array.from({ length: 5 }).map((_, index) => (_jsxs("div", { className: "rounded-lg border bg-card p-4", children: [_jsx(Skeleton, { className: "mb-4 h-4 w-24" }), _jsx(Skeleton, { className: "h-7 w-20" }), _jsx(Skeleton, { className: "mt-3 h-3 w-28" })] }, index))) }), _jsx(Skeleton, { className: "h-80 rounded-lg" })] }));
77
103
  }
78
- function AppSpendRows({ rows }) {
79
- const max = maxCost(rows);
104
+ function AppSpendRows({ rows, billing, }) {
105
+ const max = maxSpend(rows, billing);
80
106
  if (rows.length === 0) {
81
107
  return (_jsx("div", { className: "rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground", children: "No LLM usage recorded for this window." }));
82
108
  }
83
- return (_jsx("div", { className: "space-y-3", children: rows.map((row) => (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 text-sm", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-medium text-foreground", children: displayApp(row.key) }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [formatNumber(row.chatCalls), " chats \u00B7", " ", formatNumber(row.activeUsers), " users"] })] }), _jsxs("div", { className: "shrink-0 text-right", children: [_jsx("div", { className: "font-medium tabular-nums text-foreground", children: formatCost(row.costCents) }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [formatNumber(row.calls), " calls"] })] })] }), _jsx("div", { className: "h-2 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full rounded-full bg-foreground", style: { width: barWidth(row.costCents, max) } }) })] }, row.key))) }));
109
+ return (_jsx("div", { className: "space-y-3", children: rows.map((row) => (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 text-sm", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-medium text-foreground", children: displayApp(row.key) }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [formatNumber(row.chatCalls), " chats \u00B7", " ", formatNumber(row.activeUsers), " users"] })] }), _jsxs("div", { className: "shrink-0 text-right", children: [_jsx("div", { className: "font-medium tabular-nums text-foreground", children: formatSpend(row.costCents, billing) }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [formatNumber(row.calls), " calls"] })] })] }), _jsx("div", { className: "h-2 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full rounded-full bg-foreground", style: {
110
+ width: barWidth(displayAmountFromCostCents(row.costCents, billing), max),
111
+ } }) })] }, row.key))) }));
84
112
  }
85
113
  function DailyActivity({ rows }) {
86
114
  const max = Math.max(1, rows.reduce((value, row) => Math.max(value, row.calls), 0));
@@ -89,37 +117,40 @@ function DailyActivity({ rows }) {
89
117
  }
90
118
  return (_jsx("div", { className: "flex h-44 items-end gap-1", children: rows.map((row) => (_jsxs("div", { className: "group flex min-w-0 flex-1 flex-col items-center gap-2", children: [_jsx("div", { className: "relative flex h-36 w-full items-end rounded-sm bg-muted/60", children: _jsx("div", { className: "w-full rounded-sm bg-foreground transition group-hover:bg-primary", style: { height: `${Math.max(4, (row.calls / max) * 100)}%` } }) }), _jsx("div", { className: "hidden text-[10px] text-muted-foreground sm:block", children: row.date.slice(5) })] }, row.date))) }));
91
119
  }
92
- function AppAccessTable({ rows }) {
120
+ function AppAccessTable({ rows, billing, }) {
93
121
  const visibleRows = rows.filter((row) => !row.isDispatch);
94
122
  if (visibleRows.length === 0) {
95
123
  return (_jsx("div", { className: "rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground", children: "No workspace apps discovered yet." }));
96
124
  }
97
- return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[720px] text-left text-xs", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "px-2 py-2 font-medium", children: "App" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "Access" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Users" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Chats" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Cost" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Last activity" })] }) }), _jsx("tbody", { children: visibleRows.map((row) => (_jsxs("tr", { className: "border-b last:border-0", children: [_jsxs("td", { className: "px-2 py-3", children: [_jsx("div", { className: "font-medium text-foreground", children: row.name }), _jsx("div", { className: "font-mono text-[11px] text-muted-foreground", children: row.path })] }), _jsx("td", { className: "px-2 py-3", children: _jsx(Badge, { variant: "outline", className: cn(row.status === "pending" &&
98
- "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"), children: row.status === "pending" ? "Building" : row.accessLabel }) }), _jsxs("td", { className: "px-2 py-3 text-right tabular-nums", children: [formatNumber(row.usersWithUsage), " /", " ", formatNumber(row.accessUsers)] }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatNumber(row.chatCalls) }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatCost(row.costCents) }), _jsx("td", { className: "px-2 py-3 text-right text-muted-foreground", children: timeAgo(row.lastActiveAt) })] }, row.id))) })] }) }));
125
+ return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[720px] text-left text-xs", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "px-2 py-2 font-medium", children: "App" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "Access" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Users" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Chats" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: billing.shortLabel }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Last activity" })] }) }), _jsx("tbody", { children: visibleRows.map((row) => (_jsxs("tr", { className: "border-b last:border-0", children: [_jsxs("td", { className: "px-2 py-3", children: [_jsx("div", { className: "font-medium text-foreground", children: row.name }), _jsx("div", { className: "font-mono text-[11px] text-muted-foreground", children: row.path })] }), _jsx("td", { className: "px-2 py-3", children: _jsx(Badge, { variant: "outline", className: cn(row.status === "pending" &&
126
+ "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"), children: row.status === "pending" ? "Building" : row.accessLabel }) }), _jsxs("td", { className: "px-2 py-3 text-right tabular-nums", children: [formatNumber(row.usersWithUsage), " /", " ", formatNumber(row.accessUsers)] }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatNumber(row.chatCalls) }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatSpend(row.costCents, billing) }), _jsx("td", { className: "px-2 py-3 text-right text-muted-foreground", children: timeAgo(row.lastActiveAt) })] }, row.id))) })] }) }));
99
127
  }
100
- function UserTable({ rows }) {
128
+ function UserTable({ rows, billing, }) {
101
129
  if (rows.length === 0) {
102
130
  return (_jsx("div", { className: "rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground", children: "No users have triggered LLM usage in this window." }));
103
131
  }
104
- return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[760px] text-left text-xs", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "px-2 py-2 font-medium", children: "User" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "Role" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "Top app" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Chats" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Threads" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Tokens" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Cost" })] }) }), _jsx("tbody", { children: rows.slice(0, 12).map((row) => (_jsxs("tr", { className: "border-b last:border-0", children: [_jsxs("td", { className: "max-w-64 px-2 py-3", children: [_jsx("div", { className: "truncate font-medium text-foreground", children: row.ownerEmail }), _jsx("div", { className: "text-muted-foreground", children: timeAgo(row.lastActiveAt ?? row.lastChatAt) })] }), _jsx("td", { className: "px-2 py-3", children: _jsx(Badge, { variant: "secondary", children: row.role ?? "user" }) }), _jsx("td", { className: "px-2 py-3 text-muted-foreground", children: displayApp(row.topApp) }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatNumber(row.chatCalls) }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatNumber(row.chatThreads) }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatTokens(row.inputTokens + row.outputTokens) }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatCost(row.costCents) })] }, row.ownerEmail))) })] }) }));
132
+ return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[760px] text-left text-xs", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "px-2 py-2 font-medium", children: "User" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "Role" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "Top app" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Chats" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Threads" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Tokens" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: billing.shortLabel })] }) }), _jsx("tbody", { children: rows.slice(0, 12).map((row) => (_jsxs("tr", { className: "border-b last:border-0", children: [_jsxs("td", { className: "max-w-64 px-2 py-3", children: [_jsx("div", { className: "truncate font-medium text-foreground", children: row.ownerEmail }), _jsx("div", { className: "text-muted-foreground", children: timeAgo(row.lastActiveAt ?? row.lastChatAt) })] }), _jsx("td", { className: "px-2 py-3", children: _jsx(Badge, { variant: "secondary", children: row.role ?? "user" }) }), _jsx("td", { className: "px-2 py-3 text-muted-foreground", children: displayApp(row.topApp) }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatNumber(row.chatCalls) }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatNumber(row.chatThreads) }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatTokens(row.inputTokens + row.outputTokens) }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatSpend(row.costCents, billing) })] }, row.ownerEmail))) })] }) }));
105
133
  }
106
- function CompactBreakdown({ rows, empty, }) {
107
- const max = maxCost(rows);
134
+ function CompactBreakdown({ rows, empty, billing, }) {
135
+ const max = maxSpend(rows, billing);
108
136
  if (rows.length === 0) {
109
137
  return _jsx("div", { className: "text-sm text-muted-foreground", children: empty });
110
138
  }
111
- return (_jsx("div", { className: "space-y-3", children: rows.slice(0, 6).map((row) => (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 text-xs", children: [_jsx("span", { className: "truncate font-medium text-foreground", children: row.label }), _jsx("span", { className: "shrink-0 tabular-nums text-muted-foreground", children: formatCost(row.costCents) })] }), _jsx("div", { className: "h-1.5 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full rounded-full bg-muted-foreground", style: { width: barWidth(row.costCents, max) } }) })] }, row.key))) }));
139
+ return (_jsx("div", { className: "space-y-3", children: rows.slice(0, 6).map((row) => (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 text-xs", children: [_jsx("span", { className: "truncate font-medium text-foreground", children: row.label }), _jsx("span", { className: "shrink-0 tabular-nums text-muted-foreground", children: formatSpend(row.costCents, billing) })] }), _jsx("div", { className: "h-1.5 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full rounded-full bg-muted-foreground", style: {
140
+ width: barWidth(displayAmountFromCostCents(row.costCents, billing), max),
141
+ } }) })] }, row.key))) }));
112
142
  }
113
- function RecentTable({ rows }) {
143
+ function RecentTable({ rows, billing, }) {
114
144
  if (rows.length === 0) {
115
145
  return (_jsx("div", { className: "rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground", children: "No recent LLM calls." }));
116
146
  }
117
- return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[760px] text-left text-xs", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "px-2 py-2 font-medium", children: "When" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "User" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "App" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "Label" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "Model" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: "Cost" })] }) }), _jsx("tbody", { children: rows.slice(0, 10).map((row) => (_jsxs("tr", { className: "border-b last:border-0", children: [_jsx("td", { className: "px-2 py-3 text-muted-foreground", children: timeAgo(row.createdAt) }), _jsx("td", { className: "max-w-56 px-2 py-3", children: _jsx("div", { className: "truncate text-foreground", children: row.ownerEmail }) }), _jsx("td", { className: "px-2 py-3 text-muted-foreground", children: displayApp(row.app) }), _jsx("td", { className: "px-2 py-3", children: _jsx(Badge, { variant: "outline", children: row.label }) }), _jsx("td", { className: "max-w-48 px-2 py-3", children: _jsx("div", { className: "truncate text-muted-foreground", children: row.model }) }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatCost(row.costCents) })] }, row.id))) })] }) }));
147
+ return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[760px] text-left text-xs", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "px-2 py-2 font-medium", children: "When" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "User" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "App" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "Label" }), _jsx("th", { className: "px-2 py-2 font-medium", children: "Model" }), _jsx("th", { className: "px-2 py-2 text-right font-medium", children: billing.shortLabel })] }) }), _jsx("tbody", { children: rows.slice(0, 10).map((row) => (_jsxs("tr", { className: "border-b last:border-0", children: [_jsx("td", { className: "px-2 py-3 text-muted-foreground", children: timeAgo(row.createdAt) }), _jsx("td", { className: "max-w-56 px-2 py-3", children: _jsx("div", { className: "truncate text-foreground", children: row.ownerEmail }) }), _jsx("td", { className: "px-2 py-3 text-muted-foreground", children: displayApp(row.app) }), _jsx("td", { className: "px-2 py-3", children: _jsx(Badge, { variant: "outline", children: row.label }) }), _jsx("td", { className: "max-w-48 px-2 py-3", children: _jsx("div", { className: "truncate text-muted-foreground", children: row.model }) }), _jsx("td", { className: "px-2 py-3 text-right tabular-nums", children: formatSpend(row.costCents, billing) })] }, row.id))) })] }) }));
118
148
  }
119
149
  export default function MetricsRoute() {
120
150
  const [sinceDays, setSinceDays] = useState(30);
121
151
  const { data, isLoading, error } = useActionQuery("list-dispatch-usage-metrics", { sinceDays }, { refetchInterval: 30_000 });
122
152
  const metrics = data;
153
+ const billing = metrics?.billing ?? USD_BILLING;
123
154
  const totalTokens = useMemo(() => {
124
155
  if (!metrics)
125
156
  return 0;
@@ -128,8 +159,12 @@ export default function MetricsRoute() {
128
159
  metrics.totals.cacheReadTokens +
129
160
  metrics.totals.cacheWriteTokens);
130
161
  }, [metrics]);
131
- return (_jsx(DispatchShell, { title: "Metrics", description: "Workspace-wide LLM spend, chat volume, user activity, and app access.", children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsx("div", { className: "text-sm text-muted-foreground", children: metrics?.access.scope === "organization"
162
+ return (_jsx(DispatchShell, { title: "Metrics", description: billing.unit === "builder-credits"
163
+ ? "Workspace-wide Builder.io credit spend, chat volume, user activity, and app access."
164
+ : "Workspace-wide LLM spend, chat volume, user activity, and app access.", children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsx("div", { className: "text-sm text-muted-foreground", children: metrics?.access.scope === "organization"
132
165
  ? `${metrics.access.totalUsers} workspace users`
133
- : `${metrics?.access.totalUsers ?? 0} signed-in users` }), _jsx(RangeSelector, { value: sinceDays, onChange: setSinceDays })] }), error ? (_jsxs(Alert, { variant: "destructive", children: [_jsx(IconAlertTriangle, { className: "h-4 w-4" }), _jsx(AlertTitle, { children: "Metrics unavailable" }), _jsx(AlertDescription, { children: error instanceof Error ? error.message : "Unable to load usage." })] })) : null, isLoading && !metrics ? _jsx(LoadingMetrics, {}) : null, metrics ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-3 md:grid-cols-2 xl:grid-cols-5", children: [_jsx(MetricCard, { label: "Estimated spend", value: formatCost(metrics.totals.costCents), detail: `${formatTokens(totalTokens)} total tokens`, icon: _jsx(IconCoin, { size: 17 }) }), _jsx(MetricCard, { label: "LLM calls", value: formatNumber(metrics.totals.calls), detail: `${formatNumber(metrics.totals.chatCalls)} chat turns`, icon: _jsx(IconActivity, { size: 17 }) }), _jsx(MetricCard, { label: "Active users", value: formatNumber(metrics.totals.activeUsers), detail: `${formatNumber(metrics.access.totalUsers)} users with access`, icon: _jsx(IconUsersGroup, { size: 17 }) }), _jsx(MetricCard, { label: "Workspace apps", value: formatNumber(metrics.totals.workspaceApps), detail: `${formatNumber(metrics.byApp.length)} with usage`, icon: _jsx(IconApps, { size: 17 }) }), _jsx(MetricCard, { label: "Chat threads", value: formatNumber(metrics.totals.chatThreads), detail: `${formatNumber(metrics.totals.chatMessages)} messages`, icon: _jsx(IconMessages, { size: 17 }) })] }), _jsxs("div", { className: "grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(320px,0.65fr)]", children: [_jsx(Panel, { title: "Spend By App", icon: _jsx(IconChartBar, { size: 16 }), children: _jsx(AppSpendRows, { rows: metrics.byApp }) }), _jsx(Panel, { title: "Daily Activity", icon: _jsx(IconClockHour4, { size: 16 }), children: _jsx(DailyActivity, { rows: metrics.daily }) })] }), _jsx(Panel, { title: "Access By App", icon: _jsx(IconApps, { size: 16 }), children: _jsx(AppAccessTable, { rows: metrics.appAccess }) }), _jsx(Panel, { title: "Users", icon: _jsx(IconUsersGroup, { size: 16 }), children: _jsx(UserTable, { rows: metrics.byUser }) }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsx(Panel, { title: "Models", icon: _jsx(IconChartBar, { size: 16 }), children: _jsx(CompactBreakdown, { rows: metrics.byModel, empty: "No model usage in this window." }) }), _jsx(Panel, { title: "Work Types", icon: _jsx(IconActivity, { size: 16 }), children: _jsx(CompactBreakdown, { rows: metrics.byLabel, empty: "No labeled usage in this window." }) })] }), _jsx(Panel, { title: "Recent LLM Calls", icon: _jsx(IconActivity, { size: 16 }), children: _jsx(RecentTable, { rows: metrics.recent }) })] })) : null] }) }));
166
+ : `${metrics?.access.totalUsers ?? 0} signed-in users` }), _jsx(RangeSelector, { value: sinceDays, onChange: setSinceDays })] }), error ? (_jsxs(Alert, { variant: "destructive", children: [_jsx(IconAlertTriangle, { className: "h-4 w-4" }), _jsx(AlertTitle, { children: "Metrics unavailable" }), _jsx(AlertDescription, { children: error instanceof Error ? error.message : "Unable to load usage." })] })) : null, isLoading && !metrics ? _jsx(LoadingMetrics, {}) : null, metrics ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-3 md:grid-cols-2 xl:grid-cols-5", children: [_jsx(MetricCard, { label: billing.label, value: formatSpend(metrics.totals.costCents, billing), detail: `${formatTokens(totalTokens)} total tokens`, icon: _jsx(IconCoin, { size: 17 }) }), _jsx(MetricCard, { label: "LLM calls", value: formatNumber(metrics.totals.calls), detail: `${formatNumber(metrics.totals.chatCalls)} chat turns`, icon: _jsx(IconActivity, { size: 17 }) }), _jsx(MetricCard, { label: "Active users", value: formatNumber(metrics.totals.activeUsers), detail: `${formatNumber(metrics.access.totalUsers)} users with access`, icon: _jsx(IconUsersGroup, { size: 17 }) }), _jsx(MetricCard, { label: "Workspace apps", value: formatNumber(metrics.totals.workspaceApps), detail: `${formatNumber(metrics.byApp.length)} with usage`, icon: _jsx(IconApps, { size: 17 }) }), _jsx(MetricCard, { label: "Chat threads", value: formatNumber(metrics.totals.chatThreads), detail: `${formatNumber(metrics.totals.chatMessages)} messages`, icon: _jsx(IconMessages, { size: 17 }) })] }), _jsxs("div", { className: "grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(320px,0.65fr)]", children: [_jsx(Panel, { title: billing.unit === "builder-credits"
167
+ ? "Credit Spend By App"
168
+ : "Spend By App", icon: _jsx(IconChartBar, { size: 16 }), children: _jsx(AppSpendRows, { rows: metrics.byApp, billing: billing }) }), _jsx(Panel, { title: "Daily Activity", icon: _jsx(IconClockHour4, { size: 16 }), children: _jsx(DailyActivity, { rows: metrics.daily }) })] }), _jsx(Panel, { title: "Access By App", icon: _jsx(IconApps, { size: 16 }), children: _jsx(AppAccessTable, { rows: metrics.appAccess, billing: billing }) }), _jsx(Panel, { title: "Users", icon: _jsx(IconUsersGroup, { size: 16 }), children: _jsx(UserTable, { rows: metrics.byUser, billing: billing }) }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsx(Panel, { title: "Models", icon: _jsx(IconChartBar, { size: 16 }), children: _jsx(CompactBreakdown, { rows: metrics.byModel, empty: "No model usage in this window.", billing: billing }) }), _jsx(Panel, { title: "Work Types", icon: _jsx(IconActivity, { size: 16 }), children: _jsx(CompactBreakdown, { rows: metrics.byLabel, empty: "No labeled usage in this window.", billing: billing }) })] }), _jsx(Panel, { title: "Recent LLM Calls", icon: _jsx(IconActivity, { size: 16 }), children: _jsx(RecentTable, { rows: metrics.recent, billing: billing }) })] })) : null] }) }));
134
169
  }
135
170
  //# sourceMappingURL=metrics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../../src/routes/pages/metrics.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,cAAc,GACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEjC,MAAM,UAAU,IAAI;IAClB,OAAO,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC,CAAC;AAC3C,CAAC;AAyFD,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,CAAU,CAAC;AAEpC,SAAS,UAAU,CAAC,KAAa;IAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IAC3D,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IACvD,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG;QAAE,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IACzD,OAAO,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,cAAc,CAAC,SAAS,EAAE;QAC7C,KAAK,EAAE,UAAU;QACjB,QAAQ,EAAE,KAAK;QACf,qBAAqB,EAAE,CAAC;KACzB,CAAC,CAAC;AACL,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE;QACtC,QAAQ,EAAE,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU;QAClD,qBAAqB,EAAE,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAC/C,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE;QACtC,QAAQ,EAAE,SAAS;QACnB,qBAAqB,EAAE,CAAC;KACzB,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC;AAED,SAAS,OAAO,CAAC,SAAwB;IACvC,IAAI,CAAC,SAAS;QAAE,OAAO,aAAa,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IACpC,IAAI,IAAI,GAAG,MAAM;QAAE,OAAO,UAAU,CAAC;IACrC,IAAI,IAAI,GAAG,SAAS;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC;IACjE,IAAI,IAAI,GAAG,UAAU;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC;IACrE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC;AACjD,CAAC;AAED,SAAS,UAAU,CAAC,KAAgC;IAClD,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;IAC9B,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,cAAc;QAAE,OAAO,cAAc,CAAC;IAClE,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,OAAO,CAAC,IAAkC;IACjD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAa,EAAE,GAAW;IAC1C,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;AAC5D,CAAC;AAED,SAAS,aAAa,CAAC,EACrB,KAAK,EACL,QAAQ,GAIT;IACC,OAAO,CACL,cAAK,SAAS,EAAC,sCAAsC,YAClD,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACrB,MAAC,MAAM,IAEL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,EAChD,IAAI,EAAC,IAAI,EACT,SAAS,EAAC,kBAAkB,EAC5B,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,aAE7B,KAAK,UAPD,KAAK,CAQH,CACV,CAAC,GACE,CACP,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,EAClB,KAAK,EACL,KAAK,EACL,MAAM,EACN,IAAI,GAML;IACC,OAAO,CACL,eAAK,SAAS,EAAC,+BAA+B,aAC5C,eAAK,SAAS,EAAC,8CAA8C,aAC3D,eAAM,SAAS,EAAC,2CAA2C,YACxD,KAAK,GACD,EACP,eAAM,SAAS,EAAC,uBAAuB,YAAE,IAAI,GAAQ,IACjD,EACN,cAAK,SAAS,EAAC,qDAAqD,YACjE,KAAK,GACF,EACN,cAAK,SAAS,EAAC,6CAA6C,YACzD,MAAM,GACH,IACF,CACP,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CAAC,EACb,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,MAAM,GAMP;IACC,OAAO,CACL,mBAAS,SAAS,EAAC,2BAA2B,aAC5C,eAAK,SAAS,EAAC,4DAA4D,aACzE,eAAK,SAAS,EAAC,iCAAiC,aAC9C,eAAM,SAAS,EAAC,uBAAuB,YAAE,IAAI,GAAQ,EACrD,aAAI,SAAS,EAAC,gDAAgD,YAC3D,KAAK,GACH,IACD,EACL,MAAM,IACH,EACN,cAAK,SAAS,EAAC,KAAK,YAAE,QAAQ,GAAO,IAC7B,CACX,CAAC;AACJ,CAAC;AAED,SAAS,cAAc;IACrB,OAAO,CACL,eAAK,SAAS,EAAC,WAAW,aACxB,cAAK,SAAS,EAAC,0CAA0C,YACtD,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAC3C,eAAiB,SAAS,EAAC,+BAA+B,aACxD,KAAC,QAAQ,IAAC,SAAS,EAAC,eAAe,GAAG,EACtC,KAAC,QAAQ,IAAC,SAAS,EAAC,UAAU,GAAG,EACjC,KAAC,QAAQ,IAAC,SAAS,EAAC,eAAe,GAAG,KAH9B,KAAK,CAIT,CACP,CAAC,GACE,EACN,KAAC,QAAQ,IAAC,SAAS,EAAC,iBAAiB,GAAG,IACpC,CACP,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,EAAE,IAAI,EAAiC;IAC3D,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,CACL,cAAK,SAAS,EAAC,yEAAyE,uDAElF,CACP,CAAC;IACJ,CAAC;IACD,OAAO,CACL,cAAK,SAAS,EAAC,WAAW,YACvB,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACjB,eAAmB,SAAS,EAAC,aAAa,aACxC,eAAK,SAAS,EAAC,iDAAiD,aAC9D,eAAK,SAAS,EAAC,SAAS,aACtB,cAAK,SAAS,EAAC,sCAAsC,YAClD,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,GAChB,EACN,eAAK,SAAS,EAAC,+BAA+B,aAC3C,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,mBAAU,GAAG,EACxC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,cAC1B,IACF,EACN,eAAK,SAAS,EAAC,qBAAqB,aAClC,cAAK,SAAS,EAAC,0CAA0C,YACtD,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,GACtB,EACN,eAAK,SAAS,EAAC,+BAA+B,aAC3C,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,cACpB,IACF,IACF,EACN,cAAK,SAAS,EAAC,2CAA2C,YACxD,cACE,SAAS,EAAC,mCAAmC,EAC7C,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,GAC9C,GACE,KAzBE,GAAG,CAAC,GAAG,CA0BX,CACP,CAAC,GACE,CACP,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,EAAE,IAAI,EAAgC;IAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAClB,CAAC,EACD,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAC3D,CAAC;IACF,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,CACL,cAAK,SAAS,EAAC,yEAAyE,4CAElF,CACP,CAAC;IACJ,CAAC;IACD,OAAO,CACL,cAAK,SAAS,EAAC,2BAA2B,YACvC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACjB,eAEE,SAAS,EAAC,uDAAuD,aAEjE,cAAK,SAAS,EAAC,4DAA4D,YACzE,cACE,SAAS,EAAC,mEAAmE,EAC7E,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,EAAE,GAC7D,GACE,EACN,cAAK,SAAS,EAAC,mDAAmD,YAC/D,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GACd,KAXD,GAAG,CAAC,IAAI,CAYT,CACP,CAAC,GACE,CACP,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,EAAE,IAAI,EAA+B;IAC3D,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAC1D,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,CACL,cAAK,SAAS,EAAC,yEAAyE,kDAElF,CACP,CAAC;IACJ,CAAC;IACD,OAAO,CACL,cAAK,SAAS,EAAC,iBAAiB,YAC9B,iBAAO,SAAS,EAAC,wCAAwC,aACvD,0BACE,cAAI,SAAS,EAAC,gCAAgC,aAC5C,aAAI,SAAS,EAAC,uBAAuB,oBAAS,EAC9C,aAAI,SAAS,EAAC,uBAAuB,uBAAY,EACjD,aAAI,SAAS,EAAC,kCAAkC,sBAAW,EAC3D,aAAI,SAAS,EAAC,kCAAkC,sBAAW,EAC3D,aAAI,SAAS,EAAC,kCAAkC,qBAAU,EAC1D,aAAI,SAAS,EAAC,kCAAkC,8BAAmB,IAChE,GACC,EACR,0BACG,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACxB,cAAiB,SAAS,EAAC,wBAAwB,aACjD,cAAI,SAAS,EAAC,WAAW,aACvB,cAAK,SAAS,EAAC,6BAA6B,YAAE,GAAG,CAAC,IAAI,GAAO,EAC7D,cAAK,SAAS,EAAC,6CAA6C,YACzD,GAAG,CAAC,IAAI,GACL,IACH,EACL,aAAI,SAAS,EAAC,WAAW,YACvB,KAAC,KAAK,IACJ,OAAO,EAAC,SAAS,EACjB,SAAS,EAAE,EAAE,CACX,GAAG,CAAC,MAAM,KAAK,SAAS;wCACtB,wEAAwE,CAC3E,YAEA,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,GAClD,GACL,EACL,cAAI,SAAS,EAAC,mCAAmC,aAC9C,YAAY,CAAC,GAAG,CAAC,cAAc,CAAC,QAAI,GAAG,EACvC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,IAC3B,EACL,aAAI,SAAS,EAAC,mCAAmC,YAC9C,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,GACzB,EACL,aAAI,SAAS,EAAC,mCAAmC,YAC9C,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,GACvB,EACL,aAAI,SAAS,EAAC,4CAA4C,YACvD,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,GACvB,KA9BE,GAAG,CAAC,EAAE,CA+BV,CACN,CAAC,GACI,IACF,GACJ,CACP,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,EAAE,IAAI,EAA+B;IACtD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,CACL,cAAK,SAAS,EAAC,yEAAyE,kEAElF,CACP,CAAC;IACJ,CAAC;IACD,OAAO,CACL,cAAK,SAAS,EAAC,iBAAiB,YAC9B,iBAAO,SAAS,EAAC,wCAAwC,aACvD,0BACE,cAAI,SAAS,EAAC,gCAAgC,aAC5C,aAAI,SAAS,EAAC,uBAAuB,qBAAU,EAC/C,aAAI,SAAS,EAAC,uBAAuB,qBAAU,EAC/C,aAAI,SAAS,EAAC,uBAAuB,wBAAa,EAClD,aAAI,SAAS,EAAC,kCAAkC,sBAAW,EAC3D,aAAI,SAAS,EAAC,kCAAkC,wBAAa,EAC7D,aAAI,SAAS,EAAC,kCAAkC,uBAAY,EAC5D,aAAI,SAAS,EAAC,kCAAkC,qBAAU,IACvD,GACC,EACR,0BACG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAC9B,cAAyB,SAAS,EAAC,wBAAwB,aACzD,cAAI,SAAS,EAAC,oBAAoB,aAChC,cAAK,SAAS,EAAC,sCAAsC,YAClD,GAAG,CAAC,UAAU,GACX,EACN,cAAK,SAAS,EAAC,uBAAuB,YACnC,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,UAAU,CAAC,GACxC,IACH,EACL,aAAI,SAAS,EAAC,WAAW,YACvB,KAAC,KAAK,IAAC,OAAO,EAAC,WAAW,YAAE,GAAG,CAAC,IAAI,IAAI,MAAM,GAAS,GACpD,EACL,aAAI,SAAS,EAAC,iCAAiC,YAC5C,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,GACpB,EACL,aAAI,SAAS,EAAC,mCAAmC,YAC9C,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,GACzB,EACL,aAAI,SAAS,EAAC,mCAAmC,YAC9C,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,GAC3B,EACL,aAAI,SAAS,EAAC,mCAAmC,YAC9C,YAAY,CAAC,GAAG,CAAC,WAAW,GAAG,GAAG,CAAC,YAAY,CAAC,GAC9C,EACL,aAAI,SAAS,EAAC,mCAAmC,YAC9C,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,GACvB,KA1BE,GAAG,CAAC,UAAU,CA2BlB,CACN,CAAC,GACI,IACF,GACJ,CACP,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,EACxB,IAAI,EACJ,KAAK,GAIN;IACC,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,cAAK,SAAS,EAAC,+BAA+B,YAAE,KAAK,GAAO,CAAC;IACtE,CAAC;IACD,OAAO,CACL,cAAK,SAAS,EAAC,WAAW,YACvB,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAC7B,eAAmB,SAAS,EAAC,WAAW,aACtC,eAAK,SAAS,EAAC,iDAAiD,aAC9D,eAAM,SAAS,EAAC,sCAAsC,YACnD,GAAG,CAAC,KAAK,GACL,EACP,eAAM,SAAS,EAAC,6CAA6C,YAC1D,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,GACrB,IACH,EACN,cAAK,SAAS,EAAC,6CAA6C,YAC1D,cACE,SAAS,EAAC,yCAAyC,EACnD,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,GAC9C,GACE,KAdE,GAAG,CAAC,GAAG,CAeX,CACP,CAAC,GACE,CACP,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,IAAI,EAAiC;IAC1D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,CACL,cAAK,SAAS,EAAC,yEAAyE,qCAElF,CACP,CAAC;IACJ,CAAC;IACD,OAAO,CACL,cAAK,SAAS,EAAC,iBAAiB,YAC9B,iBAAO,SAAS,EAAC,wCAAwC,aACvD,0BACE,cAAI,SAAS,EAAC,gCAAgC,aAC5C,aAAI,SAAS,EAAC,uBAAuB,qBAAU,EAC/C,aAAI,SAAS,EAAC,uBAAuB,qBAAU,EAC/C,aAAI,SAAS,EAAC,uBAAuB,oBAAS,EAC9C,aAAI,SAAS,EAAC,uBAAuB,sBAAW,EAChD,aAAI,SAAS,EAAC,uBAAuB,sBAAW,EAChD,aAAI,SAAS,EAAC,kCAAkC,qBAAU,IACvD,GACC,EACR,0BACG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAC9B,cAAiB,SAAS,EAAC,wBAAwB,aACjD,aAAI,SAAS,EAAC,iCAAiC,YAC5C,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,GACpB,EACL,aAAI,SAAS,EAAC,oBAAoB,YAChC,cAAK,SAAS,EAAC,0BAA0B,YAAE,GAAG,CAAC,UAAU,GAAO,GAC7D,EACL,aAAI,SAAS,EAAC,iCAAiC,YAC5C,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,GACjB,EACL,aAAI,SAAS,EAAC,WAAW,YACvB,KAAC,KAAK,IAAC,OAAO,EAAC,SAAS,YAAE,GAAG,CAAC,KAAK,GAAS,GACzC,EACL,aAAI,SAAS,EAAC,oBAAoB,YAChC,cAAK,SAAS,EAAC,gCAAgC,YAC5C,GAAG,CAAC,KAAK,GACN,GACH,EACL,aAAI,SAAS,EAAC,mCAAmC,YAC9C,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,GACvB,KApBE,GAAG,CAAC,EAAE,CAqBV,CACN,CAAC,GACI,IACF,GACJ,CACP,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,OAAO,UAAU,YAAY;IAClC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC/C,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,cAAc,CAC/C,6BAA6B,EAC7B,EAAE,SAAS,EAAE,EACb,EAAE,eAAe,EAAE,MAAM,EAAE,CAC5B,CAAC;IACF,MAAM,OAAO,GAAG,IAAwC,CAAC;IACzD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE;QAC/B,IAAI,CAAC,OAAO;YAAE,OAAO,CAAC,CAAC;QACvB,OAAO,CACL,OAAO,CAAC,MAAM,CAAC,WAAW;YAC1B,OAAO,CAAC,MAAM,CAAC,YAAY;YAC3B,OAAO,CAAC,MAAM,CAAC,eAAe;YAC9B,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAChC,CAAC;IACJ,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAEd,OAAO,CACL,KAAC,aAAa,IACZ,KAAK,EAAC,SAAS,EACf,WAAW,EAAC,uEAAuE,YAEnF,eAAK,SAAS,EAAC,WAAW,aACxB,eAAK,SAAS,EAAC,mDAAmD,aAChE,cAAK,SAAS,EAAC,+BAA+B,YAC3C,OAAO,EAAE,MAAM,CAAC,KAAK,KAAK,cAAc;gCACvC,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,kBAAkB;gCAChD,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,CAAC,UAAU,IAAI,CAAC,kBAAkB,GACpD,EACN,KAAC,aAAa,IAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,GAAI,IACvD,EAEL,KAAK,CAAC,CAAC,CAAC,CACP,MAAC,KAAK,IAAC,OAAO,EAAC,aAAa,aAC1B,KAAC,iBAAiB,IAAC,SAAS,EAAC,SAAS,GAAG,EACzC,KAAC,UAAU,sCAAiC,EAC5C,KAAC,gBAAgB,cACd,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,uBAAuB,GAChD,IACb,CACT,CAAC,CAAC,CAAC,IAAI,EAEP,SAAS,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,KAAC,cAAc,KAAG,CAAC,CAAC,CAAC,IAAI,EAEjD,OAAO,CAAC,CAAC,CAAC,CACT,8BACE,eAAK,SAAS,EAAC,0CAA0C,aACvD,KAAC,UAAU,IACT,KAAK,EAAC,iBAAiB,EACvB,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,EAC3C,MAAM,EAAE,GAAG,YAAY,CAAC,WAAW,CAAC,eAAe,EACnD,IAAI,EAAE,KAAC,QAAQ,IAAC,IAAI,EAAE,EAAE,GAAI,GAC5B,EACF,KAAC,UAAU,IACT,KAAK,EAAC,WAAW,EACjB,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EACzC,MAAM,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,aAAa,EAC9D,IAAI,EAAE,KAAC,YAAY,IAAC,IAAI,EAAE,EAAE,GAAI,GAChC,EACF,KAAC,UAAU,IACT,KAAK,EAAC,cAAc,EACpB,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,EAC/C,MAAM,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,oBAAoB,EACtE,IAAI,EAAE,KAAC,cAAc,IAAC,IAAI,EAAE,EAAE,GAAI,GAClC,EACF,KAAC,UAAU,IACT,KAAK,EAAC,gBAAgB,EACtB,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,EACjD,MAAM,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,aAAa,EAC1D,IAAI,EAAE,KAAC,QAAQ,IAAC,IAAI,EAAE,EAAE,GAAI,GAC5B,EACF,KAAC,UAAU,IACT,KAAK,EAAC,cAAc,EACpB,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,EAC/C,MAAM,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,WAAW,EAC/D,IAAI,EAAE,KAAC,YAAY,IAAC,IAAI,EAAE,EAAE,GAAI,GAChC,IACE,EAEN,eAAK,SAAS,EAAC,iEAAiE,aAC9E,KAAC,KAAK,IAAC,KAAK,EAAC,cAAc,EAAC,IAAI,EAAE,KAAC,YAAY,IAAC,IAAI,EAAE,EAAE,GAAI,YAC1D,KAAC,YAAY,IAAC,IAAI,EAAE,OAAO,CAAC,KAAK,GAAI,GAC/B,EACR,KAAC,KAAK,IAAC,KAAK,EAAC,gBAAgB,EAAC,IAAI,EAAE,KAAC,cAAc,IAAC,IAAI,EAAE,EAAE,GAAI,YAC9D,KAAC,aAAa,IAAC,IAAI,EAAE,OAAO,CAAC,KAAK,GAAI,GAChC,IACJ,EAEN,KAAC,KAAK,IAAC,KAAK,EAAC,eAAe,EAAC,IAAI,EAAE,KAAC,QAAQ,IAAC,IAAI,EAAE,EAAE,GAAI,YACvD,KAAC,cAAc,IAAC,IAAI,EAAE,OAAO,CAAC,SAAS,GAAI,GACrC,EAER,KAAC,KAAK,IAAC,KAAK,EAAC,OAAO,EAAC,IAAI,EAAE,KAAC,cAAc,IAAC,IAAI,EAAE,EAAE,GAAI,YACrD,KAAC,SAAS,IAAC,IAAI,EAAE,OAAO,CAAC,MAAM,GAAI,GAC7B,EAER,eAAK,SAAS,EAAC,2BAA2B,aACxC,KAAC,KAAK,IAAC,KAAK,EAAC,QAAQ,EAAC,IAAI,EAAE,KAAC,YAAY,IAAC,IAAI,EAAE,EAAE,GAAI,YACpD,KAAC,gBAAgB,IACf,IAAI,EAAE,OAAO,CAAC,OAAO,EACrB,KAAK,EAAC,gCAAgC,GACtC,GACI,EACR,KAAC,KAAK,IAAC,KAAK,EAAC,YAAY,EAAC,IAAI,EAAE,KAAC,YAAY,IAAC,IAAI,EAAE,EAAE,GAAI,YACxD,KAAC,gBAAgB,IACf,IAAI,EAAE,OAAO,CAAC,OAAO,EACrB,KAAK,EAAC,kCAAkC,GACxC,GACI,IACJ,EAEN,KAAC,KAAK,IAAC,KAAK,EAAC,kBAAkB,EAAC,IAAI,EAAE,KAAC,YAAY,IAAC,IAAI,EAAE,EAAE,GAAI,YAC9D,KAAC,WAAW,IAAC,IAAI,EAAE,OAAO,CAAC,MAAM,GAAI,GAC/B,IACP,CACJ,CAAC,CAAC,CAAC,IAAI,IACJ,GACQ,CACjB,CAAC;AACJ,CAAC","sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { useActionQuery } from \"@agent-native/core/client\";\nimport {\n IconActivity,\n IconAlertTriangle,\n IconApps,\n IconChartBar,\n IconClockHour4,\n IconCoin,\n IconMessages,\n IconUsersGroup,\n} from \"@tabler/icons-react\";\nimport { DispatchShell } from \"@/components/dispatch-shell\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { cn } from \"@/lib/utils\";\n\nexport function meta() {\n return [{ title: \"Metrics — Dispatch\" }];\n}\n\ninterface UsageMetricBucket {\n key: string;\n label: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n activeUsers: number;\n lastActiveAt: number | null;\n}\n\ninterface UserUsageMetric extends UsageMetricBucket {\n ownerEmail: string;\n chatThreads: number;\n chatMessages: number;\n lastChatAt: number | null;\n topApp: string | null;\n role: string | null;\n}\n\ninterface AppAccessMetric {\n id: string;\n name: string;\n path: string;\n status?: \"ready\" | \"pending\";\n isDispatch: boolean;\n accessLabel: string;\n accessUsers: number;\n usersWithUsage: number;\n usageCalls: number;\n chatCalls: number;\n costCents: number;\n lastActiveAt: number | null;\n}\n\ninterface DailyUsageMetric {\n date: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n activeUsers: number;\n}\n\ninterface RecentUsageMetric {\n id: number;\n createdAt: number;\n ownerEmail: string;\n app: string;\n label: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n costCents: number;\n}\n\ninterface DispatchUsageMetrics {\n sinceDays: number;\n access: {\n viewerEmail: string;\n orgId: string | null;\n role: string | null;\n scope: \"organization\" | \"solo\";\n totalUsers: number;\n };\n totals: {\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n activeUsers: number;\n chatThreads: number;\n chatMessages: number;\n workspaceApps: number;\n };\n byApp: UsageMetricBucket[];\n byUser: UserUsageMetric[];\n byLabel: UsageMetricBucket[];\n byModel: UsageMetricBucket[];\n daily: DailyUsageMetric[];\n appAccess: AppAccessMetric[];\n recent: RecentUsageMetric[];\n}\n\nconst RANGES = [7, 30, 90] as const;\n\nfunction formatCost(cents: number): string {\n if (!Number.isFinite(cents) || cents === 0) return \"$0.00\";\n if (Math.abs(cents) < 1) return `${cents.toFixed(3)}¢`;\n if (Math.abs(cents) < 100) return `${cents.toFixed(2)}¢`;\n return (cents / 100).toLocaleString(undefined, {\n style: \"currency\",\n currency: \"USD\",\n maximumFractionDigits: 2,\n });\n}\n\nfunction formatNumber(value: number): string {\n return new Intl.NumberFormat(undefined, {\n notation: value >= 10_000 ? \"compact\" : \"standard\",\n maximumFractionDigits: value >= 10_000 ? 1 : 0,\n }).format(value);\n}\n\nfunction formatTokens(value: number): string {\n return new Intl.NumberFormat(undefined, {\n notation: \"compact\",\n maximumFractionDigits: 1,\n }).format(value);\n}\n\nfunction timeAgo(timestamp: number | null): string {\n if (!timestamp) return \"No activity\";\n const diff = Date.now() - timestamp;\n if (diff < 60_000) return \"just now\";\n if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;\n if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;\n return `${Math.floor(diff / 86_400_000)}d ago`;\n}\n\nfunction displayApp(value: string | null | undefined): string {\n const trimmed = value?.trim();\n if (!trimmed || trimmed === \"unattributed\") return \"Unattributed\";\n return trimmed;\n}\n\nfunction maxCost(rows: Array<{ costCents: number }>): number {\n return rows.reduce((max, row) => Math.max(max, row.costCents), 0);\n}\n\nfunction barWidth(value: number, max: number): string {\n if (max <= 0 || value <= 0) return \"0%\";\n return `${Math.max(4, Math.round((value / max) * 100))}%`;\n}\n\nfunction RangeSelector({\n value,\n onChange,\n}: {\n value: number;\n onChange: (value: number) => void;\n}) {\n return (\n <div className=\"flex rounded-md border bg-card p-0.5\">\n {RANGES.map((range) => (\n <Button\n key={range}\n type=\"button\"\n variant={value === range ? \"secondary\" : \"ghost\"}\n size=\"sm\"\n className=\"h-7 px-3 text-xs\"\n onClick={() => onChange(range)}\n >\n {range}d\n </Button>\n ))}\n </div>\n );\n}\n\nfunction MetricCard({\n label,\n value,\n detail,\n icon,\n}: {\n label: string;\n value: string;\n detail: string;\n icon: ReactNode;\n}) {\n return (\n <div className=\"rounded-lg border bg-card p-4\">\n <div className=\"mb-3 flex items-center justify-between gap-3\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n {label}\n </span>\n <span className=\"text-muted-foreground\">{icon}</span>\n </div>\n <div className=\"text-2xl font-semibold tabular-nums text-foreground\">\n {value}\n </div>\n <div className=\"mt-1 truncate text-xs text-muted-foreground\">\n {detail}\n </div>\n </div>\n );\n}\n\nfunction Panel({\n title,\n icon,\n children,\n action,\n}: {\n title: string;\n icon: ReactNode;\n children: ReactNode;\n action?: ReactNode;\n}) {\n return (\n <section className=\"rounded-lg border bg-card\">\n <div className=\"flex items-center justify-between gap-3 border-b px-4 py-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span className=\"text-muted-foreground\">{icon}</span>\n <h2 className=\"truncate text-sm font-semibold text-foreground\">\n {title}\n </h2>\n </div>\n {action}\n </div>\n <div className=\"p-4\">{children}</div>\n </section>\n );\n}\n\nfunction LoadingMetrics() {\n return (\n <div className=\"space-y-4\">\n <div className=\"grid gap-3 md:grid-cols-2 xl:grid-cols-5\">\n {Array.from({ length: 5 }).map((_, index) => (\n <div key={index} className=\"rounded-lg border bg-card p-4\">\n <Skeleton className=\"mb-4 h-4 w-24\" />\n <Skeleton className=\"h-7 w-20\" />\n <Skeleton className=\"mt-3 h-3 w-28\" />\n </div>\n ))}\n </div>\n <Skeleton className=\"h-80 rounded-lg\" />\n </div>\n );\n}\n\nfunction AppSpendRows({ rows }: { rows: UsageMetricBucket[] }) {\n const max = maxCost(rows);\n if (rows.length === 0) {\n return (\n <div className=\"rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground\">\n No LLM usage recorded for this window.\n </div>\n );\n }\n return (\n <div className=\"space-y-3\">\n {rows.map((row) => (\n <div key={row.key} className=\"space-y-1.5\">\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <div className=\"min-w-0\">\n <div className=\"truncate font-medium text-foreground\">\n {displayApp(row.key)}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {formatNumber(row.chatCalls)} chats ·{\" \"}\n {formatNumber(row.activeUsers)} users\n </div>\n </div>\n <div className=\"shrink-0 text-right\">\n <div className=\"font-medium tabular-nums text-foreground\">\n {formatCost(row.costCents)}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {formatNumber(row.calls)} calls\n </div>\n </div>\n </div>\n <div className=\"h-2 overflow-hidden rounded-full bg-muted\">\n <div\n className=\"h-full rounded-full bg-foreground\"\n style={{ width: barWidth(row.costCents, max) }}\n />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction DailyActivity({ rows }: { rows: DailyUsageMetric[] }) {\n const max = Math.max(\n 1,\n rows.reduce((value, row) => Math.max(value, row.calls), 0),\n );\n if (rows.length === 0) {\n return (\n <div className=\"rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground\">\n No activity in this window.\n </div>\n );\n }\n return (\n <div className=\"flex h-44 items-end gap-1\">\n {rows.map((row) => (\n <div\n key={row.date}\n className=\"group flex min-w-0 flex-1 flex-col items-center gap-2\"\n >\n <div className=\"relative flex h-36 w-full items-end rounded-sm bg-muted/60\">\n <div\n className=\"w-full rounded-sm bg-foreground transition group-hover:bg-primary\"\n style={{ height: `${Math.max(4, (row.calls / max) * 100)}%` }}\n />\n </div>\n <div className=\"hidden text-[10px] text-muted-foreground sm:block\">\n {row.date.slice(5)}\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction AppAccessTable({ rows }: { rows: AppAccessMetric[] }) {\n const visibleRows = rows.filter((row) => !row.isDispatch);\n if (visibleRows.length === 0) {\n return (\n <div className=\"rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground\">\n No workspace apps discovered yet.\n </div>\n );\n }\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[720px] text-left text-xs\">\n <thead>\n <tr className=\"border-b text-muted-foreground\">\n <th className=\"px-2 py-2 font-medium\">App</th>\n <th className=\"px-2 py-2 font-medium\">Access</th>\n <th className=\"px-2 py-2 text-right font-medium\">Users</th>\n <th className=\"px-2 py-2 text-right font-medium\">Chats</th>\n <th className=\"px-2 py-2 text-right font-medium\">Cost</th>\n <th className=\"px-2 py-2 text-right font-medium\">Last activity</th>\n </tr>\n </thead>\n <tbody>\n {visibleRows.map((row) => (\n <tr key={row.id} className=\"border-b last:border-0\">\n <td className=\"px-2 py-3\">\n <div className=\"font-medium text-foreground\">{row.name}</div>\n <div className=\"font-mono text-[11px] text-muted-foreground\">\n {row.path}\n </div>\n </td>\n <td className=\"px-2 py-3\">\n <Badge\n variant=\"outline\"\n className={cn(\n row.status === \"pending\" &&\n \"border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300\",\n )}\n >\n {row.status === \"pending\" ? \"Building\" : row.accessLabel}\n </Badge>\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatNumber(row.usersWithUsage)} /{\" \"}\n {formatNumber(row.accessUsers)}\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatNumber(row.chatCalls)}\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatCost(row.costCents)}\n </td>\n <td className=\"px-2 py-3 text-right text-muted-foreground\">\n {timeAgo(row.lastActiveAt)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nfunction UserTable({ rows }: { rows: UserUsageMetric[] }) {\n if (rows.length === 0) {\n return (\n <div className=\"rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground\">\n No users have triggered LLM usage in this window.\n </div>\n );\n }\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[760px] text-left text-xs\">\n <thead>\n <tr className=\"border-b text-muted-foreground\">\n <th className=\"px-2 py-2 font-medium\">User</th>\n <th className=\"px-2 py-2 font-medium\">Role</th>\n <th className=\"px-2 py-2 font-medium\">Top app</th>\n <th className=\"px-2 py-2 text-right font-medium\">Chats</th>\n <th className=\"px-2 py-2 text-right font-medium\">Threads</th>\n <th className=\"px-2 py-2 text-right font-medium\">Tokens</th>\n <th className=\"px-2 py-2 text-right font-medium\">Cost</th>\n </tr>\n </thead>\n <tbody>\n {rows.slice(0, 12).map((row) => (\n <tr key={row.ownerEmail} className=\"border-b last:border-0\">\n <td className=\"max-w-64 px-2 py-3\">\n <div className=\"truncate font-medium text-foreground\">\n {row.ownerEmail}\n </div>\n <div className=\"text-muted-foreground\">\n {timeAgo(row.lastActiveAt ?? row.lastChatAt)}\n </div>\n </td>\n <td className=\"px-2 py-3\">\n <Badge variant=\"secondary\">{row.role ?? \"user\"}</Badge>\n </td>\n <td className=\"px-2 py-3 text-muted-foreground\">\n {displayApp(row.topApp)}\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatNumber(row.chatCalls)}\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatNumber(row.chatThreads)}\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatTokens(row.inputTokens + row.outputTokens)}\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatCost(row.costCents)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nfunction CompactBreakdown({\n rows,\n empty,\n}: {\n rows: UsageMetricBucket[];\n empty: string;\n}) {\n const max = maxCost(rows);\n if (rows.length === 0) {\n return <div className=\"text-sm text-muted-foreground\">{empty}</div>;\n }\n return (\n <div className=\"space-y-3\">\n {rows.slice(0, 6).map((row) => (\n <div key={row.key} className=\"space-y-1\">\n <div className=\"flex items-center justify-between gap-3 text-xs\">\n <span className=\"truncate font-medium text-foreground\">\n {row.label}\n </span>\n <span className=\"shrink-0 tabular-nums text-muted-foreground\">\n {formatCost(row.costCents)}\n </span>\n </div>\n <div className=\"h-1.5 overflow-hidden rounded-full bg-muted\">\n <div\n className=\"h-full rounded-full bg-muted-foreground\"\n style={{ width: barWidth(row.costCents, max) }}\n />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction RecentTable({ rows }: { rows: RecentUsageMetric[] }) {\n if (rows.length === 0) {\n return (\n <div className=\"rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground\">\n No recent LLM calls.\n </div>\n );\n }\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[760px] text-left text-xs\">\n <thead>\n <tr className=\"border-b text-muted-foreground\">\n <th className=\"px-2 py-2 font-medium\">When</th>\n <th className=\"px-2 py-2 font-medium\">User</th>\n <th className=\"px-2 py-2 font-medium\">App</th>\n <th className=\"px-2 py-2 font-medium\">Label</th>\n <th className=\"px-2 py-2 font-medium\">Model</th>\n <th className=\"px-2 py-2 text-right font-medium\">Cost</th>\n </tr>\n </thead>\n <tbody>\n {rows.slice(0, 10).map((row) => (\n <tr key={row.id} className=\"border-b last:border-0\">\n <td className=\"px-2 py-3 text-muted-foreground\">\n {timeAgo(row.createdAt)}\n </td>\n <td className=\"max-w-56 px-2 py-3\">\n <div className=\"truncate text-foreground\">{row.ownerEmail}</div>\n </td>\n <td className=\"px-2 py-3 text-muted-foreground\">\n {displayApp(row.app)}\n </td>\n <td className=\"px-2 py-3\">\n <Badge variant=\"outline\">{row.label}</Badge>\n </td>\n <td className=\"max-w-48 px-2 py-3\">\n <div className=\"truncate text-muted-foreground\">\n {row.model}\n </div>\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatCost(row.costCents)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nexport default function MetricsRoute() {\n const [sinceDays, setSinceDays] = useState(30);\n const { data, isLoading, error } = useActionQuery(\n \"list-dispatch-usage-metrics\",\n { sinceDays },\n { refetchInterval: 30_000 },\n );\n const metrics = data as DispatchUsageMetrics | undefined;\n const totalTokens = useMemo(() => {\n if (!metrics) return 0;\n return (\n metrics.totals.inputTokens +\n metrics.totals.outputTokens +\n metrics.totals.cacheReadTokens +\n metrics.totals.cacheWriteTokens\n );\n }, [metrics]);\n\n return (\n <DispatchShell\n title=\"Metrics\"\n description=\"Workspace-wide LLM spend, chat volume, user activity, and app access.\"\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"text-sm text-muted-foreground\">\n {metrics?.access.scope === \"organization\"\n ? `${metrics.access.totalUsers} workspace users`\n : `${metrics?.access.totalUsers ?? 0} signed-in users`}\n </div>\n <RangeSelector value={sinceDays} onChange={setSinceDays} />\n </div>\n\n {error ? (\n <Alert variant=\"destructive\">\n <IconAlertTriangle className=\"h-4 w-4\" />\n <AlertTitle>Metrics unavailable</AlertTitle>\n <AlertDescription>\n {error instanceof Error ? error.message : \"Unable to load usage.\"}\n </AlertDescription>\n </Alert>\n ) : null}\n\n {isLoading && !metrics ? <LoadingMetrics /> : null}\n\n {metrics ? (\n <>\n <div className=\"grid gap-3 md:grid-cols-2 xl:grid-cols-5\">\n <MetricCard\n label=\"Estimated spend\"\n value={formatCost(metrics.totals.costCents)}\n detail={`${formatTokens(totalTokens)} total tokens`}\n icon={<IconCoin size={17} />}\n />\n <MetricCard\n label=\"LLM calls\"\n value={formatNumber(metrics.totals.calls)}\n detail={`${formatNumber(metrics.totals.chatCalls)} chat turns`}\n icon={<IconActivity size={17} />}\n />\n <MetricCard\n label=\"Active users\"\n value={formatNumber(metrics.totals.activeUsers)}\n detail={`${formatNumber(metrics.access.totalUsers)} users with access`}\n icon={<IconUsersGroup size={17} />}\n />\n <MetricCard\n label=\"Workspace apps\"\n value={formatNumber(metrics.totals.workspaceApps)}\n detail={`${formatNumber(metrics.byApp.length)} with usage`}\n icon={<IconApps size={17} />}\n />\n <MetricCard\n label=\"Chat threads\"\n value={formatNumber(metrics.totals.chatThreads)}\n detail={`${formatNumber(metrics.totals.chatMessages)} messages`}\n icon={<IconMessages size={17} />}\n />\n </div>\n\n <div className=\"grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(320px,0.65fr)]\">\n <Panel title=\"Spend By App\" icon={<IconChartBar size={16} />}>\n <AppSpendRows rows={metrics.byApp} />\n </Panel>\n <Panel title=\"Daily Activity\" icon={<IconClockHour4 size={16} />}>\n <DailyActivity rows={metrics.daily} />\n </Panel>\n </div>\n\n <Panel title=\"Access By App\" icon={<IconApps size={16} />}>\n <AppAccessTable rows={metrics.appAccess} />\n </Panel>\n\n <Panel title=\"Users\" icon={<IconUsersGroup size={16} />}>\n <UserTable rows={metrics.byUser} />\n </Panel>\n\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Panel title=\"Models\" icon={<IconChartBar size={16} />}>\n <CompactBreakdown\n rows={metrics.byModel}\n empty=\"No model usage in this window.\"\n />\n </Panel>\n <Panel title=\"Work Types\" icon={<IconActivity size={16} />}>\n <CompactBreakdown\n rows={metrics.byLabel}\n empty=\"No labeled usage in this window.\"\n />\n </Panel>\n </div>\n\n <Panel title=\"Recent LLM Calls\" icon={<IconActivity size={16} />}>\n <RecentTable rows={metrics.recent} />\n </Panel>\n </>\n ) : null}\n </div>\n </DispatchShell>\n );\n}\n"]}
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../../src/routes/pages/metrics.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { useActionQuery } from \"@agent-native/core/client\";\nimport {\n IconActivity,\n IconAlertTriangle,\n IconApps,\n IconChartBar,\n IconClockHour4,\n IconCoin,\n IconMessages,\n IconUsersGroup,\n} from \"@tabler/icons-react\";\nimport { DispatchShell } from \"@/components/dispatch-shell\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { cn } from \"@/lib/utils\";\n\nexport function meta() {\n return [{ title: \"Metrics — Dispatch\" }];\n}\n\ninterface UsageMetricBucket {\n key: string;\n label: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n activeUsers: number;\n lastActiveAt: number | null;\n}\n\ninterface UserUsageMetric extends UsageMetricBucket {\n ownerEmail: string;\n chatThreads: number;\n chatMessages: number;\n lastChatAt: number | null;\n topApp: string | null;\n role: string | null;\n}\n\ninterface AppAccessMetric {\n id: string;\n name: string;\n path: string;\n status?: \"ready\" | \"pending\";\n isDispatch: boolean;\n accessLabel: string;\n accessUsers: number;\n usersWithUsage: number;\n usageCalls: number;\n chatCalls: number;\n costCents: number;\n lastActiveAt: number | null;\n}\n\ninterface DailyUsageMetric {\n date: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n activeUsers: number;\n}\n\ninterface RecentUsageMetric {\n id: number;\n createdAt: number;\n ownerEmail: string;\n app: string;\n label: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n costCents: number;\n}\n\ninterface UsageBillingMode {\n unit: \"usd\" | \"builder-credits\";\n label: string;\n shortLabel: string;\n source: \"estimated-provider-cost\" | \"builder-agent-credits\";\n hardCostMarginMultiplier?: number;\n creditsPerUsd?: number;\n}\n\ninterface DispatchUsageMetrics {\n billing?: UsageBillingMode;\n sinceDays: number;\n access: {\n viewerEmail: string;\n orgId: string | null;\n role: string | null;\n scope: \"organization\" | \"solo\";\n totalUsers: number;\n };\n totals: {\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n activeUsers: number;\n chatThreads: number;\n chatMessages: number;\n workspaceApps: number;\n };\n byApp: UsageMetricBucket[];\n byUser: UserUsageMetric[];\n byLabel: UsageMetricBucket[];\n byModel: UsageMetricBucket[];\n daily: DailyUsageMetric[];\n appAccess: AppAccessMetric[];\n recent: RecentUsageMetric[];\n}\n\nconst RANGES = [7, 30, 90] as const;\n\nconst USD_BILLING: UsageBillingMode = {\n unit: \"usd\",\n label: \"Estimated spend\",\n shortLabel: \"Cost\",\n source: \"estimated-provider-cost\",\n};\n\nfunction displayAmountFromCostCents(\n cents: number,\n billing: UsageBillingMode,\n): number {\n if (billing.unit !== \"builder-credits\") return cents;\n const margin = billing.hardCostMarginMultiplier ?? 1.25;\n const creditsPerUsd = billing.creditsPerUsd ?? 20;\n const credits = (cents / 100) * margin * creditsPerUsd;\n return credits <= 0 ? 0 : Math.ceil(credits * 1000) / 1000;\n}\n\nfunction formatCredits(credits: number): string {\n if (!Number.isFinite(credits) || credits === 0) return \"0 credits\";\n const maximumFractionDigits = credits < 1 ? 3 : credits < 10 ? 2 : 1;\n const value = credits.toLocaleString(undefined, {\n maximumFractionDigits,\n });\n return `${value} ${credits === 1 ? \"credit\" : \"credits\"}`;\n}\n\nfunction formatSpend(cents: number, billing: UsageBillingMode): string {\n if (billing.unit === \"builder-credits\") {\n return formatCredits(displayAmountFromCostCents(cents, billing));\n }\n if (!Number.isFinite(cents) || cents === 0) return \"$0.00\";\n if (Math.abs(cents) < 1) return `${cents.toFixed(3)}¢`;\n if (Math.abs(cents) < 100) return `${cents.toFixed(2)}¢`;\n return (cents / 100).toLocaleString(undefined, {\n style: \"currency\",\n currency: \"USD\",\n maximumFractionDigits: 2,\n });\n}\n\nfunction formatNumber(value: number): string {\n return new Intl.NumberFormat(undefined, {\n notation: value >= 10_000 ? \"compact\" : \"standard\",\n maximumFractionDigits: value >= 10_000 ? 1 : 0,\n }).format(value);\n}\n\nfunction formatTokens(value: number): string {\n return new Intl.NumberFormat(undefined, {\n notation: \"compact\",\n maximumFractionDigits: 1,\n }).format(value);\n}\n\nfunction timeAgo(timestamp: number | null): string {\n if (!timestamp) return \"No activity\";\n const diff = Date.now() - timestamp;\n if (diff < 60_000) return \"just now\";\n if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;\n if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;\n return `${Math.floor(diff / 86_400_000)}d ago`;\n}\n\nfunction displayApp(value: string | null | undefined): string {\n const trimmed = value?.trim();\n if (!trimmed || trimmed === \"unattributed\") return \"Unattributed\";\n return trimmed;\n}\n\nfunction maxSpend(\n rows: Array<{ costCents: number }>,\n billing: UsageBillingMode,\n): number {\n return rows.reduce(\n (max, row) =>\n Math.max(max, displayAmountFromCostCents(row.costCents, billing)),\n 0,\n );\n}\n\nfunction barWidth(value: number, max: number): string {\n if (max <= 0 || value <= 0) return \"0%\";\n return `${Math.max(4, Math.round((value / max) * 100))}%`;\n}\n\nfunction RangeSelector({\n value,\n onChange,\n}: {\n value: number;\n onChange: (value: number) => void;\n}) {\n return (\n <div className=\"flex rounded-md border bg-card p-0.5\">\n {RANGES.map((range) => (\n <Button\n key={range}\n type=\"button\"\n variant={value === range ? \"secondary\" : \"ghost\"}\n size=\"sm\"\n className=\"h-7 px-3 text-xs\"\n onClick={() => onChange(range)}\n >\n {range}d\n </Button>\n ))}\n </div>\n );\n}\n\nfunction MetricCard({\n label,\n value,\n detail,\n icon,\n}: {\n label: string;\n value: string;\n detail: string;\n icon: ReactNode;\n}) {\n return (\n <div className=\"rounded-lg border bg-card p-4\">\n <div className=\"mb-3 flex items-center justify-between gap-3\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n {label}\n </span>\n <span className=\"text-muted-foreground\">{icon}</span>\n </div>\n <div className=\"text-2xl font-semibold tabular-nums text-foreground\">\n {value}\n </div>\n <div className=\"mt-1 truncate text-xs text-muted-foreground\">\n {detail}\n </div>\n </div>\n );\n}\n\nfunction Panel({\n title,\n icon,\n children,\n action,\n}: {\n title: string;\n icon: ReactNode;\n children: ReactNode;\n action?: ReactNode;\n}) {\n return (\n <section className=\"rounded-lg border bg-card\">\n <div className=\"flex items-center justify-between gap-3 border-b px-4 py-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span className=\"text-muted-foreground\">{icon}</span>\n <h2 className=\"truncate text-sm font-semibold text-foreground\">\n {title}\n </h2>\n </div>\n {action}\n </div>\n <div className=\"p-4\">{children}</div>\n </section>\n );\n}\n\nfunction LoadingMetrics() {\n return (\n <div className=\"space-y-4\">\n <div className=\"grid gap-3 md:grid-cols-2 xl:grid-cols-5\">\n {Array.from({ length: 5 }).map((_, index) => (\n <div key={index} className=\"rounded-lg border bg-card p-4\">\n <Skeleton className=\"mb-4 h-4 w-24\" />\n <Skeleton className=\"h-7 w-20\" />\n <Skeleton className=\"mt-3 h-3 w-28\" />\n </div>\n ))}\n </div>\n <Skeleton className=\"h-80 rounded-lg\" />\n </div>\n );\n}\n\nfunction AppSpendRows({\n rows,\n billing,\n}: {\n rows: UsageMetricBucket[];\n billing: UsageBillingMode;\n}) {\n const max = maxSpend(rows, billing);\n if (rows.length === 0) {\n return (\n <div className=\"rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground\">\n No LLM usage recorded for this window.\n </div>\n );\n }\n return (\n <div className=\"space-y-3\">\n {rows.map((row) => (\n <div key={row.key} className=\"space-y-1.5\">\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <div className=\"min-w-0\">\n <div className=\"truncate font-medium text-foreground\">\n {displayApp(row.key)}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {formatNumber(row.chatCalls)} chats ·{\" \"}\n {formatNumber(row.activeUsers)} users\n </div>\n </div>\n <div className=\"shrink-0 text-right\">\n <div className=\"font-medium tabular-nums text-foreground\">\n {formatSpend(row.costCents, billing)}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {formatNumber(row.calls)} calls\n </div>\n </div>\n </div>\n <div className=\"h-2 overflow-hidden rounded-full bg-muted\">\n <div\n className=\"h-full rounded-full bg-foreground\"\n style={{\n width: barWidth(\n displayAmountFromCostCents(row.costCents, billing),\n max,\n ),\n }}\n />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction DailyActivity({ rows }: { rows: DailyUsageMetric[] }) {\n const max = Math.max(\n 1,\n rows.reduce((value, row) => Math.max(value, row.calls), 0),\n );\n if (rows.length === 0) {\n return (\n <div className=\"rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground\">\n No activity in this window.\n </div>\n );\n }\n return (\n <div className=\"flex h-44 items-end gap-1\">\n {rows.map((row) => (\n <div\n key={row.date}\n className=\"group flex min-w-0 flex-1 flex-col items-center gap-2\"\n >\n <div className=\"relative flex h-36 w-full items-end rounded-sm bg-muted/60\">\n <div\n className=\"w-full rounded-sm bg-foreground transition group-hover:bg-primary\"\n style={{ height: `${Math.max(4, (row.calls / max) * 100)}%` }}\n />\n </div>\n <div className=\"hidden text-[10px] text-muted-foreground sm:block\">\n {row.date.slice(5)}\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction AppAccessTable({\n rows,\n billing,\n}: {\n rows: AppAccessMetric[];\n billing: UsageBillingMode;\n}) {\n const visibleRows = rows.filter((row) => !row.isDispatch);\n if (visibleRows.length === 0) {\n return (\n <div className=\"rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground\">\n No workspace apps discovered yet.\n </div>\n );\n }\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[720px] text-left text-xs\">\n <thead>\n <tr className=\"border-b text-muted-foreground\">\n <th className=\"px-2 py-2 font-medium\">App</th>\n <th className=\"px-2 py-2 font-medium\">Access</th>\n <th className=\"px-2 py-2 text-right font-medium\">Users</th>\n <th className=\"px-2 py-2 text-right font-medium\">Chats</th>\n <th className=\"px-2 py-2 text-right font-medium\">\n {billing.shortLabel}\n </th>\n <th className=\"px-2 py-2 text-right font-medium\">Last activity</th>\n </tr>\n </thead>\n <tbody>\n {visibleRows.map((row) => (\n <tr key={row.id} className=\"border-b last:border-0\">\n <td className=\"px-2 py-3\">\n <div className=\"font-medium text-foreground\">{row.name}</div>\n <div className=\"font-mono text-[11px] text-muted-foreground\">\n {row.path}\n </div>\n </td>\n <td className=\"px-2 py-3\">\n <Badge\n variant=\"outline\"\n className={cn(\n row.status === \"pending\" &&\n \"border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300\",\n )}\n >\n {row.status === \"pending\" ? \"Building\" : row.accessLabel}\n </Badge>\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatNumber(row.usersWithUsage)} /{\" \"}\n {formatNumber(row.accessUsers)}\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatNumber(row.chatCalls)}\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatSpend(row.costCents, billing)}\n </td>\n <td className=\"px-2 py-3 text-right text-muted-foreground\">\n {timeAgo(row.lastActiveAt)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nfunction UserTable({\n rows,\n billing,\n}: {\n rows: UserUsageMetric[];\n billing: UsageBillingMode;\n}) {\n if (rows.length === 0) {\n return (\n <div className=\"rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground\">\n No users have triggered LLM usage in this window.\n </div>\n );\n }\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[760px] text-left text-xs\">\n <thead>\n <tr className=\"border-b text-muted-foreground\">\n <th className=\"px-2 py-2 font-medium\">User</th>\n <th className=\"px-2 py-2 font-medium\">Role</th>\n <th className=\"px-2 py-2 font-medium\">Top app</th>\n <th className=\"px-2 py-2 text-right font-medium\">Chats</th>\n <th className=\"px-2 py-2 text-right font-medium\">Threads</th>\n <th className=\"px-2 py-2 text-right font-medium\">Tokens</th>\n <th className=\"px-2 py-2 text-right font-medium\">\n {billing.shortLabel}\n </th>\n </tr>\n </thead>\n <tbody>\n {rows.slice(0, 12).map((row) => (\n <tr key={row.ownerEmail} className=\"border-b last:border-0\">\n <td className=\"max-w-64 px-2 py-3\">\n <div className=\"truncate font-medium text-foreground\">\n {row.ownerEmail}\n </div>\n <div className=\"text-muted-foreground\">\n {timeAgo(row.lastActiveAt ?? row.lastChatAt)}\n </div>\n </td>\n <td className=\"px-2 py-3\">\n <Badge variant=\"secondary\">{row.role ?? \"user\"}</Badge>\n </td>\n <td className=\"px-2 py-3 text-muted-foreground\">\n {displayApp(row.topApp)}\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatNumber(row.chatCalls)}\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatNumber(row.chatThreads)}\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatTokens(row.inputTokens + row.outputTokens)}\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatSpend(row.costCents, billing)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nfunction CompactBreakdown({\n rows,\n empty,\n billing,\n}: {\n rows: UsageMetricBucket[];\n empty: string;\n billing: UsageBillingMode;\n}) {\n const max = maxSpend(rows, billing);\n if (rows.length === 0) {\n return <div className=\"text-sm text-muted-foreground\">{empty}</div>;\n }\n return (\n <div className=\"space-y-3\">\n {rows.slice(0, 6).map((row) => (\n <div key={row.key} className=\"space-y-1\">\n <div className=\"flex items-center justify-between gap-3 text-xs\">\n <span className=\"truncate font-medium text-foreground\">\n {row.label}\n </span>\n <span className=\"shrink-0 tabular-nums text-muted-foreground\">\n {formatSpend(row.costCents, billing)}\n </span>\n </div>\n <div className=\"h-1.5 overflow-hidden rounded-full bg-muted\">\n <div\n className=\"h-full rounded-full bg-muted-foreground\"\n style={{\n width: barWidth(\n displayAmountFromCostCents(row.costCents, billing),\n max,\n ),\n }}\n />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction RecentTable({\n rows,\n billing,\n}: {\n rows: RecentUsageMetric[];\n billing: UsageBillingMode;\n}) {\n if (rows.length === 0) {\n return (\n <div className=\"rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground\">\n No recent LLM calls.\n </div>\n );\n }\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[760px] text-left text-xs\">\n <thead>\n <tr className=\"border-b text-muted-foreground\">\n <th className=\"px-2 py-2 font-medium\">When</th>\n <th className=\"px-2 py-2 font-medium\">User</th>\n <th className=\"px-2 py-2 font-medium\">App</th>\n <th className=\"px-2 py-2 font-medium\">Label</th>\n <th className=\"px-2 py-2 font-medium\">Model</th>\n <th className=\"px-2 py-2 text-right font-medium\">\n {billing.shortLabel}\n </th>\n </tr>\n </thead>\n <tbody>\n {rows.slice(0, 10).map((row) => (\n <tr key={row.id} className=\"border-b last:border-0\">\n <td className=\"px-2 py-3 text-muted-foreground\">\n {timeAgo(row.createdAt)}\n </td>\n <td className=\"max-w-56 px-2 py-3\">\n <div className=\"truncate text-foreground\">{row.ownerEmail}</div>\n </td>\n <td className=\"px-2 py-3 text-muted-foreground\">\n {displayApp(row.app)}\n </td>\n <td className=\"px-2 py-3\">\n <Badge variant=\"outline\">{row.label}</Badge>\n </td>\n <td className=\"max-w-48 px-2 py-3\">\n <div className=\"truncate text-muted-foreground\">\n {row.model}\n </div>\n </td>\n <td className=\"px-2 py-3 text-right tabular-nums\">\n {formatSpend(row.costCents, billing)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nexport default function MetricsRoute() {\n const [sinceDays, setSinceDays] = useState(30);\n const { data, isLoading, error } = useActionQuery(\n \"list-dispatch-usage-metrics\",\n { sinceDays },\n { refetchInterval: 30_000 },\n );\n const metrics = data as DispatchUsageMetrics | undefined;\n const billing = metrics?.billing ?? USD_BILLING;\n const totalTokens = useMemo(() => {\n if (!metrics) return 0;\n return (\n metrics.totals.inputTokens +\n metrics.totals.outputTokens +\n metrics.totals.cacheReadTokens +\n metrics.totals.cacheWriteTokens\n );\n }, [metrics]);\n\n return (\n <DispatchShell\n title=\"Metrics\"\n description={\n billing.unit === \"builder-credits\"\n ? \"Workspace-wide Builder.io credit spend, chat volume, user activity, and app access.\"\n : \"Workspace-wide LLM spend, chat volume, user activity, and app access.\"\n }\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"text-sm text-muted-foreground\">\n {metrics?.access.scope === \"organization\"\n ? `${metrics.access.totalUsers} workspace users`\n : `${metrics?.access.totalUsers ?? 0} signed-in users`}\n </div>\n <RangeSelector value={sinceDays} onChange={setSinceDays} />\n </div>\n\n {error ? (\n <Alert variant=\"destructive\">\n <IconAlertTriangle className=\"h-4 w-4\" />\n <AlertTitle>Metrics unavailable</AlertTitle>\n <AlertDescription>\n {error instanceof Error ? error.message : \"Unable to load usage.\"}\n </AlertDescription>\n </Alert>\n ) : null}\n\n {isLoading && !metrics ? <LoadingMetrics /> : null}\n\n {metrics ? (\n <>\n <div className=\"grid gap-3 md:grid-cols-2 xl:grid-cols-5\">\n <MetricCard\n label={billing.label}\n value={formatSpend(metrics.totals.costCents, billing)}\n detail={`${formatTokens(totalTokens)} total tokens`}\n icon={<IconCoin size={17} />}\n />\n <MetricCard\n label=\"LLM calls\"\n value={formatNumber(metrics.totals.calls)}\n detail={`${formatNumber(metrics.totals.chatCalls)} chat turns`}\n icon={<IconActivity size={17} />}\n />\n <MetricCard\n label=\"Active users\"\n value={formatNumber(metrics.totals.activeUsers)}\n detail={`${formatNumber(metrics.access.totalUsers)} users with access`}\n icon={<IconUsersGroup size={17} />}\n />\n <MetricCard\n label=\"Workspace apps\"\n value={formatNumber(metrics.totals.workspaceApps)}\n detail={`${formatNumber(metrics.byApp.length)} with usage`}\n icon={<IconApps size={17} />}\n />\n <MetricCard\n label=\"Chat threads\"\n value={formatNumber(metrics.totals.chatThreads)}\n detail={`${formatNumber(metrics.totals.chatMessages)} messages`}\n icon={<IconMessages size={17} />}\n />\n </div>\n\n <div className=\"grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(320px,0.65fr)]\">\n <Panel\n title={\n billing.unit === \"builder-credits\"\n ? \"Credit Spend By App\"\n : \"Spend By App\"\n }\n icon={<IconChartBar size={16} />}\n >\n <AppSpendRows rows={metrics.byApp} billing={billing} />\n </Panel>\n <Panel title=\"Daily Activity\" icon={<IconClockHour4 size={16} />}>\n <DailyActivity rows={metrics.daily} />\n </Panel>\n </div>\n\n <Panel title=\"Access By App\" icon={<IconApps size={16} />}>\n <AppAccessTable rows={metrics.appAccess} billing={billing} />\n </Panel>\n\n <Panel title=\"Users\" icon={<IconUsersGroup size={16} />}>\n <UserTable rows={metrics.byUser} billing={billing} />\n </Panel>\n\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Panel title=\"Models\" icon={<IconChartBar size={16} />}>\n <CompactBreakdown\n rows={metrics.byModel}\n empty=\"No model usage in this window.\"\n billing={billing}\n />\n </Panel>\n <Panel title=\"Work Types\" icon={<IconActivity size={16} />}>\n <CompactBreakdown\n rows={metrics.byLabel}\n empty=\"No labeled usage in this window.\"\n billing={billing}\n />\n </Panel>\n </div>\n\n <Panel title=\"Recent LLM Calls\" icon={<IconActivity size={16} />}>\n <RecentTable rows={metrics.recent} billing={billing} />\n </Panel>\n </>\n ) : null}\n </div>\n </DispatchShell>\n );\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { type UsageBillingMode } from "@agent-native/core/usage";
1
2
  import { type WorkspaceAppSummary } from "./app-creation-store.js";
2
3
  export interface UsageMetricBucket {
3
4
  key: string;
@@ -56,6 +57,7 @@ export interface RecentUsageMetric {
56
57
  costCents: number;
57
58
  }
58
59
  export interface DispatchUsageMetrics {
60
+ billing: UsageBillingMode;
59
61
  sinceMs: number;
60
62
  sinceDays: number;
61
63
  generatedAt: number;
@@ -1 +1 @@
1
- {"version":3,"file":"usage-metrics-store.d.ts","sourceRoot":"","sources":["../../../src/server/lib/usage-metrics-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"usage-metrics-store.d.ts","sourceRoot":"","sources":["../../../src/server/lib/usage-metrics-store.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,11 @@
1
- import { getUsageSummary } from "@agent-native/core/usage";
1
+ import { getUsageSummary, usageBillingForEngine, } from "@agent-native/core/usage";
2
2
  import { getDbExec } from "@agent-native/core/db";
3
+ import { detectEngineFromEnv, detectEngineFromUserSecrets, getAgentEngineEntry, isAgentEngineSettingConfigured, isStoredEngineUsable, registerBuiltinEngines, } from "@agent-native/core/agent/engine";
4
+ import { getSetting } from "@agent-native/core/settings";
3
5
  import { currentOrgId, currentOwnerEmail } from "./dispatch-store.js";
4
6
  import { listWorkspaceApps, } from "./app-creation-store.js";
5
7
  const DAY_MS = 86_400_000;
8
+ registerBuiltinEngines();
6
9
  function numberField(row, key) {
7
10
  return Number(row[key] ?? 0) || 0;
8
11
  }
@@ -40,6 +43,27 @@ function isEnvAdmin(email) {
40
43
  ...envEmails("DISPATCH_DEFAULT_OWNER_EMAIL"),
41
44
  ].includes(normalized);
42
45
  }
46
+ async function detectUsageEngineName() {
47
+ try {
48
+ const stored = (await getSetting("agent-engine"));
49
+ if (isAgentEngineSettingConfigured(stored)) {
50
+ return stored.engine;
51
+ }
52
+ if (stored && typeof stored.engine === "string") {
53
+ const entry = getAgentEngineEntry(stored.engine);
54
+ if (entry && isStoredEngineUsable(stored, entry)) {
55
+ return stored.engine;
56
+ }
57
+ }
58
+ const detectedFromUser = await detectEngineFromUserSecrets();
59
+ if (detectedFromUser)
60
+ return detectedFromUser.name;
61
+ return detectEngineFromEnv()?.name ?? null;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
43
67
  async function queryRows(sql, args = []) {
44
68
  try {
45
69
  const result = await getDbExec().execute({ sql, args });
@@ -179,6 +203,7 @@ export async function listDispatchUsageMetrics(input) {
179
203
  const { viewerEmail, orgId, role } = await assertCanViewMetrics();
180
204
  const sinceDays = Math.max(1, Math.min(365, input.sinceDays ?? 30));
181
205
  const sinceMs = Date.now() - sinceDays * DAY_MS;
206
+ const billing = usageBillingForEngine(await detectUsageEngineName());
182
207
  // Initializes token_usage on fresh deployments before the read-only
183
208
  // aggregate queries below. The fake owner avoids changing visible data.
184
209
  await getUsageSummary({ ownerEmail: "__dispatch_metrics_init__", sinceMs });
@@ -347,6 +372,7 @@ export async function listDispatchUsageMetrics(input) {
347
372
  messages: acc.messages + value.messages,
348
373
  }), { threads: 0, messages: 0 });
349
374
  return {
375
+ billing,
350
376
  sinceMs,
351
377
  sinceDays,
352
378
  generatedAt: Date.now(),
@@ -1 +1 @@
1
- {"version":3,"file":"usage-metrics-store.js","sourceRoot":"","sources":["../../../src/server/lib/usage-metrics-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { getUsageSummary } from \"@agent-native/core/usage\";\nimport { getDbExec } from \"@agent-native/core/db\";\nimport { currentOrgId, currentOwnerEmail } from \"./dispatch-store.js\";\nimport {\n listWorkspaceApps,\n type WorkspaceAppSummary,\n} from \"./app-creation-store.js\";\n\nconst DAY_MS = 86_400_000;\n\nexport interface UsageMetricBucket {\n key: string;\n label: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n activeUsers: number;\n lastActiveAt: number | null;\n}\n\nexport interface UserUsageMetric extends UsageMetricBucket {\n ownerEmail: string;\n chatThreads: number;\n chatMessages: number;\n lastChatAt: number | null;\n topApp: string | null;\n role: string | null;\n}\n\nexport interface AppAccessMetric {\n id: string;\n name: string;\n path: string;\n status: WorkspaceAppSummary[\"status\"];\n isDispatch: boolean;\n accessModel: \"workspace\" | \"solo\";\n accessLabel: string;\n accessUsers: number;\n usersWithUsage: number;\n usageCalls: number;\n chatCalls: number;\n costCents: number;\n lastActiveAt: number | null;\n}\n\nexport interface DailyUsageMetric {\n date: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n activeUsers: number;\n}\n\nexport interface RecentUsageMetric {\n id: number;\n createdAt: number;\n ownerEmail: string;\n app: string;\n label: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n costCents: number;\n}\n\nexport interface DispatchUsageMetrics {\n sinceMs: number;\n sinceDays: number;\n generatedAt: number;\n access: {\n viewerEmail: string;\n orgId: string | null;\n role: string | null;\n scope: \"organization\" | \"solo\";\n totalUsers: number;\n };\n totals: {\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n activeUsers: number;\n chatThreads: number;\n chatMessages: number;\n workspaceApps: number;\n };\n byApp: UsageMetricBucket[];\n byUser: UserUsageMetric[];\n byLabel: UsageMetricBucket[];\n byModel: UsageMetricBucket[];\n daily: DailyUsageMetric[];\n appAccess: AppAccessMetric[];\n recent: RecentUsageMetric[];\n}\n\ninterface MemberRecord {\n email: string;\n role: string | null;\n joinedAt: number | null;\n}\n\ninterface ChatStats {\n threads: number;\n messages: number;\n lastChatAt: number | null;\n}\n\nfunction numberField(row: Record<string, unknown>, key: string): number {\n return Number(row[key] ?? 0) || 0;\n}\n\nfunction stringField(row: Record<string, unknown>, key: string): string {\n return String(row[key] ?? \"\");\n}\n\nfunction nullableNumberField(\n row: Record<string, unknown>,\n key: string,\n): number | null {\n const value = row[key];\n if (value == null) return null;\n const numberValue = Number(value);\n return Number.isFinite(numberValue) ? numberValue : null;\n}\n\nfunction labelForKey(value: string): string {\n const trimmed = value.trim();\n return trimmed || \"Unattributed\";\n}\n\nfunction normalizeAppKey(value: string | null | undefined): string {\n const raw = (value ?? \"\").trim().toLowerCase();\n if (!raw) return \"unattributed\";\n return raw.replace(/^agent-native-/, \"\");\n}\n\nfunction envEmails(name: string): string[] {\n return (process.env[name] ?? \"\")\n .split(\",\")\n .map((value) => value.trim().toLowerCase())\n .filter(Boolean);\n}\n\nfunction isEnvAdmin(email: string): boolean {\n const normalized = email.trim().toLowerCase();\n return [\n ...envEmails(\"DISPATCH_ADMIN_EMAILS\"),\n ...envEmails(\"WORKSPACE_OWNER_EMAIL\"),\n ...envEmails(\"DISPATCH_DEFAULT_OWNER_EMAIL\"),\n ].includes(normalized);\n}\n\nasync function queryRows<T extends Record<string, unknown>>(\n sql: string,\n args: unknown[] = [],\n): Promise<T[]> {\n try {\n const result = await getDbExec().execute({ sql, args });\n return result.rows as T[];\n } catch {\n return [];\n }\n}\n\nasync function getViewerOrgRole(\n orgId: string | null,\n email: string,\n): Promise<string | null> {\n if (!orgId) return null;\n const rows = await queryRows<{ role?: string }>(\n `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,\n [orgId, email.toLowerCase()],\n );\n const role = rows[0]?.role;\n return typeof role === \"string\" ? role : null;\n}\n\nasync function listOrgMembers(orgId: string | null): Promise<MemberRecord[]> {\n if (!orgId) return [];\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT email, role, joined_at AS joined_at FROM org_members WHERE org_id = ? ORDER BY joined_at ASC`,\n [orgId],\n );\n return rows\n .map((row) => ({\n email: stringField(row, \"email\").trim(),\n role: stringField(row, \"role\") || null,\n joinedAt: nullableNumberField(row, \"joined_at\"),\n }))\n .filter((member) => member.email);\n}\n\nasync function listSignedInUsers(): Promise<MemberRecord[]> {\n const authRows = await queryRows<Record<string, unknown>>(\n `SELECT email, created_at AS joined_at FROM \"user\" ORDER BY created_at ASC`,\n );\n if (authRows.length > 0) {\n return authRows\n .map((row) => ({\n email: stringField(row, \"email\").trim(),\n role: null,\n joinedAt: nullableNumberField(row, \"joined_at\"),\n }))\n .filter((member) => member.email);\n }\n\n const usageRows = await queryRows<{ email?: string }>(\n `SELECT DISTINCT owner_email AS email FROM token_usage`,\n );\n const threadRows = await queryRows<{ email?: string }>(\n `SELECT DISTINCT owner_email AS email FROM chat_threads`,\n );\n const emails = new Set<string>();\n for (const row of [...usageRows, ...threadRows]) {\n if (row.email) emails.add(row.email);\n }\n return [...emails].sort().map((email) => ({\n email,\n role: null,\n joinedAt: null,\n }));\n}\n\nfunction usageScope(\n sinceMs: number,\n memberEmails: string[],\n): { where: string; args: unknown[] } {\n if (memberEmails.length === 0) {\n return { where: \"created_at >= ?\", args: [sinceMs] };\n }\n const placeholders = memberEmails.map(() => \"?\").join(\", \");\n return {\n where: `created_at >= ? AND owner_email IN (${placeholders})`,\n args: [sinceMs, ...memberEmails],\n };\n}\n\nfunction threadScope(\n sinceMs: number,\n memberEmails: string[],\n): { where: string; args: unknown[] } {\n if (memberEmails.length === 0) {\n return { where: \"updated_at >= ?\", args: [sinceMs] };\n }\n const placeholders = memberEmails.map(() => \"?\").join(\", \");\n return {\n where: `updated_at >= ? AND owner_email IN (${placeholders})`,\n args: [sinceMs, ...memberEmails],\n };\n}\n\nfunction bucketFromRow(row: Record<string, unknown>): UsageMetricBucket {\n const key = stringField(row, \"k\");\n return {\n key,\n label: labelForKey(key),\n costCents: numberField(row, \"cost_x100\") / 100,\n calls: numberField(row, \"calls\"),\n chatCalls: numberField(row, \"chat_calls\"),\n inputTokens: numberField(row, \"input_tokens\"),\n outputTokens: numberField(row, \"output_tokens\"),\n cacheReadTokens: numberField(row, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(row, \"cache_write_tokens\"),\n activeUsers: numberField(row, \"active_users\"),\n lastActiveAt: nullableNumberField(row, \"last_active_at\"),\n };\n}\n\nasync function usageBuckets(\n columnExpression: string,\n where: string,\n args: unknown[],\n limit: number,\n): Promise<UsageMetricBucket[]> {\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT ${columnExpression} AS k,\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,\n COUNT(*) AS calls,\n SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,\n COALESCE(SUM(input_tokens), 0) AS input_tokens,\n COALESCE(SUM(output_tokens), 0) AS output_tokens,\n COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,\n COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,\n COUNT(DISTINCT owner_email) AS active_users,\n MAX(created_at) AS last_active_at\n FROM token_usage\n WHERE ${where}\n GROUP BY ${columnExpression}\n ORDER BY cost_x100 DESC\n LIMIT ?`,\n [...args, limit],\n );\n return rows.map(bucketFromRow);\n}\n\nasync function loadChatStats(\n where: string,\n args: unknown[],\n): Promise<Map<string, ChatStats>> {\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT owner_email AS owner_email,\n COUNT(*) AS threads,\n COALESCE(SUM(message_count), 0) AS messages,\n MAX(updated_at) AS last_chat_at\n FROM chat_threads\n WHERE ${where}\n GROUP BY owner_email`,\n args,\n );\n return new Map(\n rows.map((row) => [\n stringField(row, \"owner_email\"),\n {\n threads: numberField(row, \"threads\"),\n messages: numberField(row, \"messages\"),\n lastChatAt: nullableNumberField(row, \"last_chat_at\"),\n },\n ]),\n );\n}\n\nasync function assertCanViewMetrics(): Promise<{\n viewerEmail: string;\n orgId: string | null;\n role: string | null;\n}> {\n const viewerEmail = currentOwnerEmail();\n const orgId = currentOrgId();\n const role = await getViewerOrgRole(orgId, viewerEmail);\n if (isEnvAdmin(viewerEmail) || role === \"owner\" || role === \"admin\") {\n return { viewerEmail, orgId, role };\n }\n if (!orgId) {\n return { viewerEmail, orgId, role };\n }\n throw new Error(\n \"Only organization owners and admins can view workspace usage metrics.\",\n );\n}\n\nexport async function listDispatchUsageMetrics(input: {\n sinceDays?: number;\n}): Promise<DispatchUsageMetrics> {\n const { viewerEmail, orgId, role } = await assertCanViewMetrics();\n const sinceDays = Math.max(1, Math.min(365, input.sinceDays ?? 30));\n const sinceMs = Date.now() - sinceDays * DAY_MS;\n\n // Initializes token_usage on fresh deployments before the read-only\n // aggregate queries below. The fake owner avoids changing visible data.\n await getUsageSummary({ ownerEmail: \"__dispatch_metrics_init__\", sinceMs });\n\n const rawMembers = orgId\n ? await listOrgMembers(orgId)\n : await listSignedInUsers();\n const members =\n orgId && rawMembers.length === 0\n ? [{ email: viewerEmail, role, joinedAt: null }]\n : rawMembers;\n const memberEmails = orgId ? members.map((member) => member.email) : [];\n const memberByEmail = new Map(\n members.map((member) => [member.email.toLowerCase(), member]),\n );\n const usage = usageScope(sinceMs, memberEmails);\n const threads = threadScope(sinceMs, memberEmails);\n\n const [apps, totalsRows, byApp, byUserBase, byLabel, byModel, chatStats] =\n await Promise.all([\n listWorkspaceApps({ includeAgentCards: false }),\n queryRows<Record<string, unknown>>(\n `SELECT\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,\n COUNT(*) AS calls,\n SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,\n COALESCE(SUM(input_tokens), 0) AS input_tokens,\n COALESCE(SUM(output_tokens), 0) AS output_tokens,\n COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,\n COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,\n COUNT(DISTINCT owner_email) AS active_users\n FROM token_usage\n WHERE ${usage.where}`,\n usage.args,\n ),\n usageBuckets(\n `COALESCE(NULLIF(app, ''), 'unattributed')`,\n usage.where,\n usage.args,\n 20,\n ),\n usageBuckets(\"owner_email\", usage.where, usage.args, 50),\n usageBuckets(\n `COALESCE(NULLIF(label, ''), 'chat')`,\n usage.where,\n usage.args,\n 20,\n ),\n usageBuckets(\n `COALESCE(NULLIF(model, ''), 'unknown')`,\n usage.where,\n usage.args,\n 20,\n ),\n loadChatStats(threads.where, threads.args),\n ]);\n\n const topAppRows = await queryRows<Record<string, unknown>>(\n `SELECT owner_email AS owner_email,\n COALESCE(NULLIF(app, ''), 'unattributed') AS app,\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100\n FROM token_usage\n WHERE ${usage.where}\n GROUP BY owner_email, COALESCE(NULLIF(app, ''), 'unattributed')\n ORDER BY owner_email ASC, cost_x100 DESC`,\n usage.args,\n );\n const topAppByUser = new Map<string, string>();\n for (const row of topAppRows) {\n const email = stringField(row, \"owner_email\");\n if (!topAppByUser.has(email)) {\n topAppByUser.set(email, stringField(row, \"app\"));\n }\n }\n\n const byUserMap = new Map<string, UserUsageMetric>();\n for (const bucket of byUserBase) {\n const ownerEmail = bucket.key;\n const stats = chatStats.get(ownerEmail) ?? {\n threads: 0,\n messages: 0,\n lastChatAt: null,\n };\n const member = memberByEmail.get(ownerEmail.toLowerCase());\n byUserMap.set(ownerEmail, {\n ...bucket,\n ownerEmail,\n chatThreads: stats.threads,\n chatMessages: stats.messages,\n lastChatAt: stats.lastChatAt,\n topApp: topAppByUser.get(ownerEmail) ?? null,\n role: member?.role ?? null,\n });\n }\n for (const [ownerEmail, stats] of chatStats) {\n if (byUserMap.has(ownerEmail)) continue;\n const member = memberByEmail.get(ownerEmail.toLowerCase());\n byUserMap.set(ownerEmail, {\n key: ownerEmail,\n label: ownerEmail,\n ownerEmail,\n costCents: 0,\n calls: 0,\n chatCalls: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n activeUsers: 1,\n lastActiveAt: stats.lastChatAt,\n chatThreads: stats.threads,\n chatMessages: stats.messages,\n lastChatAt: stats.lastChatAt,\n topApp: null,\n role: member?.role ?? null,\n });\n }\n\n const dayRows = await queryRows<Record<string, unknown>>(\n `SELECT created_at, owner_email, label, cost_cents_x100\n FROM token_usage\n WHERE ${usage.where}\n ORDER BY created_at ASC`,\n usage.args,\n );\n const dailyMap = new Map<\n string,\n { costX100: number; calls: number; chatCalls: number; users: Set<string> }\n >();\n for (const row of dayRows) {\n const date = new Date(numberField(row, \"created_at\"))\n .toISOString()\n .slice(0, 10);\n const current = dailyMap.get(date) ?? {\n costX100: 0,\n calls: 0,\n chatCalls: 0,\n users: new Set<string>(),\n };\n current.costX100 += numberField(row, \"cost_cents_x100\");\n current.calls += 1;\n if (stringField(row, \"label\") === \"chat\") current.chatCalls += 1;\n current.users.add(stringField(row, \"owner_email\"));\n dailyMap.set(date, current);\n }\n const daily = [...dailyMap.entries()]\n .map(([date, value]) => ({\n date,\n costCents: value.costX100 / 100,\n calls: value.calls,\n chatCalls: value.chatCalls,\n activeUsers: value.users.size,\n }))\n .sort((a, b) => a.date.localeCompare(b.date));\n\n const recentRows = await queryRows<Record<string, unknown>>(\n `SELECT id, created_at, owner_email, app, label, model,\n input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,\n cost_cents_x100\n FROM token_usage\n WHERE ${usage.where}\n ORDER BY created_at DESC\n LIMIT 50`,\n usage.args,\n );\n const recent = recentRows.map((row) => ({\n id: numberField(row, \"id\"),\n createdAt: numberField(row, \"created_at\"),\n ownerEmail: stringField(row, \"owner_email\"),\n app: stringField(row, \"app\") || \"unattributed\",\n label: stringField(row, \"label\") || \"chat\",\n model: stringField(row, \"model\") || \"unknown\",\n inputTokens: numberField(row, \"input_tokens\"),\n outputTokens: numberField(row, \"output_tokens\"),\n cacheReadTokens: numberField(row, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(row, \"cache_write_tokens\"),\n costCents: numberField(row, \"cost_cents_x100\") / 100,\n }));\n\n const appUsageByKey = new Map(\n byApp.map((bucket) => [normalizeAppKey(bucket.key), bucket]),\n );\n const accessUsers = members.length || byUserMap.size;\n const accessModel = orgId ? \"workspace\" : \"solo\";\n const accessLabel = orgId ? \"Workspace members\" : \"Signed-in users\";\n const appAccess = apps.map((app) => {\n const usageBucket = appUsageByKey.get(normalizeAppKey(app.id));\n return {\n id: app.id,\n name: app.name,\n path: app.path,\n status: app.status,\n isDispatch: app.isDispatch,\n accessModel,\n accessLabel,\n accessUsers,\n usersWithUsage: usageBucket?.activeUsers ?? 0,\n usageCalls: usageBucket?.calls ?? 0,\n chatCalls: usageBucket?.chatCalls ?? 0,\n costCents: usageBucket?.costCents ?? 0,\n lastActiveAt: usageBucket?.lastActiveAt ?? null,\n } satisfies AppAccessMetric;\n });\n\n const totals = totalsRows[0] ?? {};\n const chatThreadTotals = [...chatStats.values()].reduce(\n (acc, value) => ({\n threads: acc.threads + value.threads,\n messages: acc.messages + value.messages,\n }),\n { threads: 0, messages: 0 },\n );\n\n return {\n sinceMs,\n sinceDays,\n generatedAt: Date.now(),\n access: {\n viewerEmail,\n orgId,\n role,\n scope: orgId ? \"organization\" : \"solo\",\n totalUsers: accessUsers,\n },\n totals: {\n costCents: numberField(totals, \"cost_x100\") / 100,\n calls: numberField(totals, \"calls\"),\n chatCalls: numberField(totals, \"chat_calls\"),\n inputTokens: numberField(totals, \"input_tokens\"),\n outputTokens: numberField(totals, \"output_tokens\"),\n cacheReadTokens: numberField(totals, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(totals, \"cache_write_tokens\"),\n activeUsers: numberField(totals, \"active_users\"),\n chatThreads: chatThreadTotals.threads,\n chatMessages: chatThreadTotals.messages,\n workspaceApps: apps.filter((app) => !app.isDispatch).length,\n },\n byApp,\n byUser: [...byUserMap.values()].sort((a, b) => {\n if (b.costCents !== a.costCents) return b.costCents - a.costCents;\n return (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0);\n }),\n byLabel,\n byModel,\n daily,\n appAccess,\n recent,\n };\n}\n"]}
1
+ {"version":3,"file":"usage-metrics-store.js","sourceRoot":"","sources":["../../../src/server/lib/usage-metrics-store.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n getUsageSummary,\n usageBillingForEngine,\n type UsageBillingMode,\n} from \"@agent-native/core/usage\";\nimport { getDbExec } from \"@agent-native/core/db\";\nimport {\n detectEngineFromEnv,\n detectEngineFromUserSecrets,\n getAgentEngineEntry,\n isAgentEngineSettingConfigured,\n isStoredEngineUsable,\n registerBuiltinEngines,\n} from \"@agent-native/core/agent/engine\";\nimport { getSetting } from \"@agent-native/core/settings\";\nimport { currentOrgId, currentOwnerEmail } from \"./dispatch-store.js\";\nimport {\n listWorkspaceApps,\n type WorkspaceAppSummary,\n} from \"./app-creation-store.js\";\n\nconst DAY_MS = 86_400_000;\n\nregisterBuiltinEngines();\n\nexport interface UsageMetricBucket {\n key: string;\n label: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n activeUsers: number;\n lastActiveAt: number | null;\n}\n\nexport interface UserUsageMetric extends UsageMetricBucket {\n ownerEmail: string;\n chatThreads: number;\n chatMessages: number;\n lastChatAt: number | null;\n topApp: string | null;\n role: string | null;\n}\n\nexport interface AppAccessMetric {\n id: string;\n name: string;\n path: string;\n status: WorkspaceAppSummary[\"status\"];\n isDispatch: boolean;\n accessModel: \"workspace\" | \"solo\";\n accessLabel: string;\n accessUsers: number;\n usersWithUsage: number;\n usageCalls: number;\n chatCalls: number;\n costCents: number;\n lastActiveAt: number | null;\n}\n\nexport interface DailyUsageMetric {\n date: string;\n costCents: number;\n calls: number;\n chatCalls: number;\n activeUsers: number;\n}\n\nexport interface RecentUsageMetric {\n id: number;\n createdAt: number;\n ownerEmail: string;\n app: string;\n label: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n costCents: number;\n}\n\nexport interface DispatchUsageMetrics {\n billing: UsageBillingMode;\n sinceMs: number;\n sinceDays: number;\n generatedAt: number;\n access: {\n viewerEmail: string;\n orgId: string | null;\n role: string | null;\n scope: \"organization\" | \"solo\";\n totalUsers: number;\n };\n totals: {\n costCents: number;\n calls: number;\n chatCalls: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n activeUsers: number;\n chatThreads: number;\n chatMessages: number;\n workspaceApps: number;\n };\n byApp: UsageMetricBucket[];\n byUser: UserUsageMetric[];\n byLabel: UsageMetricBucket[];\n byModel: UsageMetricBucket[];\n daily: DailyUsageMetric[];\n appAccess: AppAccessMetric[];\n recent: RecentUsageMetric[];\n}\n\ninterface MemberRecord {\n email: string;\n role: string | null;\n joinedAt: number | null;\n}\n\ninterface ChatStats {\n threads: number;\n messages: number;\n lastChatAt: number | null;\n}\n\nfunction numberField(row: Record<string, unknown>, key: string): number {\n return Number(row[key] ?? 0) || 0;\n}\n\nfunction stringField(row: Record<string, unknown>, key: string): string {\n return String(row[key] ?? \"\");\n}\n\nfunction nullableNumberField(\n row: Record<string, unknown>,\n key: string,\n): number | null {\n const value = row[key];\n if (value == null) return null;\n const numberValue = Number(value);\n return Number.isFinite(numberValue) ? numberValue : null;\n}\n\nfunction labelForKey(value: string): string {\n const trimmed = value.trim();\n return trimmed || \"Unattributed\";\n}\n\nfunction normalizeAppKey(value: string | null | undefined): string {\n const raw = (value ?? \"\").trim().toLowerCase();\n if (!raw) return \"unattributed\";\n return raw.replace(/^agent-native-/, \"\");\n}\n\nfunction envEmails(name: string): string[] {\n return (process.env[name] ?? \"\")\n .split(\",\")\n .map((value) => value.trim().toLowerCase())\n .filter(Boolean);\n}\n\nfunction isEnvAdmin(email: string): boolean {\n const normalized = email.trim().toLowerCase();\n return [\n ...envEmails(\"DISPATCH_ADMIN_EMAILS\"),\n ...envEmails(\"WORKSPACE_OWNER_EMAIL\"),\n ...envEmails(\"DISPATCH_DEFAULT_OWNER_EMAIL\"),\n ].includes(normalized);\n}\n\nasync function detectUsageEngineName(): Promise<string | null> {\n try {\n const stored = (await getSetting(\"agent-engine\")) as {\n engine?: string;\n } | null;\n if (isAgentEngineSettingConfigured(stored)) {\n return (stored as { engine: string }).engine;\n }\n if (stored && typeof stored.engine === \"string\") {\n const entry = getAgentEngineEntry(stored.engine);\n if (entry && isStoredEngineUsable(stored, entry)) {\n return stored.engine;\n }\n }\n\n const detectedFromUser = await detectEngineFromUserSecrets();\n if (detectedFromUser) return detectedFromUser.name;\n\n return detectEngineFromEnv()?.name ?? null;\n } catch {\n return null;\n }\n}\n\nasync function queryRows<T extends Record<string, unknown>>(\n sql: string,\n args: unknown[] = [],\n): Promise<T[]> {\n try {\n const result = await getDbExec().execute({ sql, args });\n return result.rows as T[];\n } catch {\n return [];\n }\n}\n\nasync function getViewerOrgRole(\n orgId: string | null,\n email: string,\n): Promise<string | null> {\n if (!orgId) return null;\n const rows = await queryRows<{ role?: string }>(\n `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,\n [orgId, email.toLowerCase()],\n );\n const role = rows[0]?.role;\n return typeof role === \"string\" ? role : null;\n}\n\nasync function listOrgMembers(orgId: string | null): Promise<MemberRecord[]> {\n if (!orgId) return [];\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT email, role, joined_at AS joined_at FROM org_members WHERE org_id = ? ORDER BY joined_at ASC`,\n [orgId],\n );\n return rows\n .map((row) => ({\n email: stringField(row, \"email\").trim(),\n role: stringField(row, \"role\") || null,\n joinedAt: nullableNumberField(row, \"joined_at\"),\n }))\n .filter((member) => member.email);\n}\n\nasync function listSignedInUsers(): Promise<MemberRecord[]> {\n const authRows = await queryRows<Record<string, unknown>>(\n `SELECT email, created_at AS joined_at FROM \"user\" ORDER BY created_at ASC`,\n );\n if (authRows.length > 0) {\n return authRows\n .map((row) => ({\n email: stringField(row, \"email\").trim(),\n role: null,\n joinedAt: nullableNumberField(row, \"joined_at\"),\n }))\n .filter((member) => member.email);\n }\n\n const usageRows = await queryRows<{ email?: string }>(\n `SELECT DISTINCT owner_email AS email FROM token_usage`,\n );\n const threadRows = await queryRows<{ email?: string }>(\n `SELECT DISTINCT owner_email AS email FROM chat_threads`,\n );\n const emails = new Set<string>();\n for (const row of [...usageRows, ...threadRows]) {\n if (row.email) emails.add(row.email);\n }\n return [...emails].sort().map((email) => ({\n email,\n role: null,\n joinedAt: null,\n }));\n}\n\nfunction usageScope(\n sinceMs: number,\n memberEmails: string[],\n): { where: string; args: unknown[] } {\n if (memberEmails.length === 0) {\n return { where: \"created_at >= ?\", args: [sinceMs] };\n }\n const placeholders = memberEmails.map(() => \"?\").join(\", \");\n return {\n where: `created_at >= ? AND owner_email IN (${placeholders})`,\n args: [sinceMs, ...memberEmails],\n };\n}\n\nfunction threadScope(\n sinceMs: number,\n memberEmails: string[],\n): { where: string; args: unknown[] } {\n if (memberEmails.length === 0) {\n return { where: \"updated_at >= ?\", args: [sinceMs] };\n }\n const placeholders = memberEmails.map(() => \"?\").join(\", \");\n return {\n where: `updated_at >= ? AND owner_email IN (${placeholders})`,\n args: [sinceMs, ...memberEmails],\n };\n}\n\nfunction bucketFromRow(row: Record<string, unknown>): UsageMetricBucket {\n const key = stringField(row, \"k\");\n return {\n key,\n label: labelForKey(key),\n costCents: numberField(row, \"cost_x100\") / 100,\n calls: numberField(row, \"calls\"),\n chatCalls: numberField(row, \"chat_calls\"),\n inputTokens: numberField(row, \"input_tokens\"),\n outputTokens: numberField(row, \"output_tokens\"),\n cacheReadTokens: numberField(row, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(row, \"cache_write_tokens\"),\n activeUsers: numberField(row, \"active_users\"),\n lastActiveAt: nullableNumberField(row, \"last_active_at\"),\n };\n}\n\nasync function usageBuckets(\n columnExpression: string,\n where: string,\n args: unknown[],\n limit: number,\n): Promise<UsageMetricBucket[]> {\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT ${columnExpression} AS k,\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,\n COUNT(*) AS calls,\n SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,\n COALESCE(SUM(input_tokens), 0) AS input_tokens,\n COALESCE(SUM(output_tokens), 0) AS output_tokens,\n COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,\n COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,\n COUNT(DISTINCT owner_email) AS active_users,\n MAX(created_at) AS last_active_at\n FROM token_usage\n WHERE ${where}\n GROUP BY ${columnExpression}\n ORDER BY cost_x100 DESC\n LIMIT ?`,\n [...args, limit],\n );\n return rows.map(bucketFromRow);\n}\n\nasync function loadChatStats(\n where: string,\n args: unknown[],\n): Promise<Map<string, ChatStats>> {\n const rows = await queryRows<Record<string, unknown>>(\n `SELECT owner_email AS owner_email,\n COUNT(*) AS threads,\n COALESCE(SUM(message_count), 0) AS messages,\n MAX(updated_at) AS last_chat_at\n FROM chat_threads\n WHERE ${where}\n GROUP BY owner_email`,\n args,\n );\n return new Map(\n rows.map((row) => [\n stringField(row, \"owner_email\"),\n {\n threads: numberField(row, \"threads\"),\n messages: numberField(row, \"messages\"),\n lastChatAt: nullableNumberField(row, \"last_chat_at\"),\n },\n ]),\n );\n}\n\nasync function assertCanViewMetrics(): Promise<{\n viewerEmail: string;\n orgId: string | null;\n role: string | null;\n}> {\n const viewerEmail = currentOwnerEmail();\n const orgId = currentOrgId();\n const role = await getViewerOrgRole(orgId, viewerEmail);\n if (isEnvAdmin(viewerEmail) || role === \"owner\" || role === \"admin\") {\n return { viewerEmail, orgId, role };\n }\n if (!orgId) {\n return { viewerEmail, orgId, role };\n }\n throw new Error(\n \"Only organization owners and admins can view workspace usage metrics.\",\n );\n}\n\nexport async function listDispatchUsageMetrics(input: {\n sinceDays?: number;\n}): Promise<DispatchUsageMetrics> {\n const { viewerEmail, orgId, role } = await assertCanViewMetrics();\n const sinceDays = Math.max(1, Math.min(365, input.sinceDays ?? 30));\n const sinceMs = Date.now() - sinceDays * DAY_MS;\n const billing = usageBillingForEngine(await detectUsageEngineName());\n\n // Initializes token_usage on fresh deployments before the read-only\n // aggregate queries below. The fake owner avoids changing visible data.\n await getUsageSummary({ ownerEmail: \"__dispatch_metrics_init__\", sinceMs });\n\n const rawMembers = orgId\n ? await listOrgMembers(orgId)\n : await listSignedInUsers();\n const members =\n orgId && rawMembers.length === 0\n ? [{ email: viewerEmail, role, joinedAt: null }]\n : rawMembers;\n const memberEmails = orgId ? members.map((member) => member.email) : [];\n const memberByEmail = new Map(\n members.map((member) => [member.email.toLowerCase(), member]),\n );\n const usage = usageScope(sinceMs, memberEmails);\n const threads = threadScope(sinceMs, memberEmails);\n\n const [apps, totalsRows, byApp, byUserBase, byLabel, byModel, chatStats] =\n await Promise.all([\n listWorkspaceApps({ includeAgentCards: false }),\n queryRows<Record<string, unknown>>(\n `SELECT\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,\n COUNT(*) AS calls,\n SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,\n COALESCE(SUM(input_tokens), 0) AS input_tokens,\n COALESCE(SUM(output_tokens), 0) AS output_tokens,\n COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,\n COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,\n COUNT(DISTINCT owner_email) AS active_users\n FROM token_usage\n WHERE ${usage.where}`,\n usage.args,\n ),\n usageBuckets(\n `COALESCE(NULLIF(app, ''), 'unattributed')`,\n usage.where,\n usage.args,\n 20,\n ),\n usageBuckets(\"owner_email\", usage.where, usage.args, 50),\n usageBuckets(\n `COALESCE(NULLIF(label, ''), 'chat')`,\n usage.where,\n usage.args,\n 20,\n ),\n usageBuckets(\n `COALESCE(NULLIF(model, ''), 'unknown')`,\n usage.where,\n usage.args,\n 20,\n ),\n loadChatStats(threads.where, threads.args),\n ]);\n\n const topAppRows = await queryRows<Record<string, unknown>>(\n `SELECT owner_email AS owner_email,\n COALESCE(NULLIF(app, ''), 'unattributed') AS app,\n COALESCE(SUM(cost_cents_x100), 0) AS cost_x100\n FROM token_usage\n WHERE ${usage.where}\n GROUP BY owner_email, COALESCE(NULLIF(app, ''), 'unattributed')\n ORDER BY owner_email ASC, cost_x100 DESC`,\n usage.args,\n );\n const topAppByUser = new Map<string, string>();\n for (const row of topAppRows) {\n const email = stringField(row, \"owner_email\");\n if (!topAppByUser.has(email)) {\n topAppByUser.set(email, stringField(row, \"app\"));\n }\n }\n\n const byUserMap = new Map<string, UserUsageMetric>();\n for (const bucket of byUserBase) {\n const ownerEmail = bucket.key;\n const stats = chatStats.get(ownerEmail) ?? {\n threads: 0,\n messages: 0,\n lastChatAt: null,\n };\n const member = memberByEmail.get(ownerEmail.toLowerCase());\n byUserMap.set(ownerEmail, {\n ...bucket,\n ownerEmail,\n chatThreads: stats.threads,\n chatMessages: stats.messages,\n lastChatAt: stats.lastChatAt,\n topApp: topAppByUser.get(ownerEmail) ?? null,\n role: member?.role ?? null,\n });\n }\n for (const [ownerEmail, stats] of chatStats) {\n if (byUserMap.has(ownerEmail)) continue;\n const member = memberByEmail.get(ownerEmail.toLowerCase());\n byUserMap.set(ownerEmail, {\n key: ownerEmail,\n label: ownerEmail,\n ownerEmail,\n costCents: 0,\n calls: 0,\n chatCalls: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n activeUsers: 1,\n lastActiveAt: stats.lastChatAt,\n chatThreads: stats.threads,\n chatMessages: stats.messages,\n lastChatAt: stats.lastChatAt,\n topApp: null,\n role: member?.role ?? null,\n });\n }\n\n const dayRows = await queryRows<Record<string, unknown>>(\n `SELECT created_at, owner_email, label, cost_cents_x100\n FROM token_usage\n WHERE ${usage.where}\n ORDER BY created_at ASC`,\n usage.args,\n );\n const dailyMap = new Map<\n string,\n { costX100: number; calls: number; chatCalls: number; users: Set<string> }\n >();\n for (const row of dayRows) {\n const date = new Date(numberField(row, \"created_at\"))\n .toISOString()\n .slice(0, 10);\n const current = dailyMap.get(date) ?? {\n costX100: 0,\n calls: 0,\n chatCalls: 0,\n users: new Set<string>(),\n };\n current.costX100 += numberField(row, \"cost_cents_x100\");\n current.calls += 1;\n if (stringField(row, \"label\") === \"chat\") current.chatCalls += 1;\n current.users.add(stringField(row, \"owner_email\"));\n dailyMap.set(date, current);\n }\n const daily = [...dailyMap.entries()]\n .map(([date, value]) => ({\n date,\n costCents: value.costX100 / 100,\n calls: value.calls,\n chatCalls: value.chatCalls,\n activeUsers: value.users.size,\n }))\n .sort((a, b) => a.date.localeCompare(b.date));\n\n const recentRows = await queryRows<Record<string, unknown>>(\n `SELECT id, created_at, owner_email, app, label, model,\n input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,\n cost_cents_x100\n FROM token_usage\n WHERE ${usage.where}\n ORDER BY created_at DESC\n LIMIT 50`,\n usage.args,\n );\n const recent = recentRows.map((row) => ({\n id: numberField(row, \"id\"),\n createdAt: numberField(row, \"created_at\"),\n ownerEmail: stringField(row, \"owner_email\"),\n app: stringField(row, \"app\") || \"unattributed\",\n label: stringField(row, \"label\") || \"chat\",\n model: stringField(row, \"model\") || \"unknown\",\n inputTokens: numberField(row, \"input_tokens\"),\n outputTokens: numberField(row, \"output_tokens\"),\n cacheReadTokens: numberField(row, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(row, \"cache_write_tokens\"),\n costCents: numberField(row, \"cost_cents_x100\") / 100,\n }));\n\n const appUsageByKey = new Map(\n byApp.map((bucket) => [normalizeAppKey(bucket.key), bucket]),\n );\n const accessUsers = members.length || byUserMap.size;\n const accessModel = orgId ? \"workspace\" : \"solo\";\n const accessLabel = orgId ? \"Workspace members\" : \"Signed-in users\";\n const appAccess = apps.map((app) => {\n const usageBucket = appUsageByKey.get(normalizeAppKey(app.id));\n return {\n id: app.id,\n name: app.name,\n path: app.path,\n status: app.status,\n isDispatch: app.isDispatch,\n accessModel,\n accessLabel,\n accessUsers,\n usersWithUsage: usageBucket?.activeUsers ?? 0,\n usageCalls: usageBucket?.calls ?? 0,\n chatCalls: usageBucket?.chatCalls ?? 0,\n costCents: usageBucket?.costCents ?? 0,\n lastActiveAt: usageBucket?.lastActiveAt ?? null,\n } satisfies AppAccessMetric;\n });\n\n const totals = totalsRows[0] ?? {};\n const chatThreadTotals = [...chatStats.values()].reduce(\n (acc, value) => ({\n threads: acc.threads + value.threads,\n messages: acc.messages + value.messages,\n }),\n { threads: 0, messages: 0 },\n );\n\n return {\n billing,\n sinceMs,\n sinceDays,\n generatedAt: Date.now(),\n access: {\n viewerEmail,\n orgId,\n role,\n scope: orgId ? \"organization\" : \"solo\",\n totalUsers: accessUsers,\n },\n totals: {\n costCents: numberField(totals, \"cost_x100\") / 100,\n calls: numberField(totals, \"calls\"),\n chatCalls: numberField(totals, \"chat_calls\"),\n inputTokens: numberField(totals, \"input_tokens\"),\n outputTokens: numberField(totals, \"output_tokens\"),\n cacheReadTokens: numberField(totals, \"cache_read_tokens\"),\n cacheWriteTokens: numberField(totals, \"cache_write_tokens\"),\n activeUsers: numberField(totals, \"active_users\"),\n chatThreads: chatThreadTotals.threads,\n chatMessages: chatThreadTotals.messages,\n workspaceApps: apps.filter((app) => !app.isDispatch).length,\n },\n byApp,\n byUser: [...byUserMap.values()].sort((a, b) => {\n if (b.costCents !== a.costCents) return b.costCents - a.costCents;\n return (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0);\n }),\n byLabel,\n byModel,\n daily,\n appAccess,\n recent,\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
6
6
  "license": "MIT",
@@ -97,7 +97,7 @@
97
97
  "typescript": "^6.0.3",
98
98
  "vite": "8.0.3",
99
99
  "vitest": "^4.1.5",
100
- "@agent-native/core": "0.12.5"
100
+ "@agent-native/core": "0.12.6"
101
101
  },
102
102
  "scripts": {
103
103
  "build": "tsc && tsc-alias --resolve-full-paths",
@@ -4,7 +4,7 @@ import { listDispatchUsageMetrics } from "../server/lib/usage-metrics-store.js";
4
4
 
5
5
  export default defineAction({
6
6
  description:
7
- "Get workspace-level LLM usage, estimated cost, users, app access, and recent activity metrics for Dispatch admins.",
7
+ "Get workspace-level LLM usage, spend or Builder.io credit spend, users, app access, and recent activity metrics for Dispatch admins.",
8
8
  schema: z.object({
9
9
  sinceDays: z.coerce
10
10
  .number()
@@ -58,6 +58,7 @@ export default defineAction({
58
58
  try {
59
59
  const metrics = await listDispatchUsageMetrics({ sinceDays: 30 });
60
60
  screen.usageMetrics = {
61
+ billing: metrics.billing,
61
62
  totals: metrics.totals,
62
63
  byApp: metrics.byApp.slice(0, 8),
63
64
  byUser: metrics.byUser.slice(0, 8),
@@ -77,7 +77,17 @@ interface RecentUsageMetric {
77
77
  costCents: number;
78
78
  }
79
79
 
80
+ interface UsageBillingMode {
81
+ unit: "usd" | "builder-credits";
82
+ label: string;
83
+ shortLabel: string;
84
+ source: "estimated-provider-cost" | "builder-agent-credits";
85
+ hardCostMarginMultiplier?: number;
86
+ creditsPerUsd?: number;
87
+ }
88
+
80
89
  interface DispatchUsageMetrics {
90
+ billing?: UsageBillingMode;
81
91
  sinceDays: number;
82
92
  access: {
83
93
  viewerEmail: string;
@@ -110,7 +120,37 @@ interface DispatchUsageMetrics {
110
120
 
111
121
  const RANGES = [7, 30, 90] as const;
112
122
 
113
- function formatCost(cents: number): string {
123
+ const USD_BILLING: UsageBillingMode = {
124
+ unit: "usd",
125
+ label: "Estimated spend",
126
+ shortLabel: "Cost",
127
+ source: "estimated-provider-cost",
128
+ };
129
+
130
+ function displayAmountFromCostCents(
131
+ cents: number,
132
+ billing: UsageBillingMode,
133
+ ): number {
134
+ if (billing.unit !== "builder-credits") return cents;
135
+ const margin = billing.hardCostMarginMultiplier ?? 1.25;
136
+ const creditsPerUsd = billing.creditsPerUsd ?? 20;
137
+ const credits = (cents / 100) * margin * creditsPerUsd;
138
+ return credits <= 0 ? 0 : Math.ceil(credits * 1000) / 1000;
139
+ }
140
+
141
+ function formatCredits(credits: number): string {
142
+ if (!Number.isFinite(credits) || credits === 0) return "0 credits";
143
+ const maximumFractionDigits = credits < 1 ? 3 : credits < 10 ? 2 : 1;
144
+ const value = credits.toLocaleString(undefined, {
145
+ maximumFractionDigits,
146
+ });
147
+ return `${value} ${credits === 1 ? "credit" : "credits"}`;
148
+ }
149
+
150
+ function formatSpend(cents: number, billing: UsageBillingMode): string {
151
+ if (billing.unit === "builder-credits") {
152
+ return formatCredits(displayAmountFromCostCents(cents, billing));
153
+ }
114
154
  if (!Number.isFinite(cents) || cents === 0) return "$0.00";
115
155
  if (Math.abs(cents) < 1) return `${cents.toFixed(3)}¢`;
116
156
  if (Math.abs(cents) < 100) return `${cents.toFixed(2)}¢`;
@@ -150,8 +190,15 @@ function displayApp(value: string | null | undefined): string {
150
190
  return trimmed;
151
191
  }
152
192
 
153
- function maxCost(rows: Array<{ costCents: number }>): number {
154
- return rows.reduce((max, row) => Math.max(max, row.costCents), 0);
193
+ function maxSpend(
194
+ rows: Array<{ costCents: number }>,
195
+ billing: UsageBillingMode,
196
+ ): number {
197
+ return rows.reduce(
198
+ (max, row) =>
199
+ Math.max(max, displayAmountFromCostCents(row.costCents, billing)),
200
+ 0,
201
+ );
155
202
  }
156
203
 
157
204
  function barWidth(value: number, max: number): string {
@@ -257,8 +304,14 @@ function LoadingMetrics() {
257
304
  );
258
305
  }
259
306
 
260
- function AppSpendRows({ rows }: { rows: UsageMetricBucket[] }) {
261
- const max = maxCost(rows);
307
+ function AppSpendRows({
308
+ rows,
309
+ billing,
310
+ }: {
311
+ rows: UsageMetricBucket[];
312
+ billing: UsageBillingMode;
313
+ }) {
314
+ const max = maxSpend(rows, billing);
262
315
  if (rows.length === 0) {
263
316
  return (
264
317
  <div className="rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground">
@@ -282,7 +335,7 @@ function AppSpendRows({ rows }: { rows: UsageMetricBucket[] }) {
282
335
  </div>
283
336
  <div className="shrink-0 text-right">
284
337
  <div className="font-medium tabular-nums text-foreground">
285
- {formatCost(row.costCents)}
338
+ {formatSpend(row.costCents, billing)}
286
339
  </div>
287
340
  <div className="text-xs text-muted-foreground">
288
341
  {formatNumber(row.calls)} calls
@@ -292,7 +345,12 @@ function AppSpendRows({ rows }: { rows: UsageMetricBucket[] }) {
292
345
  <div className="h-2 overflow-hidden rounded-full bg-muted">
293
346
  <div
294
347
  className="h-full rounded-full bg-foreground"
295
- style={{ width: barWidth(row.costCents, max) }}
348
+ style={{
349
+ width: barWidth(
350
+ displayAmountFromCostCents(row.costCents, billing),
351
+ max,
352
+ ),
353
+ }}
296
354
  />
297
355
  </div>
298
356
  </div>
@@ -335,7 +393,13 @@ function DailyActivity({ rows }: { rows: DailyUsageMetric[] }) {
335
393
  );
336
394
  }
337
395
 
338
- function AppAccessTable({ rows }: { rows: AppAccessMetric[] }) {
396
+ function AppAccessTable({
397
+ rows,
398
+ billing,
399
+ }: {
400
+ rows: AppAccessMetric[];
401
+ billing: UsageBillingMode;
402
+ }) {
339
403
  const visibleRows = rows.filter((row) => !row.isDispatch);
340
404
  if (visibleRows.length === 0) {
341
405
  return (
@@ -353,7 +417,9 @@ function AppAccessTable({ rows }: { rows: AppAccessMetric[] }) {
353
417
  <th className="px-2 py-2 font-medium">Access</th>
354
418
  <th className="px-2 py-2 text-right font-medium">Users</th>
355
419
  <th className="px-2 py-2 text-right font-medium">Chats</th>
356
- <th className="px-2 py-2 text-right font-medium">Cost</th>
420
+ <th className="px-2 py-2 text-right font-medium">
421
+ {billing.shortLabel}
422
+ </th>
357
423
  <th className="px-2 py-2 text-right font-medium">Last activity</th>
358
424
  </tr>
359
425
  </thead>
@@ -385,7 +451,7 @@ function AppAccessTable({ rows }: { rows: AppAccessMetric[] }) {
385
451
  {formatNumber(row.chatCalls)}
386
452
  </td>
387
453
  <td className="px-2 py-3 text-right tabular-nums">
388
- {formatCost(row.costCents)}
454
+ {formatSpend(row.costCents, billing)}
389
455
  </td>
390
456
  <td className="px-2 py-3 text-right text-muted-foreground">
391
457
  {timeAgo(row.lastActiveAt)}
@@ -398,7 +464,13 @@ function AppAccessTable({ rows }: { rows: AppAccessMetric[] }) {
398
464
  );
399
465
  }
400
466
 
401
- function UserTable({ rows }: { rows: UserUsageMetric[] }) {
467
+ function UserTable({
468
+ rows,
469
+ billing,
470
+ }: {
471
+ rows: UserUsageMetric[];
472
+ billing: UsageBillingMode;
473
+ }) {
402
474
  if (rows.length === 0) {
403
475
  return (
404
476
  <div className="rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground">
@@ -417,7 +489,9 @@ function UserTable({ rows }: { rows: UserUsageMetric[] }) {
417
489
  <th className="px-2 py-2 text-right font-medium">Chats</th>
418
490
  <th className="px-2 py-2 text-right font-medium">Threads</th>
419
491
  <th className="px-2 py-2 text-right font-medium">Tokens</th>
420
- <th className="px-2 py-2 text-right font-medium">Cost</th>
492
+ <th className="px-2 py-2 text-right font-medium">
493
+ {billing.shortLabel}
494
+ </th>
421
495
  </tr>
422
496
  </thead>
423
497
  <tbody>
@@ -447,7 +521,7 @@ function UserTable({ rows }: { rows: UserUsageMetric[] }) {
447
521
  {formatTokens(row.inputTokens + row.outputTokens)}
448
522
  </td>
449
523
  <td className="px-2 py-3 text-right tabular-nums">
450
- {formatCost(row.costCents)}
524
+ {formatSpend(row.costCents, billing)}
451
525
  </td>
452
526
  </tr>
453
527
  ))}
@@ -460,11 +534,13 @@ function UserTable({ rows }: { rows: UserUsageMetric[] }) {
460
534
  function CompactBreakdown({
461
535
  rows,
462
536
  empty,
537
+ billing,
463
538
  }: {
464
539
  rows: UsageMetricBucket[];
465
540
  empty: string;
541
+ billing: UsageBillingMode;
466
542
  }) {
467
- const max = maxCost(rows);
543
+ const max = maxSpend(rows, billing);
468
544
  if (rows.length === 0) {
469
545
  return <div className="text-sm text-muted-foreground">{empty}</div>;
470
546
  }
@@ -477,13 +553,18 @@ function CompactBreakdown({
477
553
  {row.label}
478
554
  </span>
479
555
  <span className="shrink-0 tabular-nums text-muted-foreground">
480
- {formatCost(row.costCents)}
556
+ {formatSpend(row.costCents, billing)}
481
557
  </span>
482
558
  </div>
483
559
  <div className="h-1.5 overflow-hidden rounded-full bg-muted">
484
560
  <div
485
561
  className="h-full rounded-full bg-muted-foreground"
486
- style={{ width: barWidth(row.costCents, max) }}
562
+ style={{
563
+ width: barWidth(
564
+ displayAmountFromCostCents(row.costCents, billing),
565
+ max,
566
+ ),
567
+ }}
487
568
  />
488
569
  </div>
489
570
  </div>
@@ -492,7 +573,13 @@ function CompactBreakdown({
492
573
  );
493
574
  }
494
575
 
495
- function RecentTable({ rows }: { rows: RecentUsageMetric[] }) {
576
+ function RecentTable({
577
+ rows,
578
+ billing,
579
+ }: {
580
+ rows: RecentUsageMetric[];
581
+ billing: UsageBillingMode;
582
+ }) {
496
583
  if (rows.length === 0) {
497
584
  return (
498
585
  <div className="rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground">
@@ -510,7 +597,9 @@ function RecentTable({ rows }: { rows: RecentUsageMetric[] }) {
510
597
  <th className="px-2 py-2 font-medium">App</th>
511
598
  <th className="px-2 py-2 font-medium">Label</th>
512
599
  <th className="px-2 py-2 font-medium">Model</th>
513
- <th className="px-2 py-2 text-right font-medium">Cost</th>
600
+ <th className="px-2 py-2 text-right font-medium">
601
+ {billing.shortLabel}
602
+ </th>
514
603
  </tr>
515
604
  </thead>
516
605
  <tbody>
@@ -534,7 +623,7 @@ function RecentTable({ rows }: { rows: RecentUsageMetric[] }) {
534
623
  </div>
535
624
  </td>
536
625
  <td className="px-2 py-3 text-right tabular-nums">
537
- {formatCost(row.costCents)}
626
+ {formatSpend(row.costCents, billing)}
538
627
  </td>
539
628
  </tr>
540
629
  ))}
@@ -552,6 +641,7 @@ export default function MetricsRoute() {
552
641
  { refetchInterval: 30_000 },
553
642
  );
554
643
  const metrics = data as DispatchUsageMetrics | undefined;
644
+ const billing = metrics?.billing ?? USD_BILLING;
555
645
  const totalTokens = useMemo(() => {
556
646
  if (!metrics) return 0;
557
647
  return (
@@ -565,7 +655,11 @@ export default function MetricsRoute() {
565
655
  return (
566
656
  <DispatchShell
567
657
  title="Metrics"
568
- description="Workspace-wide LLM spend, chat volume, user activity, and app access."
658
+ description={
659
+ billing.unit === "builder-credits"
660
+ ? "Workspace-wide Builder.io credit spend, chat volume, user activity, and app access."
661
+ : "Workspace-wide LLM spend, chat volume, user activity, and app access."
662
+ }
569
663
  >
570
664
  <div className="space-y-4">
571
665
  <div className="flex flex-wrap items-center justify-between gap-3">
@@ -593,8 +687,8 @@ export default function MetricsRoute() {
593
687
  <>
594
688
  <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-5">
595
689
  <MetricCard
596
- label="Estimated spend"
597
- value={formatCost(metrics.totals.costCents)}
690
+ label={billing.label}
691
+ value={formatSpend(metrics.totals.costCents, billing)}
598
692
  detail={`${formatTokens(totalTokens)} total tokens`}
599
693
  icon={<IconCoin size={17} />}
600
694
  />
@@ -625,8 +719,15 @@ export default function MetricsRoute() {
625
719
  </div>
626
720
 
627
721
  <div className="grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(320px,0.65fr)]">
628
- <Panel title="Spend By App" icon={<IconChartBar size={16} />}>
629
- <AppSpendRows rows={metrics.byApp} />
722
+ <Panel
723
+ title={
724
+ billing.unit === "builder-credits"
725
+ ? "Credit Spend By App"
726
+ : "Spend By App"
727
+ }
728
+ icon={<IconChartBar size={16} />}
729
+ >
730
+ <AppSpendRows rows={metrics.byApp} billing={billing} />
630
731
  </Panel>
631
732
  <Panel title="Daily Activity" icon={<IconClockHour4 size={16} />}>
632
733
  <DailyActivity rows={metrics.daily} />
@@ -634,11 +735,11 @@ export default function MetricsRoute() {
634
735
  </div>
635
736
 
636
737
  <Panel title="Access By App" icon={<IconApps size={16} />}>
637
- <AppAccessTable rows={metrics.appAccess} />
738
+ <AppAccessTable rows={metrics.appAccess} billing={billing} />
638
739
  </Panel>
639
740
 
640
741
  <Panel title="Users" icon={<IconUsersGroup size={16} />}>
641
- <UserTable rows={metrics.byUser} />
742
+ <UserTable rows={metrics.byUser} billing={billing} />
642
743
  </Panel>
643
744
 
644
745
  <div className="grid gap-4 lg:grid-cols-2">
@@ -646,18 +747,20 @@ export default function MetricsRoute() {
646
747
  <CompactBreakdown
647
748
  rows={metrics.byModel}
648
749
  empty="No model usage in this window."
750
+ billing={billing}
649
751
  />
650
752
  </Panel>
651
753
  <Panel title="Work Types" icon={<IconActivity size={16} />}>
652
754
  <CompactBreakdown
653
755
  rows={metrics.byLabel}
654
756
  empty="No labeled usage in this window."
757
+ billing={billing}
655
758
  />
656
759
  </Panel>
657
760
  </div>
658
761
 
659
762
  <Panel title="Recent LLM Calls" icon={<IconActivity size={16} />}>
660
- <RecentTable rows={metrics.recent} />
763
+ <RecentTable rows={metrics.recent} billing={billing} />
661
764
  </Panel>
662
765
  </>
663
766
  ) : null}
@@ -1,5 +1,18 @@
1
- import { getUsageSummary } from "@agent-native/core/usage";
1
+ import {
2
+ getUsageSummary,
3
+ usageBillingForEngine,
4
+ type UsageBillingMode,
5
+ } from "@agent-native/core/usage";
2
6
  import { getDbExec } from "@agent-native/core/db";
7
+ import {
8
+ detectEngineFromEnv,
9
+ detectEngineFromUserSecrets,
10
+ getAgentEngineEntry,
11
+ isAgentEngineSettingConfigured,
12
+ isStoredEngineUsable,
13
+ registerBuiltinEngines,
14
+ } from "@agent-native/core/agent/engine";
15
+ import { getSetting } from "@agent-native/core/settings";
3
16
  import { currentOrgId, currentOwnerEmail } from "./dispatch-store.js";
4
17
  import {
5
18
  listWorkspaceApps,
@@ -8,6 +21,8 @@ import {
8
21
 
9
22
  const DAY_MS = 86_400_000;
10
23
 
24
+ registerBuiltinEngines();
25
+
11
26
  export interface UsageMetricBucket {
12
27
  key: string;
13
28
  label: string;
@@ -70,6 +85,7 @@ export interface RecentUsageMetric {
70
85
  }
71
86
 
72
87
  export interface DispatchUsageMetrics {
88
+ billing: UsageBillingMode;
73
89
  sinceMs: number;
74
90
  sinceDays: number;
75
91
  generatedAt: number;
@@ -159,6 +175,30 @@ function isEnvAdmin(email: string): boolean {
159
175
  ].includes(normalized);
160
176
  }
161
177
 
178
+ async function detectUsageEngineName(): Promise<string | null> {
179
+ try {
180
+ const stored = (await getSetting("agent-engine")) as {
181
+ engine?: string;
182
+ } | null;
183
+ if (isAgentEngineSettingConfigured(stored)) {
184
+ return (stored as { engine: string }).engine;
185
+ }
186
+ if (stored && typeof stored.engine === "string") {
187
+ const entry = getAgentEngineEntry(stored.engine);
188
+ if (entry && isStoredEngineUsable(stored, entry)) {
189
+ return stored.engine;
190
+ }
191
+ }
192
+
193
+ const detectedFromUser = await detectEngineFromUserSecrets();
194
+ if (detectedFromUser) return detectedFromUser.name;
195
+
196
+ return detectEngineFromEnv()?.name ?? null;
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+
162
202
  async function queryRows<T extends Record<string, unknown>>(
163
203
  sql: string,
164
204
  args: unknown[] = [],
@@ -353,6 +393,7 @@ export async function listDispatchUsageMetrics(input: {
353
393
  const { viewerEmail, orgId, role } = await assertCanViewMetrics();
354
394
  const sinceDays = Math.max(1, Math.min(365, input.sinceDays ?? 30));
355
395
  const sinceMs = Date.now() - sinceDays * DAY_MS;
396
+ const billing = usageBillingForEngine(await detectUsageEngineName());
356
397
 
357
398
  // Initializes token_usage on fresh deployments before the read-only
358
399
  // aggregate queries below. The fake owner avoids changing visible data.
@@ -568,6 +609,7 @@ export async function listDispatchUsageMetrics(input: {
568
609
  );
569
610
 
570
611
  return {
612
+ billing,
571
613
  sinceMs,
572
614
  sinceDays,
573
615
  generatedAt: Date.now(),