@linxiraos/pi-stats 1.1.26 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +12 -5
  2. package/build.ts +4 -0
  3. package/dist/client/favicon.ico +0 -0
  4. package/dist/client/index.css +1 -1
  5. package/dist/client/index.html +1 -0
  6. package/dist/client/index.js +96 -96
  7. package/dist/types/aggregator.d.ts +4 -1
  8. package/dist/types/client/api.d.ts +5 -1
  9. package/dist/types/client/app/nav.d.ts +5 -2
  10. package/dist/types/client/data/formatters.d.ts +7 -2
  11. package/dist/types/client/data/live.d.ts +16 -11
  12. package/dist/types/client/data/query-store.d.ts +39 -0
  13. package/dist/types/client/data/query.d.ts +10 -5
  14. package/dist/types/client/data/range.d.ts +4 -3
  15. package/dist/types/client/i18n/format.d.ts +9 -0
  16. package/dist/types/client/i18n/index.d.ts +18 -0
  17. package/dist/types/client/i18n/messages/charts.d.ts +8 -0
  18. package/dist/types/client/i18n/messages/common.d.ts +153 -0
  19. package/dist/types/client/i18n/messages/costs.d.ts +92 -0
  20. package/dist/types/client/i18n/messages/en.d.ts +1255 -0
  21. package/dist/types/client/i18n/messages/errors.d.ts +78 -0
  22. package/dist/types/client/i18n/messages/frustration.d.ts +191 -0
  23. package/dist/types/client/i18n/messages/gain.d.ts +54 -0
  24. package/dist/types/client/i18n/messages/models.d.ts +94 -0
  25. package/dist/types/client/i18n/messages/nav.d.ts +46 -0
  26. package/dist/types/client/i18n/messages/overview.d.ts +80 -0
  27. package/dist/types/client/i18n/messages/projects.d.ts +84 -0
  28. package/dist/types/client/i18n/messages/providers.d.ts +142 -0
  29. package/dist/types/client/i18n/messages/requests.d.ts +72 -0
  30. package/dist/types/client/i18n/messages/tools.d.ts +119 -0
  31. package/dist/types/client/i18n/messages/traces.d.ts +144 -0
  32. package/dist/types/client/i18n/messages/ui.d.ts +44 -0
  33. package/dist/types/client/i18n/messages/zh-CN.d.ts +13 -0
  34. package/dist/types/client/i18n/types.d.ts +15 -0
  35. package/dist/types/client/traces/Minimap.d.ts +2 -0
  36. package/dist/types/client/traces/TimelineCanvas.d.ts +29 -8
  37. package/dist/types/client/traces/TranscriptList.d.ts +3 -1
  38. package/dist/types/db.d.ts +14 -21
  39. package/dist/types/embedded-client.d.ts +5 -0
  40. package/dist/types/frustration.d.ts +18 -9
  41. package/dist/types/live.d.ts +5 -3
  42. package/dist/types/locale.d.ts +16 -0
  43. package/dist/types/parser.d.ts +0 -17
  44. package/dist/types/rollup.d.ts +9 -1
  45. package/dist/types/server.d.ts +6 -0
  46. package/dist/types/shared-types.d.ts +7 -0
  47. package/dist/types/trace.d.ts +18 -6
  48. package/dist/types/types.d.ts +13 -1
  49. package/dist/types/user-metrics.d.ts +23 -1
  50. package/package.json +5 -5
  51. package/src/aggregator.ts +31 -11
  52. package/src/client/api.ts +40 -5
  53. package/src/client/app/LiveChip.tsx +14 -13
  54. package/src/client/app/Shell.tsx +19 -13
  55. package/src/client/app/ThemeToggle.tsx +4 -3
  56. package/src/client/app/nav.ts +19 -16
  57. package/src/client/data/formatters.ts +80 -13
  58. package/src/client/data/live.tsx +85 -45
  59. package/src/client/data/query-store.ts +108 -0
  60. package/src/client/data/query.ts +47 -56
  61. package/src/client/data/range.ts +34 -14
  62. package/src/client/data/series.ts +7 -1
  63. package/src/client/favicon.ico +0 -0
  64. package/src/client/i18n/format.ts +28 -0
  65. package/src/client/i18n/index.ts +53 -0
  66. package/src/client/i18n/messages/charts.ts +8 -0
  67. package/src/client/i18n/messages/common.ts +157 -0
  68. package/src/client/i18n/messages/costs.ts +104 -0
  69. package/src/client/i18n/messages/en.ts +40 -0
  70. package/src/client/i18n/messages/errors.ts +80 -0
  71. package/src/client/i18n/messages/frustration.ts +198 -0
  72. package/src/client/i18n/messages/gain.ts +54 -0
  73. package/src/client/i18n/messages/models.ts +94 -0
  74. package/src/client/i18n/messages/nav.ts +46 -0
  75. package/src/client/i18n/messages/overview.ts +90 -0
  76. package/src/client/i18n/messages/projects.ts +84 -0
  77. package/src/client/i18n/messages/providers.ts +165 -0
  78. package/src/client/i18n/messages/requests.ts +73 -0
  79. package/src/client/i18n/messages/tools.ts +120 -0
  80. package/src/client/i18n/messages/traces.ts +160 -0
  81. package/src/client/i18n/messages/ui.ts +46 -0
  82. package/src/client/i18n/messages/zh-CN.ts +43 -0
  83. package/src/client/i18n/types.ts +19 -0
  84. package/src/client/index.tsx +4 -0
  85. package/src/client/routes/CostsRoute.tsx +61 -48
  86. package/src/client/routes/ErrorsRoute.tsx +71 -47
  87. package/src/client/routes/FrustrationRoute.tsx +131 -116
  88. package/src/client/routes/GainRoute.tsx +42 -30
  89. package/src/client/routes/ModelsRoute.tsx +90 -73
  90. package/src/client/routes/OverviewRoute.tsx +109 -63
  91. package/src/client/routes/ProjectsRoute.tsx +73 -46
  92. package/src/client/routes/ProvidersRoute.tsx +151 -114
  93. package/src/client/routes/RequestsRoute.tsx +69 -46
  94. package/src/client/routes/ToolsRoute.tsx +108 -94
  95. package/src/client/routes/TracesRoute.tsx +25 -23
  96. package/src/client/styles.css +5 -2
  97. package/src/client/traces/AggregatesPanel.tsx +19 -8
  98. package/src/client/traces/Minimap.tsx +65 -27
  99. package/src/client/traces/SpanDrawer.tsx +46 -21
  100. package/src/client/traces/SummaryStrip.tsx +27 -11
  101. package/src/client/traces/TimelineCanvas.tsx +245 -116
  102. package/src/client/traces/TraceView.tsx +51 -36
  103. package/src/client/traces/TranscriptList.tsx +20 -11
  104. package/src/client/traces/traces.css +26 -0
  105. package/src/client/ui/JsonBlock.tsx +3 -2
  106. package/src/client/ui/Modal.tsx +3 -2
  107. package/src/client/ui/RequestDrawer.tsx +33 -30
  108. package/src/client/ui/SearchInput.tsx +2 -1
  109. package/src/client/ui/States.tsx +3 -2
  110. package/src/client/ui/Table.tsx +7 -3
  111. package/src/db.ts +150 -135
  112. package/src/embedded-client.ts +9 -0
  113. package/src/frustration.ts +9 -30
  114. package/src/index.ts +2 -1
  115. package/src/live.ts +36 -11
  116. package/src/locale.ts +29 -0
  117. package/src/parser.ts +37 -72
  118. package/src/port-conflict.ts +1 -1
  119. package/src/prompts/judge/frustration.json +19 -0
  120. package/src/rollup.ts +41 -20
  121. package/src/server.ts +67 -27
  122. package/src/shared-types.ts +7 -0
  123. package/src/trace.ts +54 -40
  124. package/src/types.ts +13 -1
  125. package/src/user-metrics.ts +220 -6
