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