@@ -1,16 +1,18 @@
1
- import { format } from "@linxiraos/pi-utils/dates";
2
1
  import { type ReactNode, useMemo, useState } from "react";
3
2
  import { getProviderDashboardStats, getProviderWindowStats } from "../api";
3
+ import { t } from "../i18n";
4
4
  import { Chart, type ChartSeries, Legend, ShareBar, TimeChart, useHiddenSeries } from "../charts";
5
5
  import { buildColorLookup, OTHER_COLOR, SERIES_COLORS } from "../data/colors";
6
6
  import {
7
7
  formatCompact,
8
8
  formatCost,
9
+ formatErrorRate,
9
10
  formatEstimatedCost,
10
11
  formatInteger,
11
12
  formatPercent,
12
13
  formatRelativeTime,
13
14
  formatTokensPerSecond,
15
+ formatDateTimeShort,
14
16
  } from "../data/formatters";
15
17
  import { type QueryResult, useQuery } from "../data/query";
16
18
  import { bucketAxis, formatBucket, formatTick, rangeMeta } from "../data/range";
@@ -51,23 +53,22 @@ export interface ProvidersRouteProps {
51
53
  type BurnMetric = "tokens" | "cost" | "requests";
52
54
 
53
55
  const BURN_OPTIONS = [
54
- { value: "tokens" as const, label: "Tokens" },
55
- { value: "cost" as const, label: "Cost", title: "API-equivalent estimate" },
56
- { value: "requests" as const, label: "Requests" },
56
+ { value: "tokens" as const, label: t("common.tokens") },
57
+ { value: "cost" as const, label: t("common.cost"), title: t("providers.cost-title") },
58
+ { value: "requests" as const, label: t("common.requests") },
57
59
  ];
58
60
 
59
61
  /** Providers stacked individually in the burn chart; the rest fold into "Other". */
60
62
  const TOP_PROVIDERS = 6;
61
63
 
62
64
  const TOKEN_MIX = [
63
- { key: "input", label: "Uncached input", color: "#5b8cff" },
64
- { key: "cacheRead", label: "Cache read", color: "var(--chart-primary)" },
65
- { key: "cacheWrite", label: "Cache write", color: "#f5b54a" },
66
- { key: "output", label: "Output", color: "var(--chart-secondary)" },
65
+ { key: "input", label: t("ui.uncached-input"), color: "#5b8cff" },
66
+ { key: "cacheRead", label: t("common.cache-read"), color: "var(--chart-primary)" },
67
+ { key: "cacheWrite", label: t("common.cache-write"), color: "#f5b54a" },
68
+ { key: "output", label: t("common.output"), color: "var(--chart-secondary)" },
67
69
  ] as const;
68
70
 
69
- const SNAPSHOT_HINT =
70
- "Usage snapshots accumulate whenever usage limits are fetched (TUI footer, /usage, zeta-c usage).";
71
+ const SNAPSHOT_HINT = t("providers.snapshot-hint");
71
72
 
72
73
  interface WindowRef {
73
74
  provider: string;
@@ -75,14 +76,16 @@ interface WindowRef {
75
76
  }
76
77
 
77
78
  export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
78
- const stats = useQuery(["providers", range], () => getProviderDashboardStats(range), { enabled: active });
79
+ const stats = useQuery(["providers", range], ({ signal }) => getProviderDashboardStats(range, signal), {
80
+ enabled: active,
81
+ });
79
82
  const [metric, setMetric] = useState<BurnMetric>("tokens");
80
83
  const [hidden, toggleHidden] = useHiddenSeries();
81
84
  const meta = rangeMeta(range);
82
85
 
83
86
  // Subscription windows load independently: insights for every provider
84
87
  // first, then utilization series for the one provider being charted.
85
- const windows = useQuery(["provider-windows", range], () => getProviderWindowStats(range, null), {
88
+ const windows = useQuery(["provider-windows", range], ({ signal }) => getProviderWindowStats(range, null, signal), {
86
89
  enabled: active,
87
90
  });
88
91
  const [picked, setPicked] = useState<WindowRef | null>(null);
@@ -90,7 +93,7 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
90
93
  const selectedProvider = selected?.provider ?? null;
91
94
  const accounts = useQuery(
92
95
  ["provider-windows", range, selectedProvider],
93
- () => getProviderWindowStats(range, selectedProvider),
96
+ ({ signal }) => getProviderWindowStats(range, selectedProvider, signal),
94
97
  { enabled: active && selectedProvider !== null },
95
98
  );
96
99
 
@@ -139,13 +142,13 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
139
142
 
140
143
  const burnSeries = view.burn[metric];
141
144
  const burnFormat = metric === "cost" ? (v: number) => formatCost(v) : formatCompact;
142
- const t = view.totals;
145
+ const totals = view.totals;
143
146
 
144
147
  return (
145
148
  <div className="page">
146
149
  <PageHeader
147
- title="Providers"
148
- description={`Burn, reliability and subscription headroom per provider over ${meta.windowLabel}.`}
150
+ title={t("common.providers")}
151
+ description={t("providers.description", { window: t(meta.windowKey) })}
149
152
  />
150
153
 
151
154
  <QueryView query={stats} skeleton={<ChartSkeleton height={112} />}>
@@ -153,34 +156,42 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
153
156
  <div data-stale={stats.stale}>
154
157
  <StatGrid min={190}>
155
158
  <Stat
156
- label="Providers"
159
+ label={t("common.providers")}
157
160
  value={formatInteger(providers.length)}
158
- hint={view.topProvider ? `Most tokens: ${view.topProvider.provider}` : undefined}
161
+ hint={
162
+ view.topProvider
163
+ ? t("providers.most-tokens", { provider: view.topProvider.provider })
164
+ : undefined
165
+ }
159
166
  />
160
167
  <Stat
161
- label="Requests"
162
- value={formatInteger(t.requests)}
163
- hint={`${formatInteger(t.failed)} failed`}
168
+ label={t("common.requests")}
169
+ value={formatInteger(totals.requests)}
170
+ hint={t("providers.failed-n", { count: formatInteger(totals.failed) })}
164
171
  spark={view.spark.requests}
165
172
  />
166
173
  <Stat
167
- label="Tokens"
168
- title="Uncached input + cache reads + cache writes + output"
169
- value={formatCompact(t.tokens)}
174
+ label={t("common.tokens")}
175
+ title={t("providers.tokens-title")}
176
+ value={formatCompact(totals.tokens)}
170
177
  spark={view.spark.tokens}
171
178
  />
172
179
  <Stat
173
- label="API-equivalent cost"
174
- title="What this usage would cost at public API rates"
175
- value={formatEstimatedCost(t.cost, t.unpriced)}
176
- hint={t.unpriced > 0 ? `${formatInteger(t.unpriced)} unpriced` : undefined}
180
+ label={t("providers.api-equivalent-cost")}
181
+ title={t("providers.api-cost-title")}
182
+ value={formatEstimatedCost(totals.cost, totals.unpriced)}
183
+ hint={
184
+ totals.unpriced > 0
185
+ ? t("providers.unpriced-n", { count: formatInteger(totals.unpriced) })
186
+ : undefined
187
+ }
177
188
  spark={view.spark.cost}
178
189
  sparkColor="var(--chart-secondary)"
179
190
  />
180
191
  <Stat
181
- label="Error rate"
182
- value={formatPercent(t.requests > 0 ? t.failed / t.requests : 0)}
183
- hint={`${formatInteger(t.requests - t.failed)} succeeded`}
192
+ label={t("common.error-rate")}
193
+ value={formatErrorRate(totals.requests > 0 ? totals.failed / totals.requests : 0)}
194
+ hint={t("providers.succeeded-n", { count: formatInteger(totals.requests - totals.failed) })}
184
195
  />
185
196
  </StatGrid>
186
197
  </div>
@@ -189,11 +200,16 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
189
200
 
190
201
  <Card
191
202
  index={1}
192
- title="Provider totals"
203
+ title={t("providers.totals")}
193
204
  description={
194
- t.unpriced > 0
195
- ? `Cost is an API-equivalent estimate and excludes ${formatInteger(t.unpriced)} unpriced subscription request${t.unpriced === 1 ? "" : "s"}. Select a row for its token mix.`
196
- : "Cost is an API-equivalent estimate. Select a row for its token mix."
205
+ totals.unpriced > 0
206
+ ? t(
207
+ totals.unpriced === 1
208
+ ? "providers.totals-description-unpriced-one"
209
+ : "providers.totals-description-unpriced-n",
210
+ { count: formatInteger(totals.unpriced) },
211
+ )
212
+ : t("providers.totals-description")
197
213
  }
198
214
  flush
199
215
  stale={stats.stale}
@@ -206,11 +222,21 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
206
222
  <div className="grid grid-main-side">
207
223
  <Card
208
224
  index={2}
209
- title="Burn by provider"
225
+ title={t("providers.burn")}
210
226
  description={
211
- metric === "cost" && t.unpriced > 0
212
- ? `API-equivalent estimate per ${meta.bucketMs < 86_400_000 ? "hour" : "day"}; excludes unpriced subscription requests`
213
- : `Per ${meta.bucketMs < 3_600_000 ? "5 minutes" : meta.bucketMs < 86_400_000 ? "hour" : "day"}, top ${TOP_PROVIDERS} providers stacked`
227
+ metric === "cost" && totals.unpriced > 0
228
+ ? t("providers.burn-estimate-description", {
229
+ bucket: meta.bucketMs < 86_400_000 ? t("providers.bucket-hour") : t("providers.bucket-day"),
230
+ })
231
+ : t("providers.burn-description", {
232
+ bucket:
233
+ meta.bucketMs < 3_600_000
234
+ ? t("providers.bucket-5m")
235
+ : meta.bucketMs < 86_400_000
236
+ ? t("providers.bucket-hour")
237
+ : t("providers.bucket-day"),
238
+ count: TOP_PROVIDERS,
239
+ })
214
240
  }
215
241
  actions={<Segmented size="sm" options={BURN_OPTIONS} value={metric} onChange={setMetric} />}
216
242
  stale={stats.stale}
@@ -226,7 +252,7 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
226
252
  height={260}
227
253
  format={burnFormat}
228
254
  formatTooltip={metric === "cost" ? v => formatCost(v) : formatInteger}
229
- emptyLabel="No provider activity in this range"
255
+ emptyLabel={t("providers.no-provider-activity")}
230
256
  />
231
257
  <Legend
232
258
  items={burnSeries.map(s => ({
@@ -250,8 +276,8 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
250
276
 
251
277
  <Card
252
278
  index={4}
253
- title="Subscription windows"
254
- description="What one usage window buys you and how many accounts peak demand needs. Select a row to chart it."
279
+ title={t("providers.subscription-windows")}
280
+ description={t("providers.subscription-windows-description")}
255
281
  flush
256
282
  stale={windows.stale}
257
283
  >
@@ -310,7 +336,7 @@ function ProviderTotalsTable({
310
336
  return [
311
337
  {
312
338
  key: "provider",
313
- header: "Provider",
339
+ header: t("common.provider"),
314
340
  sort: p => p.provider,
315
341
  render: p => (
316
342
  <span className="row" style={{ gap: 8 }}>
@@ -321,7 +347,7 @@ function ProviderTotalsTable({
321
347
  },
322
348
  {
323
349
  key: "requests",
324
- header: "Requests",
350
+ header: t("common.requests"),
325
351
  align: "right",
326
352
  sort: p => p.totalRequests,
327
353
  render: p => (
@@ -330,7 +356,7 @@ function ProviderTotalsTable({
330
356
  },
331
357
  {
332
358
  key: "errors",
333
- header: "Error rate",
359
+ header: t("common.error-rate"),
334
360
  align: "right",
335
361
  sort: p => (p.totalRequests > 0 ? p.failedRequests / p.totalRequests : 0),
336
362
  render: p => {
@@ -339,7 +365,7 @@ function ProviderTotalsTable({
339
365
  <span className="row" style={{ gap: 8, justifyContent: "flex-end" }}>
340
366
  <span className="num dim">{formatInteger(p.failedRequests)}</span>
341
367
  <Badge tone={p.failedRequests > 0 ? errorRateTone(rate) : "neutral"} mono>
342
- {formatPercent(rate)}
368
+ {formatErrorRate(rate)}
343
369
  </Badge>
344
370
  </span>
345
371
  );
@@ -347,16 +373,16 @@ function ProviderTotalsTable({
347
373
  },
348
374
  {
349
375
  key: "models",
350
- header: "Models",
351
- title: "Distinct models used through this provider",
376
+ header: t("common.models"),
377
+ title: t("providers.models-title"),
352
378
  align: "right",
353
379
  sort: p => p.models,
354
380
  render: p => <span className="num">{formatInteger(p.models)}</span>,
355
381
  },
356
382
  {
357
383
  key: "tokens",
358
- header: "Tokens",
359
- title: "Uncached input + cache reads + cache writes + output",
384
+ header: t("common.tokens"),
385
+ title: t("providers.tokens-title"),
360
386
  align: "right",
361
387
  sort: p => p.totalTokens,
362
388
  render: p => (
@@ -370,8 +396,8 @@ function ProviderTotalsTable({
370
396
  },
371
397
  {
372
398
  key: "share",
373
- header: "Share",
374
- title: "Fraction of all tokens in range",
399
+ header: t("common.share"),
400
+ title: t("providers.share-title"),
375
401
  align: "right",
376
402
  sort: p => p.totalTokens,
377
403
  render: p => (
@@ -380,14 +406,18 @@ function ProviderTotalsTable({
380
406
  },
381
407
  {
382
408
  key: "cost",
383
- header: "Cost",
384
- title: "API-equivalent estimate",
409
+ header: t("common.cost"),
410
+ title: t("providers.cost-title"),
385
411
  align: "right",
386
412
  sort: p => p.totalCost,
387
413
  render: p => (
388
414
  <span
389
415
  className="num"
390
- title={p.unpricedRequests > 0 ? `${formatInteger(p.unpricedRequests)} unpriced` : undefined}
416
+ title={
417
+ p.unpricedRequests > 0
418
+ ? t("providers.unpriced-n", { count: formatInteger(p.unpricedRequests) })
419
+ : undefined
420
+ }
391
421
  >
392
422
  {formatEstimatedCost(p.totalCost, p.unpricedRequests)}
393
423
  </span>
@@ -395,16 +425,16 @@ function ProviderTotalsTable({
395
425
  },
396
426
  {
397
427
  key: "tps",
398
- header: "Tokens/s",
399
- title: "Average output throughput",
428
+ header: t("common.tokens-per-second"),
429
+ title: t("providers.tps-title"),
400
430
  align: "right",
401
431
  sort: p => p.avgTokensPerSecond ?? -1,
402
432
  render: p => <span className="num">{formatTokensPerSecond(p.avgTokensPerSecond)}</span>,
403
433
  },
404
434
  {
405
435
  key: "premium",
406
- header: "Premium",
407
- title: "Premium requests (subscription multiplier units)",
436
+ header: t("common.premium"),
437
+ title: t("providers.premium-title"),
408
438
  align: "right",
409
439
  sort: p => p.totalPremiumRequests,
410
440
  render: p => (
@@ -426,7 +456,7 @@ function ProviderTotalsTable({
426
456
  selectedKey={open}
427
457
  onRowClick={p => setOpen(prev => (prev === p.provider ? null : p.provider))}
428
458
  expanded={p => (p.provider === open ? <TokenMix provider={p} /> : null)}
429
- empty={<EmptyState title="No requests in this range" />}
459
+ empty={<EmptyState title={t("providers.no-requests-in-range")} />}
430
460
  />
431
461
  );
432
462
  }
@@ -488,13 +518,13 @@ function PeakHoursCard({
488
518
  const series: ChartSeries[] = [
489
519
  {
490
520
  key: "tokens",
491
- label: "Tokens",
521
+ label: t("common.tokens"),
492
522
  color: "var(--chart-primary)",
493
523
  values: tokens.map((v, hour) => (hasData && hour === peak ? null : v)),
494
524
  },
495
525
  {
496
526
  key: "peak",
497
- label: "Tokens (peak hour)",
527
+ label: t("providers.tokens-peak-hour"),
498
528
  color: "var(--chart-secondary)",
499
529
  values: tokens.map((v, hour) => (hasData && hour === peak ? v : null)),
500
530
  },
@@ -507,20 +537,20 @@ function PeakHoursCard({
507
537
  return (
508
538
  <Card
509
539
  index={3}
510
- title="Peak burn hours"
540
+ title={t("providers.peak-burn-hours")}
511
541
  description={
512
542
  hours.hasData
513
- ? `Tokens by local hour of day; peak at ${hourLabel(hours.peak)}`
514
- : "Tokens by local hour of day"
543
+ ? t("providers.peak-hours-description", { hour: hourLabel(hours.peak) })
544
+ : t("providers.hours-description")
515
545
  }
516
546
  actions={
517
547
  <select
518
548
  className="input providers-select"
519
549
  value={current}
520
550
  onChange={e => setProvider(e.target.value)}
521
- aria-label="Provider"
551
+ aria-label={t("common.provider")}
522
552
  >
523
- <option value={ALL_PROVIDERS}>All providers</option>
553
+ <option value={ALL_PROVIDERS}>{t("providers.all-providers")}</option>
524
554
  {providers.map(p => (
525
555
  <option key={p.provider} value={p.provider}>
526
556
  {p.provider}
@@ -538,15 +568,15 @@ function PeakHoursCard({
538
568
  height={260}
539
569
  showTotal={false}
540
570
  formatTooltip={formatInteger}
541
- emptyLabel="No activity in this range"
571
+ emptyLabel={t("providers.no-activity-in-range")}
542
572
  tooltipExtra={hour => (
543
573
  <>
544
574
  <div className="chart-tooltip-row">
545
- <span className="chart-tooltip-label">Output</span>
575
+ <span className="chart-tooltip-label">{t("common.output")}</span>
546
576
  <span className="chart-tooltip-value">{formatInteger(hours.output[hour])}</span>
547
577
  </div>
548
578
  <div className="chart-tooltip-row">
549
- <span className="chart-tooltip-label">Requests</span>
579
+ <span className="chart-tooltip-label">{t("common.requests")}</span>
550
580
  <span className="chart-tooltip-value">{formatInteger(hours.requests[hour])}</span>
551
581
  </div>
552
582
  </>
@@ -578,7 +608,7 @@ function WindowInsightsTable({
578
608
  limit={12}
579
609
  selectedKey={selected ? `${selected.provider}::${selected.windowKey}` : null}
580
610
  onRowClick={i => onSelect({ provider: i.provider, windowKey: i.windowKey })}
581
- empty={<EmptyState title="No usage snapshots in this range" hint={SNAPSHOT_HINT} />}
611
+ empty={<EmptyState title={t("providers.no-snapshots-in-range")} hint={SNAPSHOT_HINT} />}
582
612
  />
583
613
  );
584
614
  }
@@ -586,53 +616,53 @@ function WindowInsightsTable({
586
616
  const INSIGHT_COLUMNS: Column<ProviderWindowInsight>[] = [
587
617
  {
588
618
  key: "window",
589
- header: "Window",
590
- sort: i => `${i.provider} ${i.windowLabel}`,
591
- render: i => <LabelCell primary={i.windowLabel} secondary={<span className="mono">{i.provider}</span>} />,
619
+ header: t("providers.window"),
620
+ sort: i => `${i.provider} ${i.windowKey}`,
621
+ render: i => <LabelCell primary={i.windowKey} secondary={<span className="mono">{i.provider}</span>} />,
592
622
  },
593
623
  {
594
624
  key: "accounts",
595
- header: "Accounts",
596
- title: "Accounts with at least one snapshot for this window in range",
625
+ header: t("providers.accounts"),
626
+ title: t("providers.accounts-title"),
597
627
  align: "right",
598
628
  sort: i => i.accounts,
599
629
  render: i => <span className="num">{formatInteger(i.accounts)}</span>,
600
630
  },
601
631
  {
602
632
  key: "cycles",
603
- header: "Resets",
604
- title: "Window resets observed (drops in used fraction)",
633
+ header: t("providers.resets"),
634
+ title: t("providers.resets-title"),
605
635
  align: "right",
606
636
  sort: i => i.cycles,
607
637
  render: i => <span className="num muted">{formatInteger(i.cycles)}</span>,
608
638
  },
609
639
  {
610
640
  key: "consumed",
611
- header: "Windows burned",
612
- title: "Subscription-window equivalents consumed in range (sum of used-fraction increases across accounts)",
641
+ header: t("providers.windows-burned"),
642
+ title: t("providers.windows-burned-title"),
613
643
  align: "right",
614
644
  sort: i => i.fractionConsumed,
615
645
  render: i => <span className="num">{i.fractionConsumed.toFixed(2)}</span>,
616
646
  },
617
647
  {
618
648
  key: "capacity",
619
- header: "Tokens / window",
620
- title: "Provider tokens burned in range ÷ windows burned — what one full window is worth",
649
+ header: t("providers.tokens-per-window"),
650
+ title: t("providers.tokens-per-window-title"),
621
651
  align: "right",
622
652
  sort: i => i.estTokensPerWindow ?? -1,
623
653
  render: i =>
624
654
  i.estTokensPerWindow !== null ? (
625
655
  <span className="num">{formatCompact(i.estTokensPerWindow)}</span>
626
656
  ) : (
627
- <span className="dim" title="Too little of the window was consumed to extrapolate">
657
+ <span className="dim" title={t("providers.too-little-consumed")}>
628
658
  –
629
659
  </span>
630
660
  ),
631
661
  },
632
662
  {
633
663
  key: "peak",
634
- header: "Peak utilization",
635
- title: "Peak of summed used fraction across accounts at any sampled instant; the bar is relative to fleet capacity",
664
+ header: t("providers.peak-utilization"),
665
+ title: t("providers.peak-utilization-title"),
636
666
  align: "right",
637
667
  sort: i => i.peakConcurrentFraction,
638
668
  render: i => {
@@ -649,14 +679,17 @@ const INSIGHT_COLUMNS: Column<ProviderWindowInsight>[] = [
649
679
  },
650
680
  {
651
681
  key: "ideal",
652
- header: "Accounts needed",
653
- title: "Accounts needed to keep peak demand under 90% of fleet capacity",
682
+ header: t("providers.accounts-needed"),
683
+ title: t("providers.accounts-needed-title"),
654
684
  align: "right",
655
685
  sort: i => i.idealAccounts - i.accounts,
656
686
  render: i =>
657
687
  i.idealAccounts > i.accounts ? (
658
688
  <Badge tone="warn" mono>
659
- {formatInteger(i.idealAccounts)} · have {formatInteger(i.accounts)}
689
+ {t("providers.ideal-accounts", {
690
+ ideal: formatInteger(i.idealAccounts),
691
+ have: formatInteger(i.accounts),
692
+ })}
660
693
  </Badge>
661
694
  ) : (
662
695
  <span className="num">{formatInteger(i.idealAccounts)}</span>
@@ -664,14 +697,14 @@ const INSIGHT_COLUMNS: Column<ProviderWindowInsight>[] = [
664
697
  },
665
698
  {
666
699
  key: "exhausted",
667
- header: "Exhaustions",
668
- title: "Transitions into an exhausted state observed in range",
700
+ header: t("providers.exhaustions"),
701
+ title: t("providers.exhaustions-title"),
669
702
  align: "right",
670
703
  sort: i => i.exhaustedEvents,
671
704
  render: i =>
672
705
  i.exhaustedEvents > 0 ? (
673
706
  <Badge tone="warn" mono>
674
- {formatInteger(i.exhaustedEvents)} exhausted
707
+ {t("providers.exhausted-n", { count: formatInteger(i.exhaustedEvents) })}
675
708
  </Badge>
676
709
  ) : (
677
710
  <span className="num dim">0</span>
@@ -736,7 +769,7 @@ function WindowUtilizationCard({
736
769
  () =>
737
770
  (insights ?? [])
738
771
  .filter(i => i.provider === selected?.provider)
739
- .map(i => ({ key: i.windowKey, label: i.windowLabel })),
772
+ .map(i => ({ key: i.windowKey, label: i.windowKey })),
740
773
  [insights, selected?.provider],
741
774
  );
742
775
  // While another provider's series are still on screen, show none of them.
@@ -790,7 +823,7 @@ function WindowUtilizationCard({
790
823
  .sort(
791
824
  (a, b) =>
792
825
  Number(b.color !== null) - Number(a.color !== null) ||
793
- a.series.windowLabel.localeCompare(b.series.windowLabel) ||
826
+ a.series.windowKey.localeCompare(b.series.windowKey) ||
794
827
  (b.latest?.fraction ?? -1) - (a.latest?.fraction ?? -1),
795
828
  );
796
829
  }, [providerSeries, names, chart.series, selected?.windowKey]);
@@ -800,11 +833,15 @@ function WindowUtilizationCard({
800
833
  return (
801
834
  <Card
802
835
  index={5}
803
- title="Window utilization"
836
+ title={t("providers.utilization")}
804
837
  description={
805
838
  chart.buckets.length > 0
806
- ? `${windowLabel ?? "Window"} used fraction per account, ${format(new Date(chart.first), "MMM d HH:mm")} – ${format(new Date(chart.last), "MMM d HH:mm")}`
807
- : "Recorded limit utilization per account"
839
+ ? t("providers.utilization-description", {
840
+ window: windowLabel ?? t("providers.window"),
841
+ from: formatDateTimeShort(chart.first),
842
+ to: formatDateTimeShort(chart.last),
843
+ })
844
+ : t("providers.utilization-description-plain")
808
845
  }
809
846
  actions={
810
847
  selected && (
@@ -817,7 +854,7 @@ function WindowUtilizationCard({
817
854
  const first = insights?.find(i => i.provider === next);
818
855
  if (first) onSelect({ provider: next, windowKey: first.windowKey });
819
856
  }}
820
- aria-label="Provider"
857
+ aria-label={t("common.provider")}
821
858
  >
822
859
  {providers.map(p => (
823
860
  <option key={p} value={p}>
@@ -829,7 +866,7 @@ function WindowUtilizationCard({
829
866
  className="input providers-select"
830
867
  value={selected.windowKey}
831
868
  onChange={e => onSelect({ provider: selected.provider, windowKey: e.target.value })}
832
- aria-label="Window"
869
+ aria-label={t("providers.window")}
833
870
  >
834
871
  {windows.map(w => (
835
872
  <option key={w.key} value={w.key}>
@@ -845,7 +882,7 @@ function WindowUtilizationCard({
845
882
  {insights === null ? (
846
883
  <ChartSkeleton height={260} />
847
884
  ) : selected === null ? (
848
- <EmptyState title="No usage snapshots in this range" hint={SNAPSHOT_HINT} />
885
+ <EmptyState title={t("providers.no-snapshots-in-range")} hint={SNAPSHOT_HINT} />
849
886
  ) : (
850
887
  <QueryView query={accounts} skeleton={<ChartSkeleton height={260} />}>
851
888
  {() =>
@@ -872,12 +909,12 @@ function WindowUtilizationCard({
872
909
  format={v => formatPercent(v, 0)}
873
910
  formatTooltip={v => formatPercent(v, 1)}
874
911
  references={[{ value: 1, label: "100%" }]}
875
- emptyLabel="No utilization readings for this window"
912
+ emptyLabel={t("providers.no-utilization-readings")}
876
913
  tooltipExtra={i => {
877
914
  const exhausted = chart.exhausted[i];
878
915
  return exhausted.length > 0 ? (
879
916
  <div className="chart-tooltip-row">
880
- <span className="chart-tooltip-label tone-bad">Exhausted</span>
917
+ <span className="chart-tooltip-label tone-bad">{t("providers.exhausted")}</span>
881
918
  <span className="chart-tooltip-value">{exhausted.join(", ")}</span>
882
919
  </div>
883
920
  ) : null;
@@ -978,16 +1015,16 @@ function buildUtilization(accounts: UsageWindowSeries[], names: ReadonlyMap<stri
978
1015
  }
979
1016
 
980
1017
  function statusBadge(latest: AccountRow["latest"]): ReactNode {
981
- if (!latest) return <span className="dim">No reading</span>;
982
- if (latest.exhausted) return <Badge tone="bad">Exhausted</Badge>;
983
- if (latest.fraction >= 0.8) return <Badge tone="warn">High</Badge>;
984
- return <Badge tone="ok">OK</Badge>;
1018
+ if (!latest) return <span className="dim">{t("providers.no-reading")}</span>;
1019
+ if (latest.exhausted) return <Badge tone="bad">{t("providers.exhausted")}</Badge>;
1020
+ if (latest.fraction >= 0.8) return <Badge tone="warn">{t("providers.high")}</Badge>;
1021
+ return <Badge tone="ok">{t("ui.status-ok")}</Badge>;
985
1022
  }
986
1023
 
987
1024
  const ACCOUNT_COLUMNS: Column<AccountRow>[] = [
988
1025
  {
989
1026
  key: "account",
990
- header: "Account",
1027
+ header: t("providers.account"),
991
1028
  sort: r => r.name,
992
1029
  render: r => (
993
1030
  <span className="row" style={{ gap: 8 }}>
@@ -1000,13 +1037,13 @@ const ACCOUNT_COLUMNS: Column<AccountRow>[] = [
1000
1037
  },
1001
1038
  {
1002
1039
  key: "window",
1003
- header: "Window",
1004
- sort: r => r.series.windowLabel,
1005
- render: r => <span className={r.color ? undefined : "muted"}>{r.series.windowLabel}</span>,
1040
+ header: t("providers.window"),
1041
+ sort: r => r.series.windowKey,
1042
+ render: r => <span className={r.color ? undefined : "muted"}>{r.series.windowKey}</span>,
1006
1043
  },
1007
1044
  {
1008
1045
  key: "latest",
1009
- header: "Latest used",
1046
+ header: t("providers.latest-used"),
1010
1047
  align: "right",
1011
1048
  sort: r => r.latest?.fraction ?? -1,
1012
1049
  render: r =>
@@ -1023,27 +1060,27 @@ const ACCOUNT_COLUMNS: Column<AccountRow>[] = [
1023
1060
  },
1024
1061
  {
1025
1062
  key: "status",
1026
- header: "Status",
1063
+ header: t("providers.status"),
1027
1064
  sort: r => (r.latest?.exhausted ? 2 : (r.latest?.fraction ?? 0) >= 0.8 ? 1 : 0),
1028
1065
  render: r => statusBadge(r.latest),
1029
1066
  },
1030
1067
  {
1031
1068
  key: "peak",
1032
- header: "Peak in range",
1069
+ header: t("providers.peak-in-range"),
1033
1070
  align: "right",
1034
1071
  sort: r => r.peak ?? -1,
1035
1072
  render: r => <span className="num">{r.peak === null ? "–" : formatPercent(r.peak, 0)}</span>,
1036
1073
  },
1037
1074
  {
1038
1075
  key: "samples",
1039
- header: "Snapshots",
1076
+ header: t("providers.snapshots"),
1040
1077
  align: "right",
1041
1078
  sort: r => r.samples,
1042
1079
  render: r => <span className="num muted">{formatInteger(r.samples)}</span>,
1043
1080
  },
1044
1081
  {
1045
1082
  key: "recorded",
1046
- header: "Recorded",
1083
+ header: t("providers.recorded"),
1047
1084
  align: "right",
1048
1085
  sort: r => r.latest?.timestamp ?? 0,
1049
1086
  render: r => <span className="muted">{r.latest ? formatRelativeTime(r.latest.timestamp) : "–"}</span>,