@acarmisc/backstage-plugin-litellm 0.12.3 → 0.13.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.
package/dist/index.esm.js CHANGED
@@ -152,109 +152,746 @@ var init_api = __esm({
152
152
  }
153
153
  });
154
154
 
155
+ // src/components/ui.tsx
156
+ import React from "react";
157
+ import Box from "@mui/material/Box";
158
+ import ButtonBase from "@mui/material/ButtonBase";
159
+ import Paper from "@mui/material/Paper";
160
+ import Typography from "@mui/material/Typography";
161
+ import { alpha, useTheme } from "@mui/material/styles";
162
+ function chartColor(name) {
163
+ if (name === "Other") return OTHER_COLOR;
164
+ let h = 5381;
165
+ for (let i = 0; i < name.length; i++) h = (h << 5) + h + name.charCodeAt(i) >>> 0;
166
+ return CHART_COLORS[h % CHART_COLORS.length];
167
+ }
168
+ function useChartTheme() {
169
+ const theme = useTheme();
170
+ const tickFill = theme.palette.text.secondary;
171
+ return {
172
+ grid: {
173
+ stroke: alpha(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.12 : 0.08),
174
+ strokeDasharray: "2 4",
175
+ vertical: false
176
+ },
177
+ axis: {
178
+ tick: { fontSize: 11, fill: tickFill },
179
+ tickLine: false,
180
+ axisLine: { stroke: alpha(theme.palette.text.primary, 0.12) }
181
+ },
182
+ legend: {
183
+ wrapperStyle: {
184
+ fontSize: 11,
185
+ color: tickFill,
186
+ paddingTop: 8
187
+ },
188
+ iconType: "circle",
189
+ iconSize: 8
190
+ },
191
+ cursor: { fill: alpha(theme.palette.text.primary, 0.05) }
192
+ };
193
+ }
194
+ function toneColor(theme, tone) {
195
+ switch (tone) {
196
+ case "success":
197
+ return theme.palette.success.main;
198
+ case "warning":
199
+ return theme.palette.warning.main;
200
+ case "danger":
201
+ return theme.palette.error.main;
202
+ case "info":
203
+ return theme.palette.info.main;
204
+ case "accent":
205
+ return theme.palette.primary.main;
206
+ default:
207
+ return theme.palette.text.secondary;
208
+ }
209
+ }
210
+ function SegmentedControl({
211
+ value,
212
+ onChange,
213
+ options
214
+ }) {
215
+ return /* @__PURE__ */ React.createElement(
216
+ Box,
217
+ {
218
+ role: "tablist",
219
+ sx: (theme) => ({
220
+ display: "inline-flex",
221
+ gap: 0.25,
222
+ p: 0.375,
223
+ borderRadius: 1.5,
224
+ border: "1px solid",
225
+ borderColor: theme.palette.divider
226
+ })
227
+ },
228
+ options.map((opt) => {
229
+ const selected = opt.value === value;
230
+ return /* @__PURE__ */ React.createElement(
231
+ ButtonBase,
232
+ {
233
+ key: opt.value,
234
+ role: "tab",
235
+ "aria-selected": selected,
236
+ onClick: () => onChange(opt.value),
237
+ sx: (theme) => ({
238
+ px: 1.5,
239
+ height: 28,
240
+ borderRadius: 1,
241
+ fontSize: 13,
242
+ fontWeight: selected ? 700 : 500,
243
+ // Tint the selection with the accent rather than swapping in a
244
+ // surface colour: on a dark theme `background.paper` sits *behind*
245
+ // the track, so the selected chip would read as recessed.
246
+ color: selected ? theme.palette.primary.main : theme.palette.text.secondary,
247
+ bgcolor: selected ? alpha(theme.palette.primary.main, theme.palette.mode === "dark" ? 0.2 : 0.11) : "transparent",
248
+ transition: theme.transitions.create(["background-color", "color"]),
249
+ "&:hover": {
250
+ color: selected ? theme.palette.primary.main : theme.palette.text.primary,
251
+ bgcolor: selected ? alpha(theme.palette.primary.main, theme.palette.mode === "dark" ? 0.26 : 0.15) : alpha(theme.palette.text.primary, 0.05)
252
+ }
253
+ })
254
+ },
255
+ opt.label
256
+ );
257
+ })
258
+ );
259
+ }
260
+ var MONTHS, fmtDateShort, fmtCompact, fmtUsdCompact, CHART_COLORS, SERIES, OTHER_COLOR, ChartTooltip, SeriesLegend, StatusPill, TagChip, SectionCard, ChartCard, MetricStrip, Stat, Meter, dataTableSx, quietIconButtonSx, EmptyState;
261
+ var init_ui = __esm({
262
+ "src/components/ui.tsx"() {
263
+ "use strict";
264
+ MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
265
+ fmtDateShort = (value) => {
266
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value ?? "");
267
+ if (!m) return value;
268
+ return `${MONTHS[Number(m[2]) - 1]} ${Number(m[3])}`;
269
+ };
270
+ fmtCompact = (n) => {
271
+ const v = n ?? 0;
272
+ const abs = Math.abs(v);
273
+ if (abs >= 1e9) return `${(v / 1e9).toFixed(abs >= 1e10 ? 0 : 1)}B`;
274
+ if (abs >= 1e6) return `${(v / 1e6).toFixed(abs >= 1e7 ? 0 : 1)}M`;
275
+ if (abs >= 1e3) return `${(v / 1e3).toFixed(abs >= 1e4 ? 0 : 1)}K`;
276
+ return String(Math.round(v));
277
+ };
278
+ fmtUsdCompact = (n) => {
279
+ const v = n ?? 0;
280
+ if (Math.abs(v) >= 1e3) return `$${fmtCompact(v)}`;
281
+ if (Math.abs(v) >= 10) return `$${v.toFixed(0)}`;
282
+ return `$${v.toFixed(2)}`;
283
+ };
284
+ CHART_COLORS = [
285
+ "#6366F1",
286
+ // indigo
287
+ "#14B8A6",
288
+ // teal
289
+ "#F59E0B",
290
+ // amber
291
+ "#EC4899",
292
+ // pink
293
+ "#38BDF8",
294
+ // sky
295
+ "#84CC16",
296
+ // lime
297
+ "#F97316",
298
+ // orange
299
+ "#A855F7"
300
+ // violet
301
+ ];
302
+ SERIES = {
303
+ input: "#6366F1",
304
+ output: "#14B8A6",
305
+ success: "#10B981",
306
+ failure: "#EF4444",
307
+ spend: "#F59E0B",
308
+ budget: "#EF4444"
309
+ };
310
+ OTHER_COLOR = "#94A3B8";
311
+ ChartTooltip = ({
312
+ active,
313
+ label,
314
+ payload,
315
+ valueFormatter = (v) => v.toLocaleString(),
316
+ labelFormatter = fmtDateShort,
317
+ hideZero = false
318
+ }) => {
319
+ if (!active || !payload?.length) return null;
320
+ const rows = payload.filter(
321
+ (p) => p.value !== void 0 && p.value !== null && (!hideZero || p.value !== 0)
322
+ );
323
+ if (!rows.length) return null;
324
+ return /* @__PURE__ */ React.createElement(
325
+ Paper,
326
+ {
327
+ elevation: 8,
328
+ sx: {
329
+ px: 1.5,
330
+ py: 1,
331
+ borderRadius: 1.5,
332
+ border: "1px solid",
333
+ borderColor: "divider",
334
+ minWidth: 150,
335
+ pointerEvents: "none"
336
+ }
337
+ },
338
+ /* @__PURE__ */ React.createElement(
339
+ Typography,
340
+ {
341
+ variant: "caption",
342
+ sx: { display: "block", fontWeight: 700, mb: 0.75, letterSpacing: "0.02em" }
343
+ },
344
+ typeof label === "string" ? labelFormatter(label) : label
345
+ ),
346
+ rows.map((row, i) => /* @__PURE__ */ React.createElement(
347
+ Box,
348
+ {
349
+ key: `${row.dataKey ?? row.name ?? i}`,
350
+ sx: { display: "flex", alignItems: "center", gap: 1, mt: i === 0 ? 0 : 0.5 }
351
+ },
352
+ /* @__PURE__ */ React.createElement(
353
+ Box,
354
+ {
355
+ sx: {
356
+ width: 8,
357
+ height: 8,
358
+ borderRadius: "50%",
359
+ bgcolor: row.color,
360
+ flexShrink: 0
361
+ }
362
+ }
363
+ ),
364
+ /* @__PURE__ */ React.createElement(
365
+ Typography,
366
+ {
367
+ variant: "caption",
368
+ color: "text.secondary",
369
+ sx: { flex: 1, mr: 1.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }
370
+ },
371
+ row.name
372
+ ),
373
+ /* @__PURE__ */ React.createElement(Typography, { variant: "caption", sx: { fontWeight: 700, fontVariantNumeric: "tabular-nums" } }, valueFormatter(row.value))
374
+ ))
375
+ );
376
+ };
377
+ SeriesLegend = ({
378
+ series
379
+ }) => /* @__PURE__ */ React.createElement(
380
+ Box,
381
+ {
382
+ sx: {
383
+ display: "flex",
384
+ flexWrap: "wrap",
385
+ gap: 0.5,
386
+ columnGap: 2,
387
+ rowGap: 0.5,
388
+ mt: 1.5
389
+ }
390
+ },
391
+ series.map((s) => /* @__PURE__ */ React.createElement(Box, { key: s.name, sx: { display: "flex", alignItems: "center", gap: 0.75, minWidth: 0 } }, /* @__PURE__ */ React.createElement(
392
+ Box,
393
+ {
394
+ sx: { width: 8, height: 8, borderRadius: "50%", bgcolor: s.color, flexShrink: 0 }
395
+ }
396
+ ), /* @__PURE__ */ React.createElement(
397
+ Typography,
398
+ {
399
+ variant: "caption",
400
+ color: "text.secondary",
401
+ title: s.name,
402
+ sx: {
403
+ fontSize: 11,
404
+ maxWidth: 190,
405
+ overflow: "hidden",
406
+ textOverflow: "ellipsis",
407
+ whiteSpace: "nowrap"
408
+ }
409
+ },
410
+ s.name
411
+ )))
412
+ );
413
+ StatusPill = ({ label, tone = "neutral", dot = true, title, sx }) => /* @__PURE__ */ React.createElement(
414
+ Box,
415
+ {
416
+ component: "span",
417
+ title,
418
+ sx: [
419
+ (theme) => {
420
+ const c = toneColor(theme, tone);
421
+ return {
422
+ display: "inline-flex",
423
+ alignItems: "center",
424
+ gap: 0.625,
425
+ height: 22,
426
+ px: 0.875,
427
+ borderRadius: "6px",
428
+ border: "1px solid",
429
+ borderColor: alpha(c, tone === "neutral" ? 0.25 : 0.35),
430
+ bgcolor: alpha(c, tone === "neutral" ? 0.06 : 0.12),
431
+ color: tone === "neutral" ? theme.palette.text.secondary : c,
432
+ fontSize: 11.5,
433
+ fontWeight: 600,
434
+ lineHeight: 1,
435
+ letterSpacing: "0.01em",
436
+ whiteSpace: "nowrap",
437
+ maxWidth: "100%"
438
+ };
439
+ },
440
+ ...Array.isArray(sx) ? sx : [sx]
441
+ ]
442
+ },
443
+ dot && /* @__PURE__ */ React.createElement(
444
+ Box,
445
+ {
446
+ component: "span",
447
+ sx: (theme) => ({
448
+ width: 6,
449
+ height: 6,
450
+ borderRadius: "50%",
451
+ bgcolor: toneColor(theme, tone),
452
+ flexShrink: 0
453
+ })
454
+ }
455
+ ),
456
+ /* @__PURE__ */ React.createElement(Box, { component: "span", sx: { overflow: "hidden", textOverflow: "ellipsis" } }, label)
457
+ );
458
+ TagChip = ({
459
+ label,
460
+ title,
461
+ mono = true
462
+ }) => /* @__PURE__ */ React.createElement(
463
+ Box,
464
+ {
465
+ component: "span",
466
+ title,
467
+ sx: (theme) => ({
468
+ display: "inline-flex",
469
+ alignItems: "center",
470
+ height: 20,
471
+ px: 0.75,
472
+ borderRadius: "5px",
473
+ bgcolor: alpha(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.08 : 0.05),
474
+ color: theme.palette.text.secondary,
475
+ fontFamily: mono ? "ui-monospace, SFMono-Regular, Menlo, monospace" : void 0,
476
+ fontSize: 11,
477
+ lineHeight: 1,
478
+ whiteSpace: "nowrap",
479
+ maxWidth: 220,
480
+ overflow: "hidden",
481
+ textOverflow: "ellipsis"
482
+ })
483
+ },
484
+ label
485
+ );
486
+ SectionCard = ({ title, subtitle, actions, flush = false, children }) => /* @__PURE__ */ React.createElement(Paper, { sx: { borderRadius: 2, overflow: "hidden" } }, (title || actions) && /* @__PURE__ */ React.createElement(
487
+ Box,
488
+ {
489
+ sx: {
490
+ display: "flex",
491
+ alignItems: "center",
492
+ justifyContent: "space-between",
493
+ gap: 2,
494
+ flexWrap: "wrap",
495
+ px: 2.5,
496
+ py: 2
497
+ }
498
+ },
499
+ /* @__PURE__ */ React.createElement(Box, { sx: { minWidth: 0 } }, /* @__PURE__ */ React.createElement(Typography, { variant: "h6", sx: { lineHeight: 1.2 } }, title), subtitle && /* @__PURE__ */ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, subtitle)),
500
+ actions && /* @__PURE__ */ React.createElement(Box, { sx: { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" } }, actions)
501
+ ), /* @__PURE__ */ React.createElement(Box, { sx: flush ? void 0 : { px: 2.5, pb: 2.5, pt: title ? 0 : 2.5 } }, children));
502
+ ChartCard = ({ title, meta, height = 240, children }) => (
503
+ // The plot area must have an explicit height, never a flex-derived one:
504
+ // Recharts' ResponsiveContainer measures its parent, so a parent that sizes
505
+ // itself from its children instead spins in a measure/resize loop.
506
+ /* @__PURE__ */ React.createElement(Paper, { variant: "outlined", sx: { p: 2, borderRadius: 2, height: "100%" } }, /* @__PURE__ */ React.createElement(Box, { sx: { display: "flex", alignItems: "baseline", gap: 1, mb: 1.5, flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement(
507
+ Typography,
508
+ {
509
+ variant: "subtitle2",
510
+ sx: { fontSize: 12, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase" }
511
+ },
512
+ title
513
+ ), meta && /* @__PURE__ */ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, meta)), /* @__PURE__ */ React.createElement(Box, { sx: { height, width: "100%" } }, children))
514
+ );
515
+ MetricStrip = ({ metrics }) => /* @__PURE__ */ React.createElement(
516
+ Paper,
517
+ {
518
+ variant: "outlined",
519
+ sx: {
520
+ borderRadius: 2,
521
+ display: "grid",
522
+ gridTemplateColumns: { xs: "1fr 1fr", md: `repeat(${metrics.length}, 1fr)` },
523
+ overflow: "hidden"
524
+ }
525
+ },
526
+ metrics.map((m, i) => /* @__PURE__ */ React.createElement(
527
+ Box,
528
+ {
529
+ key: m.label,
530
+ sx: (theme) => ({
531
+ px: 2.5,
532
+ py: 2,
533
+ borderLeft: i === 0 ? 0 : "1px solid",
534
+ borderTop: 0,
535
+ borderColor: "divider",
536
+ [theme.breakpoints.down("md")]: {
537
+ borderLeft: i % 2 === 0 ? 0 : "1px solid",
538
+ borderTop: i < 2 ? 0 : "1px solid",
539
+ borderColor: "divider"
540
+ }
541
+ })
542
+ },
543
+ /* @__PURE__ */ React.createElement(Box, { sx: { display: "flex", alignItems: "center", gap: 0.875, mb: 0.75 } }, /* @__PURE__ */ React.createElement(
544
+ Box,
545
+ {
546
+ sx: (theme) => ({
547
+ width: 6,
548
+ height: 6,
549
+ borderRadius: "50%",
550
+ bgcolor: toneColor(theme, m.tone ?? "accent"),
551
+ flexShrink: 0
552
+ })
553
+ }
554
+ ), /* @__PURE__ */ React.createElement(
555
+ Typography,
556
+ {
557
+ variant: "caption",
558
+ color: "text.secondary",
559
+ sx: { fontSize: 11, fontWeight: 600, letterSpacing: "0.07em", textTransform: "uppercase" }
560
+ },
561
+ m.label
562
+ )),
563
+ /* @__PURE__ */ React.createElement(
564
+ Typography,
565
+ {
566
+ sx: { fontSize: 26, fontWeight: 700, lineHeight: 1.15, fontVariantNumeric: "tabular-nums" }
567
+ },
568
+ m.value
569
+ ),
570
+ /* @__PURE__ */ React.createElement(
571
+ Typography,
572
+ {
573
+ variant: "caption",
574
+ color: "text.secondary",
575
+ sx: { display: "block", mt: 0.5, minHeight: 16 }
576
+ },
577
+ m.hint ?? "\xA0"
578
+ )
579
+ ))
580
+ );
581
+ Stat = ({ label, value }) => /* @__PURE__ */ React.createElement(Box, { sx: { minWidth: 0 } }, /* @__PURE__ */ React.createElement(
582
+ Typography,
583
+ {
584
+ variant: "caption",
585
+ color: "text.secondary",
586
+ sx: { display: "block", fontSize: 10.5, fontWeight: 600, letterSpacing: "0.07em", textTransform: "uppercase" }
587
+ },
588
+ label
589
+ ), /* @__PURE__ */ React.createElement(
590
+ Typography,
591
+ {
592
+ variant: "body2",
593
+ sx: { fontWeight: 600, fontVariantNumeric: "tabular-nums", mt: 0.25 }
594
+ },
595
+ value
596
+ ));
597
+ Meter = ({
598
+ value,
599
+ tone = "accent",
600
+ height = 5
601
+ }) => /* @__PURE__ */ React.createElement(
602
+ Box,
603
+ {
604
+ sx: (theme) => ({
605
+ height,
606
+ borderRadius: height,
607
+ bgcolor: alpha(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.12 : 0.08),
608
+ overflow: "hidden"
609
+ })
610
+ },
611
+ /* @__PURE__ */ React.createElement(
612
+ Box,
613
+ {
614
+ sx: (theme) => ({
615
+ height: "100%",
616
+ width: `${Math.max(0, Math.min(100, value))}%`,
617
+ borderRadius: height,
618
+ bgcolor: toneColor(theme, tone),
619
+ transition: theme.transitions.create("width")
620
+ })
621
+ }
622
+ )
623
+ );
624
+ dataTableSx = (theme) => ({
625
+ // Element selectors, not `.MuiTableCell-head`: host apps may set a MUI
626
+ // classname prefix (Backstage ships `v5-`), which breaks global class targeting.
627
+ "& thead th": {
628
+ fontSize: 11,
629
+ fontWeight: 700,
630
+ letterSpacing: "0.07em",
631
+ textTransform: "uppercase",
632
+ color: theme.palette.text.secondary,
633
+ backgroundColor: alpha(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.04 : 0.02),
634
+ borderBottom: `1px solid ${theme.palette.divider}`,
635
+ whiteSpace: "nowrap",
636
+ paddingTop: theme.spacing(1.25),
637
+ paddingBottom: theme.spacing(1.25)
638
+ },
639
+ "& tbody td": {
640
+ borderBottom: `1px solid ${alpha(theme.palette.divider, 0.6)}`,
641
+ paddingTop: theme.spacing(1.25),
642
+ paddingBottom: theme.spacing(1.25),
643
+ fontVariantNumeric: "tabular-nums"
644
+ },
645
+ "& tbody tr:last-of-type td": {
646
+ borderBottom: 0
647
+ },
648
+ "& tbody tr:hover": {
649
+ backgroundColor: alpha(theme.palette.text.primary, 0.03)
650
+ }
651
+ });
652
+ quietIconButtonSx = (tone = "neutral") => (theme) => ({
653
+ color: theme.palette.text.secondary,
654
+ "&:hover": {
655
+ color: toneColor(theme, tone),
656
+ bgcolor: alpha(toneColor(theme, tone), 0.1)
657
+ }
658
+ });
659
+ EmptyState = ({ message, hint, height, action }) => /* @__PURE__ */ React.createElement(
660
+ Box,
661
+ {
662
+ sx: {
663
+ height,
664
+ minHeight: height ? void 0 : 120,
665
+ display: "flex",
666
+ flexDirection: "column",
667
+ alignItems: "center",
668
+ justifyContent: "center",
669
+ gap: 1,
670
+ textAlign: "center",
671
+ py: 3
672
+ }
673
+ },
674
+ /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "text.secondary" }, message),
675
+ hint && /* @__PURE__ */ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, hint),
676
+ action
677
+ );
678
+ }
679
+ });
680
+
155
681
  // src/components/DashboardHeader.tsx
156
- import React, { useMemo } from "react";
157
- import { Box, Typography, LinearProgress, Paper, Chip, Button, Stack } from "@mui/material";
158
- import { Add, Key, Warning, Schedule } from "@mui/icons-material";
159
- var DashboardHeader;
682
+ import React2, { useMemo } from "react";
683
+ import Box2 from "@mui/material/Box";
684
+ import Typography2 from "@mui/material/Typography";
685
+ import Skeleton from "@mui/material/Skeleton";
686
+ import Paper2 from "@mui/material/Paper";
687
+ import Button from "@mui/material/Button";
688
+ import Divider from "@mui/material/Divider";
689
+ import { alpha as alpha2 } from "@mui/material/styles";
690
+ import { Add } from "@mui/icons-material";
691
+ function initials(name) {
692
+ const local = name.split("@")[0] ?? name;
693
+ const parts = local.split(/[.\-_\s]+/).filter(Boolean);
694
+ if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
695
+ return local.slice(0, 2).toUpperCase();
696
+ }
697
+ var CountStat, DashboardHeader;
160
698
  var init_DashboardHeader = __esm({
161
699
  "src/components/DashboardHeader.tsx"() {
162
700
  "use strict";
163
701
  init_api();
702
+ init_ui();
703
+ CountStat = ({ value, label, tone }) => {
704
+ const active = value > 0;
705
+ return /* @__PURE__ */ React2.createElement(Box2, { sx: { display: "flex", alignItems: "center", gap: 1 } }, /* @__PURE__ */ React2.createElement(
706
+ Box2,
707
+ {
708
+ sx: (theme) => ({
709
+ width: 7,
710
+ height: 7,
711
+ borderRadius: "50%",
712
+ flexShrink: 0,
713
+ bgcolor: active ? tone === "danger" && theme.palette.error.main || tone === "warning" && theme.palette.warning.main || theme.palette.primary.main : alpha2(theme.palette.text.primary, 0.25)
714
+ })
715
+ }
716
+ ), /* @__PURE__ */ React2.createElement(
717
+ Typography2,
718
+ {
719
+ component: "span",
720
+ sx: { fontSize: 15, fontWeight: 700, lineHeight: 1, fontVariantNumeric: "tabular-nums" }
721
+ },
722
+ value
723
+ ), /* @__PURE__ */ React2.createElement(
724
+ Typography2,
725
+ {
726
+ component: "span",
727
+ color: "text.secondary",
728
+ sx: { fontSize: 11, fontWeight: 600, letterSpacing: "0.07em", textTransform: "uppercase" }
729
+ },
730
+ label
731
+ ));
732
+ };
164
733
  DashboardHeader = ({
165
734
  userInfo,
166
735
  teams,
167
736
  keys,
168
737
  loading,
169
- onGenerateKeyClick
738
+ onGenerateKeyClick,
739
+ tabs
170
740
  }) => {
171
741
  const counts = useMemo(() => {
172
742
  const expired = keys.filter((k) => expiryStatus(k.expires_at) === "expired").length;
173
743
  const expiringSoon = keys.filter((k) => expiryStatus(k.expires_at) === "soon").length;
174
744
  return { total: keys.length, expired, expiringSoon };
175
745
  }, [keys]);
176
- if (loading) {
177
- return /* @__PURE__ */ React.createElement(Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React.createElement(LinearProgress, null));
178
- }
179
746
  const displayName = userInfo.user_email ?? userInfo.email ?? userInfo.user_id;
180
- return /* @__PURE__ */ React.createElement(Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "flex-start", gap: 2, flexWrap: "wrap" }, /* @__PURE__ */ React.createElement(Box, { flexGrow: 1 }, /* @__PURE__ */ React.createElement(Typography, { variant: "h6" }, displayName), /* @__PURE__ */ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, userInfo.user_id), teams.length > 0 && /* @__PURE__ */ React.createElement(Box, { display: "flex", gap: 0.75, flexWrap: "wrap", mt: 1 }, teams.map((team) => /* @__PURE__ */ React.createElement(
181
- Chip,
747
+ return /* @__PURE__ */ React2.createElement(Paper2, { sx: { borderRadius: 2, overflow: "hidden" } }, /* @__PURE__ */ React2.createElement(
748
+ Box2,
182
749
  {
183
- key: team.team_id,
184
- label: team.team_alias || team.team_id,
185
- size: "small",
186
- variant: "outlined",
187
- color: "primary"
188
- }
189
- )))), /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center", gap: 2, flexWrap: "wrap" }, /* @__PURE__ */ React.createElement(Stack, { direction: "row", spacing: 1 }, /* @__PURE__ */ React.createElement(Chip, { icon: /* @__PURE__ */ React.createElement(Key, { fontSize: "small" }), label: `${counts.total} keys`, size: "small" }), /* @__PURE__ */ React.createElement(
190
- Chip,
191
- {
192
- icon: /* @__PURE__ */ React.createElement(Warning, { fontSize: "small" }),
193
- label: `${counts.expired} expired`,
194
- size: "small",
195
- color: counts.expired > 0 ? "error" : "default"
196
- }
197
- ), /* @__PURE__ */ React.createElement(
198
- Chip,
199
- {
200
- icon: /* @__PURE__ */ React.createElement(Schedule, { fontSize: "small" }),
201
- label: `${counts.expiringSoon} expiring soon`,
202
- size: "small",
203
- color: counts.expiringSoon > 0 ? "warning" : "default"
204
- }
205
- )), /* @__PURE__ */ React.createElement(
206
- Button,
750
+ sx: {
751
+ display: "flex",
752
+ alignItems: "flex-start",
753
+ gap: 2,
754
+ flexWrap: "wrap",
755
+ px: 2.5,
756
+ pt: 2.5,
757
+ pb: 2
758
+ }
759
+ },
760
+ /* @__PURE__ */ React2.createElement(
761
+ Box2,
762
+ {
763
+ sx: (theme) => ({
764
+ width: 44,
765
+ height: 44,
766
+ borderRadius: 1.5,
767
+ flexShrink: 0,
768
+ display: "flex",
769
+ alignItems: "center",
770
+ justifyContent: "center",
771
+ bgcolor: alpha2(theme.palette.primary.main, 0.12),
772
+ color: theme.palette.primary.main,
773
+ fontSize: 15,
774
+ fontWeight: 700,
775
+ letterSpacing: "0.02em"
776
+ })
777
+ },
778
+ initials(displayName)
779
+ ),
780
+ /* @__PURE__ */ React2.createElement(Box2, { sx: { flexGrow: 1, minWidth: 220 } }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "h6", sx: { lineHeight: 1.25, wordBreak: "break-word" } }, displayName), /* @__PURE__ */ React2.createElement(
781
+ Typography2,
782
+ {
783
+ variant: "caption",
784
+ color: "text.secondary",
785
+ sx: { display: "block", fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }
786
+ },
787
+ userInfo.user_id
788
+ ), /* @__PURE__ */ React2.createElement(Box2, { sx: { display: "flex", gap: 0.75, flexWrap: "wrap", mt: 1, minHeight: 20 } }, loading ? /* @__PURE__ */ React2.createElement(Skeleton, { variant: "rounded", width: 140, height: 20 }) : teams.map((team) => /* @__PURE__ */ React2.createElement(
789
+ TagChip,
790
+ {
791
+ key: team.team_id,
792
+ label: team.team_alias || team.team_id,
793
+ title: team.team_id
794
+ }
795
+ )))),
796
+ /* @__PURE__ */ React2.createElement(
797
+ Button,
798
+ {
799
+ variant: "contained",
800
+ color: "primary",
801
+ disableElevation: true,
802
+ startIcon: /* @__PURE__ */ React2.createElement(Add, null),
803
+ onClick: onGenerateKeyClick
804
+ },
805
+ "Generate New Key"
806
+ )
807
+ ), /* @__PURE__ */ React2.createElement(Divider, null), /* @__PURE__ */ React2.createElement(
808
+ Box2,
207
809
  {
208
- variant: "contained",
209
- color: "primary",
210
- startIcon: /* @__PURE__ */ React.createElement(Add, null),
211
- onClick: onGenerateKeyClick
810
+ sx: {
811
+ display: "flex",
812
+ alignItems: "center",
813
+ gap: 2.5,
814
+ flexWrap: "wrap",
815
+ px: 2.5,
816
+ py: 1.5
817
+ }
212
818
  },
213
- "Generate New Key"
214
- ))));
819
+ /* @__PURE__ */ React2.createElement(CountStat, { value: counts.total, label: "keys", tone: "accent" }),
820
+ /* @__PURE__ */ React2.createElement(Divider, { orientation: "vertical", flexItem: true, sx: { my: 0.25 } }),
821
+ /* @__PURE__ */ React2.createElement(CountStat, { value: counts.expired, label: "expired", tone: "danger" }),
822
+ /* @__PURE__ */ React2.createElement(Divider, { orientation: "vertical", flexItem: true, sx: { my: 0.25 } }),
823
+ /* @__PURE__ */ React2.createElement(CountStat, { value: counts.expiringSoon, label: "expiring soon", tone: "warning" })
824
+ ), tabs && /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Divider, null), /* @__PURE__ */ React2.createElement(Box2, { sx: { px: 1 } }, tabs)));
215
825
  };
216
826
  }
217
827
  });
218
828
 
219
829
  // src/components/KeysTable.tsx
220
- import React2, { useState, useMemo as useMemo2 } from "react";
221
- import {
222
- Paper as Paper2,
223
- Table,
224
- TableBody,
225
- TableCell,
226
- TableContainer,
227
- TableHead,
228
- TableRow,
229
- Button as Button2,
230
- IconButton,
231
- Box as Box2,
232
- Typography as Typography2,
233
- Dialog,
234
- DialogTitle,
235
- DialogContent,
236
- DialogActions,
237
- TextField,
238
- Chip as Chip2,
239
- CircularProgress,
240
- Autocomplete,
241
- LinearProgress as LinearProgress2,
242
- InputAdornment
243
- } from "@mui/material";
244
- import { ContentCopy, Delete, Add as Add2, Edit, Autorenew, Search, Warning as Warning2, Lock, LockOpen } from "@mui/icons-material";
245
- function ExpiryChip({ expiresAt }) {
830
+ import React3, { useState, useMemo as useMemo2 } from "react";
831
+ import Table from "@mui/material/Table";
832
+ import TableBody from "@mui/material/TableBody";
833
+ import TableCell from "@mui/material/TableCell";
834
+ import TableContainer from "@mui/material/TableContainer";
835
+ import TableHead from "@mui/material/TableHead";
836
+ import TableRow from "@mui/material/TableRow";
837
+ import Button2 from "@mui/material/Button";
838
+ import IconButton from "@mui/material/IconButton";
839
+ import Box3 from "@mui/material/Box";
840
+ import Typography3 from "@mui/material/Typography";
841
+ import Dialog from "@mui/material/Dialog";
842
+ import DialogTitle from "@mui/material/DialogTitle";
843
+ import DialogContent from "@mui/material/DialogContent";
844
+ import DialogActions from "@mui/material/DialogActions";
845
+ import TextField from "@mui/material/TextField";
846
+ import CircularProgress from "@mui/material/CircularProgress";
847
+ import Autocomplete from "@mui/material/Autocomplete";
848
+ import Skeleton2 from "@mui/material/Skeleton";
849
+ import InputAdornment from "@mui/material/InputAdornment";
850
+ import { alpha as alpha3 } from "@mui/material/styles";
851
+ import { ContentCopy, Delete, Add as Add2, Edit, Autorenew, Search, Lock, LockOpen } from "@mui/icons-material";
852
+ function expiryChipLabel(status, expiresAt) {
853
+ if (status === "expired") return "Expired";
854
+ if (status === "soon") return `${Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 864e5)}d left`;
855
+ return formatDate(expiresAt);
856
+ }
857
+ function expiryTone(status) {
858
+ if (status === "expired") return "danger";
859
+ if (status === "soon") return "warning";
860
+ return "neutral";
861
+ }
862
+ function ExpiryCell({ expiresAt }) {
246
863
  const status = expiryStatus(expiresAt);
247
- if (!status) return /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, "-");
248
- const label = status === "expired" ? "Expired" : status === "soon" ? `${Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 864e5)}d left` : formatDate(expiresAt);
249
- const color = status === "expired" ? "error" : status === "soon" ? "warning" : "default";
250
- const icon = status === "expired" || status === "soon" ? /* @__PURE__ */ React2.createElement(Warning2, { fontSize: "small" }) : void 0;
251
- return /* @__PURE__ */ React2.createElement(Chip2, { label, color, size: "small", icon });
864
+ if (!status) {
865
+ return /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary" }, "Never");
866
+ }
867
+ if (status === "ok") {
868
+ return /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary" }, formatDate(expiresAt));
869
+ }
870
+ return /* @__PURE__ */ React3.createElement(
871
+ StatusPill,
872
+ {
873
+ label: expiryChipLabel(status, expiresAt),
874
+ tone: expiryTone(status),
875
+ title: `Expires ${formatDate(expiresAt)}`
876
+ }
877
+ );
878
+ }
879
+ function budgetTone(pct) {
880
+ if (pct >= 100) return "danger";
881
+ if (pct >= 80) return "warning";
882
+ return "accent";
883
+ }
884
+ function keyBlockIcon(isBlocking, blocked) {
885
+ if (isBlocking) return /* @__PURE__ */ React3.createElement(CircularProgress, { size: 18 });
886
+ if (blocked) return /* @__PURE__ */ React3.createElement(LockOpen, { fontSize: "small" });
887
+ return /* @__PURE__ */ React3.createElement(Lock, { fontSize: "small" });
252
888
  }
253
889
  function BudgetCell({ spend, maxBudget }) {
254
- if (!maxBudget) return /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, "-");
890
+ if (!maxBudget) {
891
+ return /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, "$", spend.toFixed(2), /* @__PURE__ */ React3.createElement(Typography3, { component: "span", variant: "caption", sx: { ml: 0.5, opacity: 0.7 } }, "/ \u221E"));
892
+ }
255
893
  const pct = Math.min(100, spend / maxBudget * 100);
256
- const color = pct >= 100 ? "error" : pct >= 80 ? "warning" : "primary";
257
- return /* @__PURE__ */ React2.createElement(Box2, { minWidth: 100 }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", justifyContent: "space-between" }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption" }, "$", spend.toFixed(2)), /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary" }, "$", maxBudget)), /* @__PURE__ */ React2.createElement(LinearProgress2, { variant: "determinate", value: pct, color, sx: { height: 5, borderRadius: 1, mt: 0.25 } }));
894
+ return /* @__PURE__ */ React3.createElement(Box3, { minWidth: 104 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums", mb: 0.5 } }, "$", spend.toFixed(2), /* @__PURE__ */ React3.createElement(Typography3, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 0.5 } }, "/ $", maxBudget)), /* @__PURE__ */ React3.createElement(Meter, { value: pct, tone: budgetTone(pct), height: 4 }));
258
895
  }
259
896
  function fmtCost(perToken) {
260
897
  if (!perToken) return null;
@@ -281,6 +918,7 @@ var init_KeysTable = __esm({
281
918
  "src/components/KeysTable.tsx"() {
282
919
  "use strict";
283
920
  init_api();
921
+ init_ui();
284
922
  maskKey = (key) => {
285
923
  if (key.length <= 8) return "***";
286
924
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
@@ -412,88 +1050,179 @@ var init_KeysTable = __esm({
412
1050
  const inCost = fmtCost(m.input_cost_per_token);
413
1051
  const outCost = fmtCost(m.output_cost_per_token);
414
1052
  const ctx = formatContextWindow(m.max_input_tokens, m.max_output_tokens);
415
- return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
1053
+ return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
416
1054
  };
417
- return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Paper2, { sx: { mb: 2 } }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2, gap: 2 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 1, alignItems: "center", flex: 1, justifyContent: "flex-end" }, /* @__PURE__ */ React2.createElement(
418
- TextField,
419
- {
420
- size: "small",
421
- placeholder: "Filter by alias or model\u2026",
422
- value: filterText,
423
- onChange: (e) => setFilterText(e.target.value),
424
- sx: { minWidth: 240 },
425
- InputProps: {
426
- startAdornment: /* @__PURE__ */ React2.createElement(InputAdornment, { position: "start" }, /* @__PURE__ */ React2.createElement(Search, { fontSize: "small" }))
427
- }
1055
+ const renderTableBody = () => {
1056
+ if (loading) {
1057
+ return /* @__PURE__ */ React3.createElement(TableRow, null, /* @__PURE__ */ React3.createElement(TableCell, { colSpan: 8, sx: { py: 2 } }, /* @__PURE__ */ React3.createElement(Skeleton2, { variant: "rounded", height: 140 })));
428
1058
  }
429
- ), expiredKeys.length > 0 && /* @__PURE__ */ React2.createElement(
430
- Button2,
431
- {
432
- variant: "outlined",
433
- color: "error",
434
- startIcon: /* @__PURE__ */ React2.createElement(Autorenew, null),
435
- onClick: () => setPruneConfirmCount(expiredKeys.length)
436
- },
437
- "Prune expired (",
438
- expiredKeys.length,
439
- ")"
440
- ), /* @__PURE__ */ React2.createElement(
441
- Button2,
442
- {
443
- variant: "contained",
444
- color: "primary",
445
- startIcon: /* @__PURE__ */ React2.createElement(Add2, null),
446
- onClick: onGenerateKeyClick
447
- },
448
- "Generate New Key"
449
- ))), /* @__PURE__ */ React2.createElement(TableContainer, null, /* @__PURE__ */ React2.createElement(Table, null, /* @__PURE__ */ React2.createElement(TableHead, null, /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React2.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React2.createElement(TableCell, null, "Created"), /* @__PURE__ */ React2.createElement(TableCell, null, "Expires"), /* @__PURE__ */ React2.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React2.createElement(TableCell, null, "TPM / RPM"), /* @__PURE__ */ React2.createElement(TableCell, null, "Models"), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React2.createElement(TableBody, null, loading ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center", sx: { py: 4 } }, filterText ? /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary" }, "No keys match filter") : /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary", gutterBottom: true }, "No keys yet \u2014 generate your first key to start calling models."), /* @__PURE__ */ React2.createElement(
450
- Button2,
1059
+ if (filteredKeys.length === 0) {
1060
+ return /* @__PURE__ */ React3.createElement(TableRow, null, /* @__PURE__ */ React3.createElement(TableCell, { colSpan: 8 }, filterText ? /* @__PURE__ */ React3.createElement(
1061
+ EmptyState,
1062
+ {
1063
+ message: "No keys match that filter",
1064
+ hint: "Try a different alias or model name."
1065
+ }
1066
+ ) : /* @__PURE__ */ React3.createElement(
1067
+ EmptyState,
1068
+ {
1069
+ message: "No keys yet",
1070
+ hint: "Generate your first key to start calling models.",
1071
+ action: /* @__PURE__ */ React3.createElement(
1072
+ Button2,
1073
+ {
1074
+ variant: "contained",
1075
+ color: "primary",
1076
+ size: "small",
1077
+ disableElevation: true,
1078
+ startIcon: /* @__PURE__ */ React3.createElement(Add2, null),
1079
+ onClick: onGenerateKeyClick,
1080
+ sx: { mt: 1 }
1081
+ },
1082
+ "Generate Your First Key"
1083
+ )
1084
+ }
1085
+ )));
1086
+ }
1087
+ return filteredKeys.map((key) => {
1088
+ const keyId = key.token ?? key.key;
1089
+ const isBlocking = blockingKeyId === keyId;
1090
+ const keyModels = key.models ?? [];
1091
+ return /* @__PURE__ */ React3.createElement(
1092
+ TableRow,
1093
+ {
1094
+ key: keyId,
1095
+ sx: key.blocked ? (theme) => ({
1096
+ // A left rule reads as "suspended" without greying the row into
1097
+ // illegibility the way a full background tint does.
1098
+ boxShadow: `inset 3px 0 0 0 ${theme.palette.error.main}`,
1099
+ "& td": { color: theme.palette.text.secondary }
1100
+ }) : void 0
1101
+ },
1102
+ /* @__PURE__ */ React3.createElement(TableCell, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", sx: { fontWeight: 600 } }, key.key_alias || "\u2014"), key.blocked && /* @__PURE__ */ React3.createElement(StatusPill, { label: "Blocked", tone: "danger" }))),
1103
+ /* @__PURE__ */ React3.createElement(TableCell, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ React3.createElement(
1104
+ Typography3,
1105
+ {
1106
+ variant: "body2",
1107
+ component: "code",
1108
+ color: "text.secondary",
1109
+ title: keyId,
1110
+ sx: (theme) => ({
1111
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
1112
+ fontSize: 12,
1113
+ bgcolor: alpha3(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.08 : 0.05),
1114
+ px: 0.75,
1115
+ py: 0.375,
1116
+ borderRadius: "5px"
1117
+ })
1118
+ },
1119
+ shortKeyId(keyId)
1120
+ ), /* @__PURE__ */ React3.createElement(
1121
+ IconButton,
1122
+ {
1123
+ size: "small",
1124
+ onClick: () => copyToClipboard(keyId),
1125
+ title: "Copy Key ID",
1126
+ sx: quietIconButtonSx("accent")
1127
+ },
1128
+ /* @__PURE__ */ React3.createElement(ContentCopy, { sx: { fontSize: 15 } })
1129
+ ))),
1130
+ /* @__PURE__ */ React3.createElement(TableCell, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary" }, formatDate(key.created_at))),
1131
+ /* @__PURE__ */ React3.createElement(TableCell, null, /* @__PURE__ */ React3.createElement(ExpiryCell, { expiresAt: key.expires_at })),
1132
+ /* @__PURE__ */ React3.createElement(TableCell, null, /* @__PURE__ */ React3.createElement(BudgetCell, { spend: key.spend ?? 0, maxBudget: key.max_budget })),
1133
+ /* @__PURE__ */ React3.createElement(TableCell, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, key.tpm_limit ?? "\u221E", " / ", key.rpm_limit ?? "\u221E")),
1134
+ /* @__PURE__ */ React3.createElement(TableCell, null, keyModels.length === 0 ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary" }, "All models") : /* @__PURE__ */ React3.createElement(Box3, { display: "flex", gap: 0.5, flexWrap: "wrap" }, keyModels.slice(0, 2).map((model) => /* @__PURE__ */ React3.createElement(TagChip, { key: model, label: model, title: model })), keyModels.length > 2 && /* @__PURE__ */ React3.createElement(
1135
+ TagChip,
1136
+ {
1137
+ label: `+${keyModels.length - 2}`,
1138
+ mono: false,
1139
+ title: keyModels.slice(2).join(", ")
1140
+ }
1141
+ ))),
1142
+ /* @__PURE__ */ React3.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", justifyContent: "flex-end", gap: 0.25 }, /* @__PURE__ */ React3.createElement(
1143
+ IconButton,
1144
+ {
1145
+ size: "small",
1146
+ onClick: () => handleOpenEdit(key),
1147
+ title: "Edit key",
1148
+ sx: quietIconButtonSx("accent")
1149
+ },
1150
+ /* @__PURE__ */ React3.createElement(Edit, { fontSize: "small" })
1151
+ ), /* @__PURE__ */ React3.createElement(
1152
+ IconButton,
1153
+ {
1154
+ size: "small",
1155
+ onClick: () => handleToggleBlock(key),
1156
+ disabled: isBlocking,
1157
+ title: key.blocked ? "Unblock key" : "Block key \u2014 suspends without revoking",
1158
+ sx: quietIconButtonSx("warning")
1159
+ },
1160
+ keyBlockIcon(isBlocking, key.blocked)
1161
+ ), /* @__PURE__ */ React3.createElement(
1162
+ IconButton,
1163
+ {
1164
+ size: "small",
1165
+ onClick: () => setDeleteConfirmId(keyId),
1166
+ title: "Revoke key",
1167
+ sx: quietIconButtonSx("danger")
1168
+ },
1169
+ /* @__PURE__ */ React3.createElement(Delete, { fontSize: "small" })
1170
+ )))
1171
+ );
1172
+ });
1173
+ };
1174
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(
1175
+ SectionCard,
451
1176
  {
452
- variant: "contained",
453
- color: "primary",
454
- size: "small",
455
- startIcon: /* @__PURE__ */ React2.createElement(Add2, null),
456
- onClick: onGenerateKeyClick
457
- },
458
- "Generate Your First Key"
459
- )))) : filteredKeys.map((key) => {
460
- const keyId = key.token ?? key.key;
461
- const isBlocking = blockingKeyId === keyId;
462
- return /* @__PURE__ */ React2.createElement(TableRow, { key: keyId, sx: key.blocked ? { bgcolor: "action.disabledBackground" } : void 0 }, /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, key.key_alias || "-", key.blocked && /* @__PURE__ */ React2.createElement(Chip2, { label: "Blocked", color: "error", size: "small" }))), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ React2.createElement(
463
- Typography2,
464
- {
465
- variant: "body2",
466
- component: "code",
467
- color: "text.secondary",
468
- title: keyId,
469
- sx: {
470
- fontFamily: "monospace",
471
- backgroundColor: "background.default",
472
- px: 1,
473
- py: 0.5,
474
- borderRadius: 1
1177
+ title: "Virtual Keys",
1178
+ subtitle: loading ? "Loading\u2026" : `${filteredKeys.length}${filterText ? ` of ${keys.length}` : ""} key${filteredKeys.length === 1 ? "" : "s"}`,
1179
+ flush: true,
1180
+ actions: /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(
1181
+ TextField,
1182
+ {
1183
+ size: "small",
1184
+ placeholder: "Filter by alias or model\u2026",
1185
+ value: filterText,
1186
+ onChange: (e) => setFilterText(e.target.value),
1187
+ sx: { minWidth: 240 },
1188
+ InputProps: {
1189
+ startAdornment: /* @__PURE__ */ React3.createElement(InputAdornment, { position: "start" }, /* @__PURE__ */ React3.createElement(Search, { fontSize: "small", color: "disabled" }))
1190
+ }
475
1191
  }
476
- },
477
- shortKeyId(keyId)
478
- ), /* @__PURE__ */ React2.createElement(IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ React2.createElement(TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(ExpiryChip, { expiresAt: key.expires_at })), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(BudgetCell, { spend: key.spend ?? 0, maxBudget: key.max_budget })), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2" }, key.tpm_limit ?? "-", " / ", key.rpm_limit ?? "-")), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ React2.createElement(Chip2, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ React2.createElement(Chip2, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => handleOpenEdit(key), title: "Edit key" }, /* @__PURE__ */ React2.createElement(Edit, { fontSize: "small" })), /* @__PURE__ */ React2.createElement(
479
- IconButton,
480
- {
481
- onClick: () => handleToggleBlock(key),
482
- disabled: isBlocking,
483
- color: key.blocked ? "warning" : "default",
484
- title: key.blocked ? "Unblock key" : "Block key \u2014 suspends without revoking"
485
- },
486
- isBlocking ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 18 }) : key.blocked ? /* @__PURE__ */ React2.createElement(LockOpen, { fontSize: "small" }) : /* @__PURE__ */ React2.createElement(Lock, { fontSize: "small" })
487
- ), /* @__PURE__ */ React2.createElement(
488
- IconButton,
489
- {
490
- color: "error",
491
- onClick: () => setDeleteConfirmId(keyId),
492
- title: "Revoke key"
493
- },
494
- /* @__PURE__ */ React2.createElement(Delete, null)
495
- )));
496
- }))))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Edit Key"), /* @__PURE__ */ React2.createElement(DialogContent, null, editingKey && /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ React2.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ React2.createElement(
1192
+ ), expiredKeys.length > 0 && /* @__PURE__ */ React3.createElement(
1193
+ Button2,
1194
+ {
1195
+ variant: "outlined",
1196
+ color: "inherit",
1197
+ startIcon: /* @__PURE__ */ React3.createElement(Autorenew, null),
1198
+ onClick: () => setPruneConfirmCount(expiredKeys.length),
1199
+ sx: (theme) => ({
1200
+ color: theme.palette.text.secondary,
1201
+ borderColor: theme.palette.divider,
1202
+ "&:hover": {
1203
+ color: theme.palette.error.main,
1204
+ borderColor: alpha3(theme.palette.error.main, 0.5),
1205
+ bgcolor: alpha3(theme.palette.error.main, 0.06)
1206
+ }
1207
+ })
1208
+ },
1209
+ "Prune expired (",
1210
+ expiredKeys.length,
1211
+ ")"
1212
+ ), /* @__PURE__ */ React3.createElement(
1213
+ Button2,
1214
+ {
1215
+ variant: "contained",
1216
+ color: "primary",
1217
+ disableElevation: true,
1218
+ startIcon: /* @__PURE__ */ React3.createElement(Add2, null),
1219
+ onClick: onGenerateKeyClick
1220
+ },
1221
+ "Generate New Key"
1222
+ ))
1223
+ },
1224
+ /* @__PURE__ */ React3.createElement(TableContainer, null, /* @__PURE__ */ React3.createElement(Table, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React3.createElement(TableHead, null, /* @__PURE__ */ React3.createElement(TableRow, null, /* @__PURE__ */ React3.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React3.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React3.createElement(TableCell, null, "Created"), /* @__PURE__ */ React3.createElement(TableCell, null, "Expires"), /* @__PURE__ */ React3.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React3.createElement(TableCell, null, "TPM / RPM"), /* @__PURE__ */ React3.createElement(TableCell, null, "Models"), /* @__PURE__ */ React3.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React3.createElement(TableBody, null, renderTableBody())))
1225
+ ), /* @__PURE__ */ React3.createElement(Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React3.createElement(DialogTitle, null, "Edit Key"), /* @__PURE__ */ React3.createElement(DialogContent, null, editingKey && /* @__PURE__ */ React3.createElement(Box3, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ React3.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ React3.createElement(
497
1226
  TextField,
498
1227
  {
499
1228
  label: "Alias",
@@ -501,7 +1230,7 @@ var init_KeysTable = __esm({
501
1230
  onChange: (e) => setEditForm({ ...editForm, key_alias: e.target.value }),
502
1231
  fullWidth: true
503
1232
  }
504
- ), models.length > 0 && /* @__PURE__ */ React2.createElement(
1233
+ ), models.length > 0 && /* @__PURE__ */ React3.createElement(
505
1234
  Autocomplete,
506
1235
  {
507
1236
  multiple: true,
@@ -510,10 +1239,10 @@ var init_KeysTable = __esm({
510
1239
  getOptionLabel: (m) => m.model_name,
511
1240
  value: editSelectedModels,
512
1241
  onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
513
- renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
514
- renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", fullWidth: true })
1242
+ renderOption: (props, m) => /* @__PURE__ */ React3.createElement("li", { ...props }, modelOption(m)),
1243
+ renderInput: (params) => /* @__PURE__ */ React3.createElement(TextField, { ...params, label: "Models", fullWidth: true })
515
1244
  }
516
- ), /* @__PURE__ */ React2.createElement(
1245
+ ), /* @__PURE__ */ React3.createElement(
517
1246
  TextField,
518
1247
  {
519
1248
  label: "Max Budget (USD)",
@@ -522,7 +1251,7 @@ var init_KeysTable = __esm({
522
1251
  onChange: (e) => setEditForm({ ...editForm, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
523
1252
  fullWidth: true
524
1253
  }
525
- ), /* @__PURE__ */ React2.createElement(
1254
+ ), /* @__PURE__ */ React3.createElement(
526
1255
  TextField,
527
1256
  {
528
1257
  label: "TPM Limit",
@@ -532,7 +1261,7 @@ var init_KeysTable = __esm({
532
1261
  helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
533
1262
  fullWidth: true
534
1263
  }
535
- ), /* @__PURE__ */ React2.createElement(
1264
+ ), /* @__PURE__ */ React3.createElement(
536
1265
  TextField,
537
1266
  {
538
1267
  label: "RPM Limit",
@@ -542,7 +1271,7 @@ var init_KeysTable = __esm({
542
1271
  helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
543
1272
  fullWidth: true
544
1273
  }
545
- ), /* @__PURE__ */ React2.createElement(Box2, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ React2.createElement(
1274
+ ), /* @__PURE__ */ React3.createElement(Box3, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ React3.createElement(
546
1275
  Button2,
547
1276
  {
548
1277
  size: "small",
@@ -551,7 +1280,7 @@ var init_KeysTable = __esm({
551
1280
  onClick: () => setResetSpendConfirm(true)
552
1281
  },
553
1282
  "Reset Spend to $0"
554
- ) : /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ React2.createElement(
1283
+ ) : /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ React3.createElement(
555
1284
  Button2,
556
1285
  {
557
1286
  size: "small",
@@ -560,8 +1289,8 @@ var init_KeysTable = __esm({
560
1289
  disabled: resetSpendSubmitting,
561
1290
  onClick: handleResetSpend
562
1291
  },
563
- resetSpendSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 16 }) : "Confirm"
564
- ), /* @__PURE__ */ React2.createElement(Button2, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button2, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ React2.createElement(Button2, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Save"))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Revoke Key?"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button2, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ React2.createElement(
1292
+ resetSpendSubmitting ? /* @__PURE__ */ React3.createElement(CircularProgress, { size: 16 }) : "Confirm"
1293
+ ), /* @__PURE__ */ React3.createElement(Button2, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ React3.createElement(DialogActions, null, /* @__PURE__ */ React3.createElement(Button2, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ React3.createElement(Button2, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ React3.createElement(CircularProgress, { size: 24 }) : "Save"))), /* @__PURE__ */ React3.createElement(Dialog, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React3.createElement(DialogTitle, null, "Revoke Key?"), /* @__PURE__ */ React3.createElement(DialogContent, null, /* @__PURE__ */ React3.createElement(Typography3, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ React3.createElement(DialogActions, null, /* @__PURE__ */ React3.createElement(Button2, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ React3.createElement(
565
1294
  Button2,
566
1295
  {
567
1296
  onClick: handleConfirmDelete,
@@ -569,8 +1298,8 @@ var init_KeysTable = __esm({
569
1298
  color: "error",
570
1299
  disabled: deleteSubmitting
571
1300
  },
572
- deleteSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Revoke"
573
- ))), /* @__PURE__ */ React2.createElement(Dialog, { open: pruneConfirmCount !== null, onClose: () => setPruneConfirmCount(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Prune Expired Keys?"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, null, "This will permanently delete ", pruneConfirmCount, " expired key", pruneConfirmCount !== 1 ? "s" : "", " from LiteLLM. Any integrations using them will stop working immediately.")), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button2, { onClick: () => setPruneConfirmCount(null), disabled: pruneSubmitting }, "Cancel"), /* @__PURE__ */ React2.createElement(
1301
+ deleteSubmitting ? /* @__PURE__ */ React3.createElement(CircularProgress, { size: 20 }) : "Revoke"
1302
+ ))), /* @__PURE__ */ React3.createElement(Dialog, { open: pruneConfirmCount !== null, onClose: () => setPruneConfirmCount(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React3.createElement(DialogTitle, null, "Prune Expired Keys?"), /* @__PURE__ */ React3.createElement(DialogContent, null, /* @__PURE__ */ React3.createElement(Typography3, null, "This will permanently delete ", pruneConfirmCount, " expired key", pruneConfirmCount !== 1 ? "s" : "", " from LiteLLM. Any integrations using them will stop working immediately.")), /* @__PURE__ */ React3.createElement(DialogActions, null, /* @__PURE__ */ React3.createElement(Button2, { onClick: () => setPruneConfirmCount(null), disabled: pruneSubmitting }, "Cancel"), /* @__PURE__ */ React3.createElement(
574
1303
  Button2,
575
1304
  {
576
1305
  onClick: handlePruneExpired,
@@ -578,7 +1307,7 @@ var init_KeysTable = __esm({
578
1307
  color: "error",
579
1308
  disabled: pruneSubmitting
580
1309
  },
581
- pruneSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Prune"
1310
+ pruneSubmitting ? /* @__PURE__ */ React3.createElement(CircularProgress, { size: 20 }) : "Prune"
582
1311
  ))));
583
1312
  };
584
1313
  }
@@ -599,26 +1328,24 @@ var init_format = __esm({
599
1328
  });
600
1329
 
601
1330
  // src/components/GenerateKeyDialog.tsx
602
- import React3, { useState as useState2, useEffect, useMemo as useMemo3 } from "react";
603
- import {
604
- Box as Box3,
605
- Typography as Typography3,
606
- Dialog as Dialog2,
607
- DialogTitle as DialogTitle2,
608
- DialogContent as DialogContent2,
609
- DialogActions as DialogActions2,
610
- TextField as TextField2,
611
- MenuItem,
612
- Button as Button3,
613
- IconButton as IconButton2,
614
- CircularProgress as CircularProgress2,
615
- Autocomplete as Autocomplete2,
616
- Checkbox,
617
- FormControlLabel,
618
- Alert,
619
- Tabs,
620
- Tab
621
- } from "@mui/material";
1331
+ import React4, { useState as useState2, useEffect, useMemo as useMemo3 } from "react";
1332
+ import Box4 from "@mui/material/Box";
1333
+ import Typography4 from "@mui/material/Typography";
1334
+ import Dialog2 from "@mui/material/Dialog";
1335
+ import DialogTitle2 from "@mui/material/DialogTitle";
1336
+ import DialogContent2 from "@mui/material/DialogContent";
1337
+ import DialogActions2 from "@mui/material/DialogActions";
1338
+ import TextField2 from "@mui/material/TextField";
1339
+ import MenuItem from "@mui/material/MenuItem";
1340
+ import Button3 from "@mui/material/Button";
1341
+ import IconButton2 from "@mui/material/IconButton";
1342
+ import CircularProgress2 from "@mui/material/CircularProgress";
1343
+ import Autocomplete2 from "@mui/material/Autocomplete";
1344
+ import Checkbox from "@mui/material/Checkbox";
1345
+ import FormControlLabel from "@mui/material/FormControlLabel";
1346
+ import Alert from "@mui/material/Alert";
1347
+ import Tabs from "@mui/material/Tabs";
1348
+ import Tab from "@mui/material/Tab";
622
1349
  import { ContentCopy as ContentCopy2, Code } from "@mui/icons-material";
623
1350
  function trimSlash(url) {
624
1351
  return url.replace(/\/+$/, "");
@@ -647,6 +1374,21 @@ response = client.chat.completions.create(
647
1374
  print(response.choices[0].message.content)`
648
1375
  };
649
1376
  }
1377
+ function aliasHelperText(aliasError, aliasDuplicate) {
1378
+ if (aliasError) return "Alias is required";
1379
+ if (aliasDuplicate) return "This alias is already used by one of your keys \u2014 LiteLLM requires aliases to be unique across all keys";
1380
+ return void 0;
1381
+ }
1382
+ function teamHelperText(teamError, teamRequired) {
1383
+ if (teamError) return "Team is required";
1384
+ if (teamRequired) return "Bind this key to a team for scoped access";
1385
+ return "Optional: bind this key to a specific team for scoped access";
1386
+ }
1387
+ function budgetHelperText(budgetInvalid, budgetEstimate) {
1388
+ if (budgetInvalid) return 'Enter a positive budget or tick "Unlimited"';
1389
+ if (budgetEstimate !== null) return `\u2248 ${fmtInt(budgetEstimate)} tokens at the selected model's rate`;
1390
+ return void 0;
1391
+ }
650
1392
  function fmtCost2(perToken) {
651
1393
  if (!perToken) return null;
652
1394
  const per1k = perToken * 1e3;
@@ -667,7 +1409,7 @@ function formatContextWindow2(maxInput, maxOutput) {
667
1409
  if (outPart) return `ctx ${outPart} out`;
668
1410
  return null;
669
1411
  }
670
- var generateDefaultAlias, emptyForm, GenerateKeyDialog, SnippetTabs;
1412
+ var generateDefaultAlias, emptyForm, SnippetTabs, GenerateKeyDialog;
671
1413
  var init_GenerateKeyDialog = __esm({
672
1414
  "src/components/GenerateKeyDialog.tsx"() {
673
1415
  "use strict";
@@ -687,6 +1429,44 @@ var init_GenerateKeyDialog = __esm({
687
1429
  team_id: void 0,
688
1430
  key_type: "llm_api"
689
1431
  });
1432
+ SnippetTabs = ({ snippets, model, onCopy }) => {
1433
+ const [tab, setTab] = useState2("curl");
1434
+ const code = tab === "curl" ? snippets.curl : snippets.openai;
1435
+ return /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ React4.createElement(Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ React4.createElement(Tab, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ React4.createElement(
1436
+ Box4,
1437
+ {
1438
+ position: "relative",
1439
+ p: 1.5,
1440
+ sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
1441
+ },
1442
+ /* @__PURE__ */ React4.createElement(
1443
+ IconButton2,
1444
+ {
1445
+ size: "small",
1446
+ onClick: () => onCopy(code),
1447
+ title: "Copy snippet",
1448
+ sx: { position: "absolute", top: 4, right: 4 }
1449
+ },
1450
+ /* @__PURE__ */ React4.createElement(ContentCopy2, { fontSize: "small" })
1451
+ ),
1452
+ /* @__PURE__ */ React4.createElement(
1453
+ Typography4,
1454
+ {
1455
+ component: "pre",
1456
+ sx: {
1457
+ fontFamily: "monospace",
1458
+ fontSize: 12,
1459
+ whiteSpace: "pre-wrap",
1460
+ wordBreak: "break-all",
1461
+ mb: 0,
1462
+ pr: 4
1463
+ }
1464
+ },
1465
+ code
1466
+ ),
1467
+ model && /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", display: "block", mt: 1 }, "Using model \u201C", model, "\u201D \u2014 swap it for any model you have access to.")
1468
+ ));
1469
+ };
690
1470
  GenerateKeyDialog = ({
691
1471
  open,
692
1472
  onClose,
@@ -780,10 +1560,42 @@ var init_GenerateKeyDialog = __esm({
780
1560
  const inCost = fmtCost2(m.input_cost_per_token);
781
1561
  const outCost = fmtCost2(m.output_cost_per_token);
782
1562
  const ctx = formatContextWindow2(m.max_input_tokens, m.max_output_tokens);
783
- return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
1563
+ return /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
1564
+ };
1565
+ const renderTeamField = () => {
1566
+ if (teams.length > 0) {
1567
+ return /* @__PURE__ */ React4.createElement(
1568
+ Autocomplete2,
1569
+ {
1570
+ options: teams,
1571
+ getOptionLabel: (t) => t.team_alias || t.team_id,
1572
+ value: selectedTeam,
1573
+ onChange: (_e, team) => {
1574
+ const teamModels = team?.models;
1575
+ const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
1576
+ setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
1577
+ },
1578
+ renderInput: (params) => /* @__PURE__ */ React4.createElement(
1579
+ TextField2,
1580
+ {
1581
+ ...params,
1582
+ label: "Team",
1583
+ error: teamError,
1584
+ helperText: teamHelperText(teamError, teamRequired),
1585
+ required: teamRequired,
1586
+ fullWidth: true
1587
+ }
1588
+ )
1589
+ }
1590
+ );
1591
+ }
1592
+ if (teamRequired) {
1593
+ return /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", color: "error" }, "Team selection is required, but you don't belong to any team yet \u2014 contact your administrator.");
1594
+ }
1595
+ return null;
784
1596
  };
785
- return /* @__PURE__ */ React3.createElement(Dialog2, { open, onClose: handleClose, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React3.createElement(DialogTitle2, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ React3.createElement(DialogContent2, null, newKeyValue ? /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ React3.createElement(
786
- Box3,
1597
+ return /* @__PURE__ */ React4.createElement(Dialog2, { open, onClose: handleClose, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React4.createElement(DialogTitle2, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ React4.createElement(DialogContent2, null, newKeyValue ? /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ React4.createElement(
1598
+ Box4,
787
1599
  {
788
1600
  display: "flex",
789
1601
  alignItems: "center",
@@ -797,8 +1609,8 @@ var init_GenerateKeyDialog = __esm({
797
1609
  borderRadius: 1
798
1610
  }
799
1611
  },
800
- /* @__PURE__ */ React3.createElement(
801
- Typography3,
1612
+ /* @__PURE__ */ React4.createElement(
1613
+ Typography4,
802
1614
  {
803
1615
  component: "code",
804
1616
  color: "text.primary",
@@ -806,8 +1618,8 @@ var init_GenerateKeyDialog = __esm({
806
1618
  },
807
1619
  newKeyValue
808
1620
  ),
809
- /* @__PURE__ */ React3.createElement(IconButton2, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React3.createElement(ContentCopy2, null))
810
- ), newKeySnippets && /* @__PURE__ */ React3.createElement(Box3, { mt: 3 }, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React3.createElement(Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ React3.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ React3.createElement(Box3, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ React3.createElement(Alert, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ React3.createElement(
1621
+ /* @__PURE__ */ React4.createElement(IconButton2, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React4.createElement(ContentCopy2, null))
1622
+ ), newKeySnippets && /* @__PURE__ */ React4.createElement(Box4, { mt: 3 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ React4.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ React4.createElement(Box4, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ React4.createElement(Alert, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ React4.createElement(
811
1623
  TextField2,
812
1624
  {
813
1625
  label: "Alias",
@@ -818,11 +1630,11 @@ var init_GenerateKeyDialog = __esm({
818
1630
  },
819
1631
  error: aliasError,
820
1632
  color: aliasDuplicate ? "warning" : void 0,
821
- helperText: aliasError ? "Alias is required" : aliasDuplicate ? "This alias is already used by one of your keys \u2014 LiteLLM requires aliases to be unique across all keys" : void 0,
1633
+ helperText: aliasHelperText(aliasError, aliasDuplicate),
822
1634
  required: true,
823
1635
  fullWidth: true
824
1636
  }
825
- ), /* @__PURE__ */ React3.createElement(
1637
+ ), /* @__PURE__ */ React4.createElement(
826
1638
  TextField2,
827
1639
  {
828
1640
  select: true,
@@ -831,35 +1643,12 @@ var init_GenerateKeyDialog = __esm({
831
1643
  onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
832
1644
  fullWidth: true
833
1645
  },
834
- /* @__PURE__ */ React3.createElement(MenuItem, { value: "1d" }, "1 Day"),
835
- /* @__PURE__ */ React3.createElement(MenuItem, { value: "7d" }, "7 Days"),
836
- /* @__PURE__ */ React3.createElement(MenuItem, { value: "30d" }, "30 Days"),
837
- /* @__PURE__ */ React3.createElement(MenuItem, { value: "90d" }, "90 Days"),
838
- /* @__PURE__ */ React3.createElement(MenuItem, { value: "1y" }, "1 Year")
839
- ), teams.length > 0 ? /* @__PURE__ */ React3.createElement(
840
- Autocomplete2,
841
- {
842
- options: teams,
843
- getOptionLabel: (t) => t.team_alias || t.team_id,
844
- value: selectedTeam,
845
- onChange: (_e, team) => {
846
- const teamModels = team?.models;
847
- const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
848
- setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
849
- },
850
- renderInput: (params) => /* @__PURE__ */ React3.createElement(
851
- TextField2,
852
- {
853
- ...params,
854
- label: "Team",
855
- error: teamError,
856
- helperText: teamError ? "Team is required" : teamRequired ? "Bind this key to a team for scoped access" : "Optional: bind this key to a specific team for scoped access",
857
- required: teamRequired,
858
- fullWidth: true
859
- }
860
- )
861
- }
862
- ) : teamRequired ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "error" }, "Team selection is required, but you don't belong to any team yet \u2014 contact your administrator.") : null, availableModels.length > 0 && /* @__PURE__ */ React3.createElement(
1646
+ /* @__PURE__ */ React4.createElement(MenuItem, { value: "1d" }, "1 Day"),
1647
+ /* @__PURE__ */ React4.createElement(MenuItem, { value: "7d" }, "7 Days"),
1648
+ /* @__PURE__ */ React4.createElement(MenuItem, { value: "30d" }, "30 Days"),
1649
+ /* @__PURE__ */ React4.createElement(MenuItem, { value: "90d" }, "90 Days"),
1650
+ /* @__PURE__ */ React4.createElement(MenuItem, { value: "1y" }, "1 Year")
1651
+ ), renderTeamField(), availableModels.length > 0 && /* @__PURE__ */ React4.createElement(
863
1652
  Autocomplete2,
864
1653
  {
865
1654
  multiple: true,
@@ -868,8 +1657,8 @@ var init_GenerateKeyDialog = __esm({
868
1657
  getOptionLabel: (m) => m.model_name,
869
1658
  value: selectedModels,
870
1659
  onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
871
- renderOption: (props, m) => /* @__PURE__ */ React3.createElement("li", { ...props }, modelOption(m)),
872
- renderInput: (params) => /* @__PURE__ */ React3.createElement(
1660
+ renderOption: (props, m) => /* @__PURE__ */ React4.createElement("li", { ...props }, modelOption(m)),
1661
+ renderInput: (params) => /* @__PURE__ */ React4.createElement(
873
1662
  TextField2,
874
1663
  {
875
1664
  ...params,
@@ -879,10 +1668,10 @@ var init_GenerateKeyDialog = __esm({
879
1668
  }
880
1669
  )
881
1670
  }
882
- ), allowUnlimitedBudget && /* @__PURE__ */ React3.createElement(
1671
+ ), allowUnlimitedBudget && /* @__PURE__ */ React4.createElement(
883
1672
  FormControlLabel,
884
1673
  {
885
- control: /* @__PURE__ */ React3.createElement(
1674
+ control: /* @__PURE__ */ React4.createElement(
886
1675
  Checkbox,
887
1676
  {
888
1677
  checked: unlimitedBudget,
@@ -898,7 +1687,7 @@ var init_GenerateKeyDialog = __esm({
898
1687
  ),
899
1688
  label: "Unlimited budget"
900
1689
  }
901
- ), /* @__PURE__ */ React3.createElement(
1690
+ ), /* @__PURE__ */ React4.createElement(
902
1691
  TextField2,
903
1692
  {
904
1693
  label: "Max Budget (USD)",
@@ -906,12 +1695,12 @@ var init_GenerateKeyDialog = __esm({
906
1695
  value: unlimitedBudget ? "" : formData.max_budget ?? "",
907
1696
  onChange: (e) => setFormData({ ...formData, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
908
1697
  error: budgetInvalid,
909
- helperText: budgetInvalid ? 'Enter a positive budget or tick "Unlimited"' : budgetEstimate !== null ? `\u2248 ${fmtInt(budgetEstimate)} tokens at the selected model's rate` : void 0,
1698
+ helperText: budgetHelperText(budgetInvalid, budgetEstimate),
910
1699
  disabled: unlimitedBudget,
911
1700
  required: true,
912
1701
  fullWidth: true
913
1702
  }
914
- ), /* @__PURE__ */ React3.createElement(
1703
+ ), /* @__PURE__ */ React4.createElement(
915
1704
  TextField2,
916
1705
  {
917
1706
  label: "TPM Limit",
@@ -921,7 +1710,7 @@ var init_GenerateKeyDialog = __esm({
921
1710
  helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
922
1711
  fullWidth: true
923
1712
  }
924
- ), /* @__PURE__ */ React3.createElement(
1713
+ ), /* @__PURE__ */ React4.createElement(
925
1714
  TextField2,
926
1715
  {
927
1716
  label: "RPM Limit",
@@ -931,7 +1720,7 @@ var init_GenerateKeyDialog = __esm({
931
1720
  helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
932
1721
  fullWidth: true
933
1722
  }
934
- ))), /* @__PURE__ */ React3.createElement(DialogActions2, null, newKeyValue ? /* @__PURE__ */ React3.createElement(Button3, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(Button3, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ React3.createElement(
1723
+ ))), /* @__PURE__ */ React4.createElement(DialogActions2, null, newKeyValue ? /* @__PURE__ */ React4.createElement(Button3, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Button3, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ React4.createElement(
935
1724
  Button3,
936
1725
  {
937
1726
  onClick: handleGenerate,
@@ -939,74 +1728,27 @@ var init_GenerateKeyDialog = __esm({
939
1728
  color: "primary",
940
1729
  disabled: !canGenerate
941
1730
  },
942
- submitting ? /* @__PURE__ */ React3.createElement(CircularProgress2, { size: 24 }) : "Generate"
1731
+ submitting ? /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 }) : "Generate"
943
1732
  ))));
944
1733
  };
945
- SnippetTabs = ({ snippets, model, onCopy }) => {
946
- const [tab, setTab] = useState2("curl");
947
- const code = tab === "curl" ? snippets.curl : snippets.openai;
948
- return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ React3.createElement(Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ React3.createElement(Tab, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ React3.createElement(
949
- Box3,
950
- {
951
- position: "relative",
952
- p: 1.5,
953
- sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
954
- },
955
- /* @__PURE__ */ React3.createElement(
956
- IconButton2,
957
- {
958
- size: "small",
959
- onClick: () => onCopy(code),
960
- title: "Copy snippet",
961
- sx: { position: "absolute", top: 4, right: 4 }
962
- },
963
- /* @__PURE__ */ React3.createElement(ContentCopy2, { fontSize: "small" })
964
- ),
965
- /* @__PURE__ */ React3.createElement(
966
- Typography3,
967
- {
968
- component: "pre",
969
- sx: {
970
- fontFamily: "monospace",
971
- fontSize: 12,
972
- whiteSpace: "pre-wrap",
973
- wordBreak: "break-all",
974
- mb: 0,
975
- pr: 4
976
- }
977
- },
978
- code
979
- ),
980
- model && /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary", display: "block", mt: 1 }, "Using model \u201C", model, "\u201D \u2014 swap it for any model you have access to.")
981
- ));
982
- };
983
1734
  }
984
1735
  });
985
1736
 
986
1737
  // src/components/UsageStats.tsx
987
- import React4, { useMemo as useMemo4, useState as useState3 } from "react";
988
- import {
989
- Paper as Paper3,
990
- Box as Box4,
991
- Typography as Typography4,
992
- FormControl,
993
- InputLabel,
994
- Select,
995
- MenuItem as MenuItem2,
996
- Grid,
997
- Tabs as Tabs2,
998
- Tab as Tab2,
999
- Table as Table2,
1000
- TableBody as TableBody2,
1001
- TableCell as TableCell2,
1002
- TableContainer as TableContainer2,
1003
- TableHead as TableHead2,
1004
- TableRow as TableRow2,
1005
- Chip as Chip3,
1006
- LinearProgress as LinearProgress3,
1007
- Skeleton,
1008
- useTheme
1009
- } from "@mui/material";
1738
+ import React5, { useMemo as useMemo4, useState as useState3 } from "react";
1739
+ import Paper3 from "@mui/material/Paper";
1740
+ import Box5 from "@mui/material/Box";
1741
+ import Typography5 from "@mui/material/Typography";
1742
+ import TextField3 from "@mui/material/TextField";
1743
+ import MenuItem2 from "@mui/material/MenuItem";
1744
+ import Grid from "@mui/material/Grid";
1745
+ import Table2 from "@mui/material/Table";
1746
+ import TableBody2 from "@mui/material/TableBody";
1747
+ import TableCell2 from "@mui/material/TableCell";
1748
+ import TableContainer2 from "@mui/material/TableContainer";
1749
+ import TableHead2 from "@mui/material/TableHead";
1750
+ import TableRow2 from "@mui/material/TableRow";
1751
+ import Skeleton3 from "@mui/material/Skeleton";
1010
1752
  import {
1011
1753
  AreaChart,
1012
1754
  Area,
@@ -1020,39 +1762,47 @@ import {
1020
1762
  Tooltip,
1021
1763
  ResponsiveContainer,
1022
1764
  Legend,
1023
- ReferenceLine
1765
+ ReferenceLine,
1766
+ Cell
1024
1767
  } from "recharts";
1025
- function modelColor(model) {
1026
- let h = 5381;
1027
- for (let i = 0; i < model.length; i++) h = (h << 5) + h + model.charCodeAt(i) >>> 0;
1028
- return MODEL_COLORS[h % MODEL_COLORS.length];
1768
+ function rateTone(rate) {
1769
+ if (rate >= 0.99) return "success";
1770
+ if (rate >= 0.9) return "warning";
1771
+ return "danger";
1029
1772
  }
1030
- var TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtPct, KpiCard, ChartSkeleton, EmptyChart, UsageStats;
1773
+ var TOP_N_MODELS, PERIOD_LS_KEY, PRESET_LABELS, fmtPct, ChartSkeleton, ChartOrFallback, SuccessRateCell, UsageStats;
1031
1774
  var init_UsageStats = __esm({
1032
1775
  "src/components/UsageStats.tsx"() {
1033
1776
  "use strict";
1034
1777
  init_format();
1778
+ init_ui();
1035
1779
  TOP_N_MODELS = 6;
1036
1780
  PERIOD_LS_KEY = "litellm_usage_period";
1037
- MODEL_COLORS = [
1038
- "#8884d8",
1039
- "#82ca9d",
1040
- "#ffc658",
1041
- "#ff7300",
1042
- "#0088fe",
1043
- "#00C49F",
1044
- "#FFBB28",
1045
- "#FF8042",
1046
- "#a4de6c",
1047
- "#d0ed57"
1048
- ];
1781
+ PRESET_LABELS = {
1782
+ today: "Today",
1783
+ "24h": "Last 24 hours",
1784
+ "7d": "Last 7 days",
1785
+ "30d": "Last 30 days"
1786
+ };
1049
1787
  fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
1050
- KpiCard = ({ label, value, hint }) => /* @__PURE__ */ React4.createElement(Paper3, { variant: "outlined", sx: { p: 2, height: "100%" } }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary" }, label), /* @__PURE__ */ React4.createElement(Typography4, { variant: "h5", sx: { mt: 0.5 } }, value), hint ? /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary" }, hint) : null);
1051
- ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */ React4.createElement(Skeleton, { variant: "rectangular", height, sx: { borderRadius: 1 } });
1052
- EmptyChart = ({
1053
- height = 260,
1054
- message = "No data for this period"
1055
- }) => /* @__PURE__ */ React4.createElement(Box4, { height, display: "flex", alignItems: "center", justifyContent: "center" }, /* @__PURE__ */ React4.createElement(Typography4, { color: "text.secondary", variant: "body2" }, message));
1788
+ ChartSkeleton = ({ height = 240 }) => /* @__PURE__ */ React5.createElement(Skeleton3, { variant: "rounded", height });
1789
+ ChartOrFallback = ({ loading, empty, height = 240, children }) => {
1790
+ if (loading) return /* @__PURE__ */ React5.createElement(ChartSkeleton, { height });
1791
+ if (empty) return /* @__PURE__ */ React5.createElement(EmptyState, { message: "No data for this period", height });
1792
+ return /* @__PURE__ */ React5.createElement(React5.Fragment, null, children);
1793
+ };
1794
+ SuccessRateCell = ({ rate, requests }) => {
1795
+ if (requests === 0) return /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary" }, "\u2014");
1796
+ const tone = rateTone(rate);
1797
+ return /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React5.createElement(Box5, { width: 72, flexShrink: 0 }, /* @__PURE__ */ React5.createElement(Meter, { value: rate * 100, tone, height: 4 })), /* @__PURE__ */ React5.createElement(
1798
+ Typography5,
1799
+ {
1800
+ variant: "caption",
1801
+ sx: { fontVariantNumeric: "tabular-nums", minWidth: 44 }
1802
+ },
1803
+ fmtPct(rate)
1804
+ ));
1805
+ };
1056
1806
  UsageStats = ({
1057
1807
  usage,
1058
1808
  models,
@@ -1061,14 +1811,9 @@ var init_UsageStats = __esm({
1061
1811
  loading,
1062
1812
  userInfo
1063
1813
  }) => {
1064
- const theme = useTheme();
1814
+ const chart = useChartTheme();
1065
1815
  const [selectedModel, setSelectedModel] = useState3("all");
1066
1816
  const [tab, setTab] = useState3("costs");
1067
- const gridStroke = theme.palette.divider;
1068
- const tickFill = theme.palette.text.secondary;
1069
- const tickStyle = { fontSize: 12, fill: tickFill };
1070
- const tickStyleSmall = { fontSize: 11, fill: tickFill };
1071
- const tickStyleTiny = { fontSize: 10, fill: tickFill };
1072
1817
  const selectedPreset = useMemo4(() => {
1073
1818
  if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
1074
1819
  const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
@@ -1176,85 +1921,236 @@ var init_UsageStats = __esm({
1176
1921
  const overallSuccessRate = totalRequests > 0 ? (usage?.successful_requests ?? 0) / totalRequests : 0;
1177
1922
  const maxBudget = userInfo?.max_budget ?? 0;
1178
1923
  const totalCumSpend = cumulativeData[cumulativeData.length - 1]?.cumulative ?? 0;
1179
- return /* @__PURE__ */ React4.createElement(Paper3, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, flexWrap: "wrap", gap: 2 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "h6" }, "Usage Analytics"), /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap" }, /* @__PURE__ */ React4.createElement(FormControl, { size: "small", sx: { minWidth: 140 } }, /* @__PURE__ */ React4.createElement(InputLabel, null, "Period"), /* @__PURE__ */ React4.createElement(
1180
- Select,
1924
+ const cumulativeDomain = maxBudget > 0 ? [0, (dataMax) => Math.max(dataMax, maxBudget) * 1.05] : [0, "auto"];
1925
+ const successTone = totalRequests === 0 ? "neutral" : rateTone(overallSuccessRate);
1926
+ const metrics = [
1181
1927
  {
1182
- value: selectedPreset,
1183
- label: "Period",
1184
- onChange: (e) => handlePresetChange(e.target.value)
1928
+ label: "Total spend",
1929
+ value: fmtUsd(usage?.total_spend ?? 0),
1930
+ hint: maxBudget > 0 ? `of ${fmtUsd(maxBudget)} budget` : void 0,
1931
+ tone: "accent"
1185
1932
  },
1186
- /* @__PURE__ */ React4.createElement(MenuItem2, { value: "today" }, "Today"),
1187
- /* @__PURE__ */ React4.createElement(MenuItem2, { value: "24h" }, "Last 24h"),
1188
- /* @__PURE__ */ React4.createElement(MenuItem2, { value: "7d" }, "Last 7 days"),
1189
- /* @__PURE__ */ React4.createElement(MenuItem2, { value: "30d" }, "Last 30 days")
1190
- )), tab === "models" && /* @__PURE__ */ React4.createElement(FormControl, { size: "small", sx: { minWidth: 180 } }, /* @__PURE__ */ React4.createElement(InputLabel, null, "Model"), /* @__PURE__ */ React4.createElement(
1191
- Select,
1192
1933
  {
1193
- value: selectedModel,
1194
- label: "Model",
1195
- onChange: (e) => setSelectedModel(e.target.value)
1934
+ label: "Requests",
1935
+ value: fmtInt(totalRequests),
1936
+ hint: `${fmtInt(usage?.failed_requests ?? 0)} failed`,
1937
+ tone: "info"
1196
1938
  },
1197
- /* @__PURE__ */ React4.createElement(MenuItem2, { value: "all" }, "All Models"),
1198
- models.map((m) => /* @__PURE__ */ React4.createElement(MenuItem2, { key: m.model_name, value: m.model_name }, m.model_name))
1199
- )))), /* @__PURE__ */ React4.createElement(Grid, { container: true, spacing: 2, sx: { mb: 2 } }, /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React4.createElement(KpiCard, { label: "Total Spend", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React4.createElement(KpiCard, { label: "Total Requests", value: fmtInt(totalRequests), hint: `${fmtInt(usage?.failed_requests ?? 0)} failed` })), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React4.createElement(KpiCard, { label: "Success Rate", value: totalRequests > 0 ? fmtPct(overallSuccessRate) : "\u2014" })), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React4.createElement(
1200
- KpiCard,
1201
1939
  {
1202
- label: "Total Tokens",
1203
- value: fmtInt(usage?.total_tokens ?? 0),
1204
- hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
1205
- }
1206
- ))), /* @__PURE__ */ React4.createElement(Tabs2, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 2 } }, /* @__PURE__ */ React4.createElement(Tab2, { value: "costs", label: "Costs" }), /* @__PURE__ */ React4.createElement(Tab2, { value: "models", label: "Model Activity" }), /* @__PURE__ */ React4.createElement(Tab2, { value: "keys", label: "Key Activity" })), tab === "costs" && /* @__PURE__ */ React4.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend by Model"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : modelSpendByDate.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart, { data: modelSpendByDate }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, void 0] }), /* @__PURE__ */ React4.createElement(Legend, null), topSpendModels.map((m) => /* @__PURE__ */ React4.createElement(
1207
- Area,
1940
+ label: "Success rate",
1941
+ value: totalRequests > 0 ? fmtPct(overallSuccessRate) : "\u2014",
1942
+ hint: totalRequests > 0 ? `${fmtInt(usage?.successful_requests ?? 0)} succeeded` : void 0,
1943
+ tone: successTone
1944
+ },
1208
1945
  {
1209
- key: m,
1210
- type: "monotone",
1211
- dataKey: m,
1212
- stackId: "spend",
1213
- stroke: modelColor(m),
1214
- fill: modelColor(m),
1215
- fillOpacity: 0.6
1946
+ label: "Tokens",
1947
+ value: fmtCompact(usage?.total_tokens ?? 0),
1948
+ hint: `${fmtCompact(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtCompact(usage?.completion_tokens ?? 0)} out`,
1949
+ tone: "success"
1216
1950
  }
1217
- )))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Token Usage"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle, tickFormatter: fmtInt }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [fmtInt(v), void 0] }), /* @__PURE__ */ React4.createElement(Legend, null), /* @__PURE__ */ React4.createElement(Area, { type: "monotone", dataKey: "promptTokens", name: "Input (prompt)", stackId: "tok", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.5 }), /* @__PURE__ */ React4.createElement(Area, { type: "monotone", dataKey: "completionTokens", name: "Output (completion)", stackId: "tok", stroke: "#82ca9d", fill: "#82ca9d", fillOpacity: 0.5 }))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(BarChart, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle }), /* @__PURE__ */ React4.createElement(Tooltip, null), /* @__PURE__ */ React4.createElement(Legend, null), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "successfulRequests", name: "Successful", fill: "#82ca9d", stackId: "r" }), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "failedRequests", name: "Failed", fill: "#e57373", stackId: "r" }))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Success Rate"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : successRateData.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(LineChart, { data: successRateData }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { domain: [0, 100], tick: tickStyle, tickFormatter: (v) => `${v}%` }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [`${v}%`, "Success rate"] }), /* @__PURE__ */ React4.createElement(ReferenceLine, { y: 100, stroke: "#82ca9d", strokeDasharray: "4 2" }), /* @__PURE__ */ React4.createElement(Line, { type: "monotone", dataKey: "successRate", name: "Success rate", stroke: "#8884d8", dot: { r: 3 } }))))), (maxBudget > 0 || cumulativeData.length > 0) && /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Cumulative Spend", maxBudget > 0 ? ` vs Budget (${fmtUsd(maxBudget)})` : "", maxBudget > 0 && /* @__PURE__ */ React4.createElement(Typography4, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 1 } }, fmtUsd(totalCumSpend), " used \xB7 ", fmtUsd(Math.max(0, maxBudget - totalCumSpend)), " remaining")), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 180 }) : cumulativeData.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, { height: 180 }) : /* @__PURE__ */ React4.createElement(Box4, { height: 180 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart, { data: cumulativeData }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Cumulative spend"] }), maxBudget > 0 && /* @__PURE__ */ React4.createElement(ReferenceLine, { y: maxBudget, stroke: "#e57373", strokeDasharray: "6 3", label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 11 } }), /* @__PURE__ */ React4.createElement(Area, { type: "monotone", dataKey: "cumulative", name: "Cumulative spend", stroke: "#ffc658", fill: "#ffc658", fillOpacity: 0.3 })))))), tab === "models" && /* @__PURE__ */ React4.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Model"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : topModelSpendBars.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: Math.max(200, topModelSpendBars.length * 36) }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(YAxis, { type: "category", dataKey: "model", tick: tickStyleSmall, width: 200 }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "spend", name: "Spend", radius: [0, 4, 4, 0] }, topModelSpendBars.map((r) => /* @__PURE__ */ React4.createElement("rect", { key: r.model, fill: modelColor(r.model) }))))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Tokens by Model"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : modelRows.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(BarChart, { data: modelRows }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "model", tick: tickStyleTiny, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle }), /* @__PURE__ */ React4.createElement(Tooltip, null), /* @__PURE__ */ React4.createElement(Legend, null), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "promptTokens", name: "Prompt", fill: "#8884d8", stackId: "t" }), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "completionTokens", name: "Completion", fill: "#82ca9d", stackId: "t" }))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React4.createElement(Table2, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead2, null, /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, null, "Model"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Success"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Prompt"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Completion"), /* @__PURE__ */ React4.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React4.createElement(TableBody2, null, loading ? /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React4.createElement(LinearProgress3, null))) : modelRows.length === 0 ? /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 8, align: "center" }, "No model activity")) : [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ React4.createElement(TableRow2, { key: r.model }, /* @__PURE__ */ React4.createElement(TableCell2, null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 0.75 }, /* @__PURE__ */ React4.createElement(Box4, { sx: { width: 10, height: 10, borderRadius: "50%", bgcolor: modelColor(r.model), flexShrink: 0 } }), r.model)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.promptTokens)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.completionTokens)), /* @__PURE__ */ React4.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(
1218
- LinearProgress3,
1219
- {
1220
- variant: "determinate",
1221
- value: r.successRate * 100,
1222
- sx: { flex: 1, height: 6, borderRadius: 3 }
1951
+ ];
1952
+ const renderKeyRows = () => {
1953
+ if (loading) {
1954
+ return /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, { colSpan: 7, sx: { py: 3 } }, /* @__PURE__ */ React5.createElement(Skeleton3, { variant: "rounded", height: 80 })));
1223
1955
  }
1224
- ), /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ React4.createElement(Grid, { container: true, spacing: 3 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Key"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : /* @__PURE__ */ React4.createElement(Box4, { height: Math.max(160, topKeySpendBars.length * 36) }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(BarChart, { data: topKeySpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(YAxis, { type: "category", dataKey: "keyAlias", tick: tickStyleSmall, width: 160 }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "spend", name: "Spend", fill: "#8884d8", radius: [0, 4, 4, 0] }))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React4.createElement(Table2, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead2, null, /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, null, "Key"), /* @__PURE__ */ React4.createElement(TableCell2, null, "Models"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Tokens"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React4.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React4.createElement(TableBody2, null, loading ? /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React4.createElement(LinearProgress3, null))) : keyRows.length === 0 ? /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 7, align: "center" }, "No key activity")) : [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ React4.createElement(TableRow2, { key: r.keyHash }, /* @__PURE__ */ React4.createElement(TableCell2, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2" }, r.keyAlias), r.teamId ? /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ React4.createElement(TableCell2, null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.map((m) => /* @__PURE__ */ React4.createElement(Chip3, { key: m, label: m, size: "small", variant: "outlined" })))), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.totalTokens)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React4.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(
1225
- LinearProgress3,
1226
- {
1227
- variant: "determinate",
1228
- value: r.successRate * 100,
1229
- sx: { flex: 1, height: 6, borderRadius: 3 }
1956
+ if (keyRows.length === 0) {
1957
+ return /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React5.createElement(EmptyState, { message: "No key activity in this period" })));
1958
+ }
1959
+ return [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ React5.createElement(TableRow2, { key: r.keyHash }, /* @__PURE__ */ React5.createElement(TableCell2, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", sx: { fontWeight: 600 } }, r.keyAlias), r.teamId ? /* @__PURE__ */ React5.createElement(Typography5, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ React5.createElement(TableCell2, null, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.slice(0, 3).map((m) => /* @__PURE__ */ React5.createElement(TagChip, { key: m, label: m, title: m })), r.models.length > 3 && /* @__PURE__ */ React5.createElement(TagChip, { label: `+${r.models.length - 3}`, mono: false }))), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, fmtCompact(r.totalTokens)), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React5.createElement(TableCell2, null, /* @__PURE__ */ React5.createElement(SuccessRateCell, { rate: r.successRate, requests: r.apiRequests }))));
1960
+ };
1961
+ const renderModelRows = () => {
1962
+ if (loading) {
1963
+ return /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, { colSpan: 8, sx: { py: 3 } }, /* @__PURE__ */ React5.createElement(Skeleton3, { variant: "rounded", height: 80 })));
1964
+ }
1965
+ if (modelRows.length === 0) {
1966
+ return /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React5.createElement(EmptyState, { message: "No model activity in this period" })));
1230
1967
  }
1231
- ), /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))));
1968
+ return [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ React5.createElement(TableRow2, { key: r.model }, /* @__PURE__ */ React5.createElement(TableCell2, null, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React5.createElement(Box5, { sx: { width: 8, height: 8, borderRadius: "50%", bgcolor: chartColor(r.model), flexShrink: 0 } }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", sx: { fontWeight: 600 } }, r.model))), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, fmtCompact(r.promptTokens)), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, fmtCompact(r.completionTokens)), /* @__PURE__ */ React5.createElement(TableCell2, null, /* @__PURE__ */ React5.createElement(SuccessRateCell, { rate: r.successRate, requests: r.apiRequests }))));
1969
+ };
1970
+ const periodSelect = /* @__PURE__ */ React5.createElement(
1971
+ TextField3,
1972
+ {
1973
+ select: true,
1974
+ size: "small",
1975
+ label: "Period",
1976
+ value: selectedPreset,
1977
+ onChange: (e) => handlePresetChange(e.target.value),
1978
+ sx: { minWidth: 160 }
1979
+ },
1980
+ Object.keys(PRESET_LABELS).map((p) => /* @__PURE__ */ React5.createElement(MenuItem2, { key: p, value: p }, PRESET_LABELS[p]))
1981
+ );
1982
+ return /* @__PURE__ */ React5.createElement(
1983
+ SectionCard,
1984
+ {
1985
+ title: "Usage Analytics",
1986
+ subtitle: `${PRESET_LABELS[selectedPreset]} \xB7 ${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
1987
+ actions: periodSelect
1988
+ },
1989
+ /* @__PURE__ */ React5.createElement(MetricStrip, { metrics }),
1990
+ /* @__PURE__ */ React5.createElement(
1991
+ Box5,
1992
+ {
1993
+ sx: {
1994
+ display: "flex",
1995
+ alignItems: "center",
1996
+ justifyContent: "space-between",
1997
+ gap: 2,
1998
+ flexWrap: "wrap",
1999
+ mt: 2.5,
2000
+ mb: 2
2001
+ }
2002
+ },
2003
+ /* @__PURE__ */ React5.createElement(
2004
+ SegmentedControl,
2005
+ {
2006
+ value: tab,
2007
+ onChange: setTab,
2008
+ options: [
2009
+ { value: "costs", label: "Costs" },
2010
+ { value: "models", label: "Model Activity" },
2011
+ { value: "keys", label: "Key Activity" }
2012
+ ]
2013
+ }
2014
+ ),
2015
+ tab === "models" && /* @__PURE__ */ React5.createElement(
2016
+ TextField3,
2017
+ {
2018
+ select: true,
2019
+ size: "small",
2020
+ label: "Model",
2021
+ value: selectedModel,
2022
+ onChange: (e) => setSelectedModel(e.target.value),
2023
+ sx: { minWidth: 220 }
2024
+ },
2025
+ /* @__PURE__ */ React5.createElement(MenuItem2, { value: "all" }, "All models"),
2026
+ models.map((m) => /* @__PURE__ */ React5.createElement(MenuItem2, { key: m.model_name, value: m.model_name }, m.model_name))
2027
+ )
2028
+ ),
2029
+ tab === "costs" && /* @__PURE__ */ React5.createElement(Grid, { container: true, spacing: 2 }, /* @__PURE__ */ React5.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React5.createElement(ChartCard, { title: "Daily spend by model", height: 260 }, /* @__PURE__ */ React5.createElement(ChartOrFallback, { loading, empty: modelSpendByDate.length === 0, height: 260 }, /* @__PURE__ */ React5.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React5.createElement(AreaChart, { data: modelSpendByDate, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ React5.createElement(CartesianGrid, { ...chart.grid }), /* @__PURE__ */ React5.createElement(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ React5.createElement(YAxis, { tickFormatter: fmtUsdCompact, width: 56, ...chart.axis }), /* @__PURE__ */ React5.createElement(
2030
+ Tooltip,
2031
+ {
2032
+ cursor: chart.cursor,
2033
+ content: /* @__PURE__ */ React5.createElement(ChartTooltip, { valueFormatter: fmtUsd, hideZero: true })
2034
+ }
2035
+ ), topSpendModels.map((m) => /* @__PURE__ */ React5.createElement(
2036
+ Area,
2037
+ {
2038
+ key: m,
2039
+ type: "monotone",
2040
+ dataKey: m,
2041
+ stackId: "spend",
2042
+ stroke: chartColor(m),
2043
+ strokeWidth: 1.5,
2044
+ fill: chartColor(m),
2045
+ fillOpacity: 0.28
2046
+ }
2047
+ ))))), !loading && topSpendModels.length > 0 && /* @__PURE__ */ React5.createElement(SeriesLegend, { series: topSpendModels.map((m) => ({ name: m, color: chartColor(m) })) }))), /* @__PURE__ */ React5.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React5.createElement(ChartCard, { title: "Daily token usage" }, /* @__PURE__ */ React5.createElement(ChartOrFallback, { loading, empty: dailyData.length === 0 }, /* @__PURE__ */ React5.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React5.createElement(AreaChart, { data: dailyData, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ React5.createElement(CartesianGrid, { ...chart.grid }), /* @__PURE__ */ React5.createElement(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ React5.createElement(YAxis, { tickFormatter: fmtCompact, width: 48, ...chart.axis }), /* @__PURE__ */ React5.createElement(Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ React5.createElement(ChartTooltip, { valueFormatter: fmtInt }) }), /* @__PURE__ */ React5.createElement(Legend, { ...chart.legend }), /* @__PURE__ */ React5.createElement(Area, { type: "monotone", dataKey: "promptTokens", name: "Input", stackId: "tok", stroke: SERIES.input, strokeWidth: 1.5, fill: SERIES.input, fillOpacity: 0.25 }), /* @__PURE__ */ React5.createElement(Area, { type: "monotone", dataKey: "completionTokens", name: "Output", stackId: "tok", stroke: SERIES.output, strokeWidth: 1.5, fill: SERIES.output, fillOpacity: 0.25 })))))), /* @__PURE__ */ React5.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React5.createElement(ChartCard, { title: "Daily requests" }, /* @__PURE__ */ React5.createElement(ChartOrFallback, { loading, empty: dailyData.length === 0 }, /* @__PURE__ */ React5.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React5.createElement(BarChart, { data: dailyData, margin: { top: 4, right: 8, left: 0, bottom: 0 }, barCategoryGap: "30%" }, /* @__PURE__ */ React5.createElement(CartesianGrid, { ...chart.grid }), /* @__PURE__ */ React5.createElement(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ React5.createElement(YAxis, { tickFormatter: fmtCompact, width: 48, ...chart.axis }), /* @__PURE__ */ React5.createElement(Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ React5.createElement(ChartTooltip, { valueFormatter: fmtInt }) }), /* @__PURE__ */ React5.createElement(Legend, { ...chart.legend }), /* @__PURE__ */ React5.createElement(Bar, { dataKey: "successfulRequests", name: "Successful", fill: SERIES.success, stackId: "r" }), /* @__PURE__ */ React5.createElement(Bar, { dataKey: "failedRequests", name: "Failed", fill: SERIES.failure, stackId: "r", radius: [3, 3, 0, 0] })))))), /* @__PURE__ */ React5.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React5.createElement(ChartCard, { title: "Daily success rate" }, /* @__PURE__ */ React5.createElement(ChartOrFallback, { loading, empty: successRateData.length === 0 }, /* @__PURE__ */ React5.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React5.createElement(LineChart, { data: successRateData, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ React5.createElement(CartesianGrid, { ...chart.grid }), /* @__PURE__ */ React5.createElement(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ React5.createElement(YAxis, { domain: [0, 100], tickFormatter: (v) => `${v}%`, width: 44, ...chart.axis }), /* @__PURE__ */ React5.createElement(
2048
+ Tooltip,
2049
+ {
2050
+ cursor: chart.cursor,
2051
+ content: /* @__PURE__ */ React5.createElement(ChartTooltip, { valueFormatter: (v) => `${v}%` })
2052
+ }
2053
+ ), /* @__PURE__ */ React5.createElement(ReferenceLine, { y: 100, stroke: SERIES.success, strokeDasharray: "4 4", strokeOpacity: 0.5 }), /* @__PURE__ */ React5.createElement(
2054
+ Line,
2055
+ {
2056
+ type: "monotone",
2057
+ dataKey: "successRate",
2058
+ name: "Success rate",
2059
+ stroke: SERIES.input,
2060
+ strokeWidth: 2,
2061
+ dot: { r: 2.5, strokeWidth: 0, fill: SERIES.input },
2062
+ activeDot: { r: 4 }
2063
+ }
2064
+ )))))), /* @__PURE__ */ React5.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React5.createElement(
2065
+ ChartCard,
2066
+ {
2067
+ title: maxBudget > 0 ? "Cumulative spend vs budget" : "Cumulative spend",
2068
+ meta: maxBudget > 0 ? `${fmtUsd(totalCumSpend)} used \xB7 ${fmtUsd(Math.max(0, maxBudget - totalCumSpend))} left` : void 0
2069
+ },
2070
+ /* @__PURE__ */ React5.createElement(ChartOrFallback, { loading, empty: cumulativeData.length === 0 }, /* @__PURE__ */ React5.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React5.createElement(AreaChart, { data: cumulativeData, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ React5.createElement(CartesianGrid, { ...chart.grid }), /* @__PURE__ */ React5.createElement(XAxis, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ React5.createElement(
2071
+ YAxis,
2072
+ {
2073
+ tickFormatter: fmtUsdCompact,
2074
+ width: 56,
2075
+ domain: cumulativeDomain,
2076
+ ...chart.axis
2077
+ }
2078
+ ), /* @__PURE__ */ React5.createElement(
2079
+ Tooltip,
2080
+ {
2081
+ cursor: chart.cursor,
2082
+ content: /* @__PURE__ */ React5.createElement(ChartTooltip, { valueFormatter: fmtUsd })
2083
+ }
2084
+ ), maxBudget > 0 && /* @__PURE__ */ React5.createElement(
2085
+ ReferenceLine,
2086
+ {
2087
+ y: maxBudget,
2088
+ stroke: SERIES.budget,
2089
+ strokeDasharray: "5 4",
2090
+ label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 10, fill: SERIES.budget }
2091
+ }
2092
+ ), /* @__PURE__ */ React5.createElement(
2093
+ Area,
2094
+ {
2095
+ type: "monotone",
2096
+ dataKey: "cumulative",
2097
+ name: "Cumulative spend",
2098
+ stroke: SERIES.spend,
2099
+ strokeWidth: 2,
2100
+ fill: SERIES.spend,
2101
+ fillOpacity: 0.18
2102
+ }
2103
+ ))))
2104
+ ))),
2105
+ tab === "models" && /* @__PURE__ */ React5.createElement(Grid, { container: true, spacing: 2 }, /* @__PURE__ */ React5.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React5.createElement(ChartCard, { title: "Spend by model", height: Math.max(200, topModelSpendBars.length * 34) }, /* @__PURE__ */ React5.createElement(
2106
+ ChartOrFallback,
2107
+ {
2108
+ loading,
2109
+ empty: topModelSpendBars.length === 0,
2110
+ height: Math.max(200, topModelSpendBars.length * 34)
2111
+ },
2112
+ /* @__PURE__ */ React5.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React5.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { top: 0, right: 12, left: 0, bottom: 0 } }, /* @__PURE__ */ React5.createElement(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }), /* @__PURE__ */ React5.createElement(XAxis, { type: "number", tickFormatter: fmtUsdCompact, ...chart.axis }), /* @__PURE__ */ React5.createElement(YAxis, { type: "category", dataKey: "model", width: 170, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }), /* @__PURE__ */ React5.createElement(Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ React5.createElement(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }), /* @__PURE__ */ React5.createElement(Bar, { dataKey: "spend", name: "Spend", radius: [0, 3, 3, 0], barSize: 14 }, topModelSpendBars.map((r) => /* @__PURE__ */ React5.createElement(Cell, { key: r.model, fill: chartColor(r.model) })))))
2113
+ ))), /* @__PURE__ */ React5.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React5.createElement(ChartCard, { title: "Tokens by model", height: Math.max(200, topModelSpendBars.length * 34) }, /* @__PURE__ */ React5.createElement(
2114
+ ChartOrFallback,
2115
+ {
2116
+ loading,
2117
+ empty: modelRows.length === 0,
2118
+ height: Math.max(200, topModelSpendBars.length * 34)
2119
+ },
2120
+ /* @__PURE__ */ React5.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React5.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { top: 0, right: 12, left: 0, bottom: 0 } }, /* @__PURE__ */ React5.createElement(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }), /* @__PURE__ */ React5.createElement(XAxis, { type: "number", tickFormatter: fmtCompact, ...chart.axis }), /* @__PURE__ */ React5.createElement(YAxis, { type: "category", dataKey: "model", width: 170, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }), /* @__PURE__ */ React5.createElement(Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ React5.createElement(ChartTooltip, { valueFormatter: fmtInt, labelFormatter: (l) => l }) }), /* @__PURE__ */ React5.createElement(Legend, { ...chart.legend }), /* @__PURE__ */ React5.createElement(Bar, { dataKey: "promptTokens", name: "Input", fill: SERIES.input, stackId: "t", barSize: 14 }), /* @__PURE__ */ React5.createElement(Bar, { dataKey: "completionTokens", name: "Output", fill: SERIES.output, stackId: "t", barSize: 14, radius: [0, 3, 3, 0] })))
2121
+ ))), /* @__PURE__ */ React5.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React5.createElement(TableContainer2, { component: Paper3, variant: "outlined", sx: { borderRadius: 2 } }, /* @__PURE__ */ React5.createElement(Table2, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React5.createElement(TableHead2, null, /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, null, "Model"), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, "Success"), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, "Input"), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, "Output"), /* @__PURE__ */ React5.createElement(TableCell2, { sx: { width: 160 } }, "Success rate"))), /* @__PURE__ */ React5.createElement(TableBody2, null, renderModelRows()))))),
2122
+ tab === "keys" && /* @__PURE__ */ React5.createElement(Grid, { container: true, spacing: 2 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ React5.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React5.createElement(ChartCard, { title: "Spend by key", height: Math.max(160, topKeySpendBars.length * 32) }, /* @__PURE__ */ React5.createElement(
2123
+ ChartOrFallback,
2124
+ {
2125
+ loading,
2126
+ empty: topKeySpendBars.length === 0,
2127
+ height: Math.max(160, topKeySpendBars.length * 32)
2128
+ },
2129
+ /* @__PURE__ */ React5.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React5.createElement(BarChart, { data: topKeySpendBars, layout: "vertical", margin: { top: 0, right: 12, left: 0, bottom: 0 } }, /* @__PURE__ */ React5.createElement(CartesianGrid, { ...chart.grid, vertical: true, horizontal: false }), /* @__PURE__ */ React5.createElement(XAxis, { type: "number", tickFormatter: fmtUsdCompact, ...chart.axis }), /* @__PURE__ */ React5.createElement(YAxis, { type: "category", dataKey: "keyAlias", width: 160, ...chart.axis, tick: { fontSize: 10.5, fill: chart.axis.tick.fill } }), /* @__PURE__ */ React5.createElement(Tooltip, { cursor: chart.cursor, content: /* @__PURE__ */ React5.createElement(ChartTooltip, { valueFormatter: fmtUsd, labelFormatter: (l) => l }) }), /* @__PURE__ */ React5.createElement(Bar, { dataKey: "spend", name: "Spend", fill: SERIES.input, radius: [0, 3, 3, 0], barSize: 14 })))
2130
+ ))), /* @__PURE__ */ React5.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React5.createElement(TableContainer2, { component: Paper3, variant: "outlined", sx: { borderRadius: 2 } }, /* @__PURE__ */ React5.createElement(Table2, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React5.createElement(TableHead2, null, /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, null, "Key"), /* @__PURE__ */ React5.createElement(TableCell2, null, "Models"), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, "Tokens"), /* @__PURE__ */ React5.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React5.createElement(TableCell2, { sx: { width: 160 } }, "Success rate"))), /* @__PURE__ */ React5.createElement(TableBody2, null, renderKeyRows())))))
2131
+ );
1232
2132
  };
1233
2133
  }
1234
2134
  });
1235
2135
 
1236
2136
  // src/components/TeamUsage.tsx
1237
- import React5, { useState as useState4 } from "react";
1238
- import {
1239
- Paper as Paper4,
1240
- Box as Box5,
1241
- Typography as Typography5,
1242
- LinearProgress as LinearProgress4,
1243
- Chip as Chip4,
1244
- Divider,
1245
- Stack as Stack2,
1246
- Table as Table3,
1247
- TableBody as TableBody3,
1248
- TableCell as TableCell3,
1249
- TableHead as TableHead3,
1250
- TableRow as TableRow3,
1251
- TableContainer as TableContainer3,
1252
- Collapse,
1253
- IconButton as IconButton3,
1254
- CircularProgress as CircularProgress3,
1255
- useTheme as useTheme2
1256
- } from "@mui/material";
1257
- import { ExpandMore, ExpandLess, Group, Speed, Memory } from "@mui/icons-material";
2137
+ import React6, { useState as useState4 } from "react";
2138
+ import Paper4 from "@mui/material/Paper";
2139
+ import Box6 from "@mui/material/Box";
2140
+ import Typography6 from "@mui/material/Typography";
2141
+ import Skeleton4 from "@mui/material/Skeleton";
2142
+ import Divider2 from "@mui/material/Divider";
2143
+ import Table3 from "@mui/material/Table";
2144
+ import TableBody3 from "@mui/material/TableBody";
2145
+ import TableCell3 from "@mui/material/TableCell";
2146
+ import TableHead3 from "@mui/material/TableHead";
2147
+ import TableRow3 from "@mui/material/TableRow";
2148
+ import TableContainer3 from "@mui/material/TableContainer";
2149
+ import Collapse from "@mui/material/Collapse";
2150
+ import IconButton3 from "@mui/material/IconButton";
2151
+ import CircularProgress3 from "@mui/material/CircularProgress";
2152
+ import { alpha as alpha4 } from "@mui/material/styles";
2153
+ import { ExpandMore, Group } from "@mui/icons-material";
1258
2154
  import {
1259
2155
  AreaChart as AreaChart2,
1260
2156
  Area as Area2,
@@ -1264,82 +2160,153 @@ import {
1264
2160
  Tooltip as Tooltip2,
1265
2161
  ResponsiveContainer as ResponsiveContainer2
1266
2162
  } from "recharts";
1267
- var Stat, TeamCard, TeamUsage;
2163
+ function budgetTone2(isOver, isNear) {
2164
+ if (isOver) return "danger";
2165
+ if (isNear) return "warning";
2166
+ return "accent";
2167
+ }
2168
+ var fmtUsd2, TeamCard, TeamUsage;
1268
2169
  var init_TeamUsage = __esm({
1269
2170
  "src/components/TeamUsage.tsx"() {
1270
2171
  "use strict";
1271
- Stat = ({ label, value }) => /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", fontWeight: 600, sx: { fontFamily: "monospace" } }, value));
2172
+ init_ui();
2173
+ fmtUsd2 = (n) => `$${(n ?? 0).toFixed(2)}`;
1272
2174
  TeamCard = ({ team, usage, usageLoading }) => {
1273
2175
  const [expanded, setExpanded] = useState4(false);
1274
- const theme = useTheme2();
1275
- const gridStroke = theme.palette.divider;
1276
- const tickFill = theme.palette.text.secondary;
2176
+ const chart = useChartTheme();
1277
2177
  const budget = team.max_budget ?? 0;
1278
2178
  const spend = team.spend ?? 0;
1279
2179
  const budgetPct = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
1280
2180
  const isOver = budget > 0 && spend >= budget;
1281
2181
  const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
1282
2182
  const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
1283
- return /* @__PURE__ */ React5.createElement(Paper4, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React5.createElement(Group, { color: "action" }), /* @__PURE__ */ React5.createElement(Box5, { flexGrow: 1 }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle1", fontWeight: 600 }, team.team_alias || "Untitled team"), /* @__PURE__ */ React5.createElement(Typography5, { variant: "caption", color: "text.secondary", sx: { fontFamily: "monospace" } }, team.team_id)), /* @__PURE__ */ React5.createElement(IconButton3, { size: "small", onClick: () => setExpanded((e) => !e), "aria-label": "toggle details" }, expanded ? /* @__PURE__ */ React5.createElement(ExpandLess, null) : /* @__PURE__ */ React5.createElement(ExpandMore, null))), /* @__PURE__ */ React5.createElement(Stack2, { direction: "row", spacing: 3, mt: 1.5, flexWrap: "wrap", useFlexGap: true, gap: 1.5 }, /* @__PURE__ */ React5.createElement(
1284
- Stat,
1285
- {
1286
- label: "Members",
1287
- value: team.members_with_roles?.length ? String(team.members_with_roles.length) : "\u2014"
1288
- }
1289
- ), /* @__PURE__ */ React5.createElement(
1290
- Stat,
1291
- {
1292
- label: "Models",
1293
- value: team.models?.length ? String(team.models.length) : "All"
1294
- }
1295
- ), /* @__PURE__ */ React5.createElement(
1296
- Stat,
1297
- {
1298
- label: "Budget",
1299
- value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited"
1300
- }
1301
- ), /* @__PURE__ */ React5.createElement(
1302
- Stat,
1303
- {
1304
- label: "Spend",
1305
- value: `$${spend.toFixed(2)}`
1306
- }
1307
- ), /* @__PURE__ */ React5.createElement(
1308
- Stat,
1309
- {
1310
- label: "TPM",
1311
- value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "\u2014"
2183
+ const renderDailySpendSection = () => {
2184
+ if (usageLoading) {
2185
+ return /* @__PURE__ */ React6.createElement(Box6, { display: "flex", justifyContent: "center", py: 3 }, /* @__PURE__ */ React6.createElement(CircularProgress3, { size: 22 }));
1312
2186
  }
1313
- ), /* @__PURE__ */ React5.createElement(
1314
- Stat,
2187
+ if (dailyData.length === 0) return null;
2188
+ return /* @__PURE__ */ React6.createElement(Box6, { mb: 2.5 }, /* @__PURE__ */ React6.createElement(
2189
+ Typography6,
2190
+ {
2191
+ variant: "caption",
2192
+ color: "text.secondary",
2193
+ sx: { display: "block", fontSize: 11, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase", mb: 1 }
2194
+ },
2195
+ "Daily spend"
2196
+ ), /* @__PURE__ */ React6.createElement(Box6, { height: 150 }, /* @__PURE__ */ React6.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React6.createElement(AreaChart2, { data: dailyData, margin: { top: 4, right: 8, left: 0, bottom: 0 } }, /* @__PURE__ */ React6.createElement(CartesianGrid2, { ...chart.grid }), /* @__PURE__ */ React6.createElement(XAxis2, { dataKey: "date", tickFormatter: fmtDateShort, ...chart.axis }), /* @__PURE__ */ React6.createElement(YAxis2, { tickFormatter: fmtUsdCompact, width: 52, ...chart.axis }), /* @__PURE__ */ React6.createElement(
2197
+ Tooltip2,
2198
+ {
2199
+ cursor: chart.cursor,
2200
+ content: /* @__PURE__ */ React6.createElement(ChartTooltip, { valueFormatter: (v) => `$${v.toFixed(4)}` })
2201
+ }
2202
+ ), /* @__PURE__ */ React6.createElement(
2203
+ Area2,
2204
+ {
2205
+ type: "monotone",
2206
+ dataKey: "spend",
2207
+ name: "Spend",
2208
+ stroke: SERIES.spend,
2209
+ strokeWidth: 2,
2210
+ fill: SERIES.spend,
2211
+ fillOpacity: 0.18
2212
+ }
2213
+ )))));
2214
+ };
2215
+ const sectionLabel = (label) => /* @__PURE__ */ React6.createElement(
2216
+ Typography6,
1315
2217
  {
1316
- label: "RPM",
1317
- value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "\u2014"
1318
- }
1319
- )), budget > 0 && /* @__PURE__ */ React5.createElement(Box5, { mt: 1.5 }, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ React5.createElement(Chip4, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ React5.createElement(Chip4, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ React5.createElement(
1320
- LinearProgress4,
2218
+ variant: "caption",
2219
+ color: "text.secondary",
2220
+ sx: { display: "block", fontSize: 11, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase", mb: 1 }
2221
+ },
2222
+ label
2223
+ );
2224
+ return /* @__PURE__ */ React6.createElement(Paper4, { variant: "outlined", sx: { borderRadius: 2, p: 2.5 } }, /* @__PURE__ */ React6.createElement(Box6, { display: "flex", alignItems: "center", gap: 1.5 }, /* @__PURE__ */ React6.createElement(
2225
+ Box6,
1321
2226
  {
1322
- variant: "determinate",
1323
- value: budgetPct,
1324
- color: isOver ? "error" : isNear ? "warning" : "primary",
1325
- sx: { height: 6, borderRadius: 1 }
1326
- }
1327
- )), /* @__PURE__ */ React5.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React5.createElement(Box5, { mt: 2 }, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React5.createElement(Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ React5.createElement(Stack2, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ React5.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ React5.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ React5.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ React5.createElement(Divider, { sx: { mb: 2 } }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ React5.createElement(Box5, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ React5.createElement(Chip4, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ React5.createElement(Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ React5.createElement(Box5, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React5.createElement(CircularProgress3, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React5.createElement(Box5, { height: 160, mb: 2 }, /* @__PURE__ */ React5.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React5.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React5.createElement(CartesianGrid2, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React5.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11, fill: tickFill } }), /* @__PURE__ */ React5.createElement(YAxis2, { tick: { fontSize: 11, fill: tickFill }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React5.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React5.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ React5.createElement(Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React5.createElement(Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ React5.createElement(TableContainer3, null, /* @__PURE__ */ React5.createElement(Table3, { size: "small" }, /* @__PURE__ */ React5.createElement(TableHead3, null, /* @__PURE__ */ React5.createElement(TableRow3, null, /* @__PURE__ */ React5.createElement(TableCell3, null, "User"), /* @__PURE__ */ React5.createElement(TableCell3, null, "Role"))), /* @__PURE__ */ React5.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React5.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React5.createElement(TableCell3, null, m.user_email ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2" }, m.user_email), /* @__PURE__ */ React5.createElement(
1328
- Typography5,
2227
+ sx: (theme) => ({
2228
+ width: 36,
2229
+ height: 36,
2230
+ borderRadius: 1.5,
2231
+ flexShrink: 0,
2232
+ display: "flex",
2233
+ alignItems: "center",
2234
+ justifyContent: "center",
2235
+ bgcolor: alpha4(theme.palette.primary.main, 0.1),
2236
+ color: theme.palette.primary.main
2237
+ })
2238
+ },
2239
+ /* @__PURE__ */ React6.createElement(Group, { fontSize: "small" })
2240
+ ), /* @__PURE__ */ React6.createElement(Box6, { flexGrow: 1, minWidth: 0 }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "subtitle1", sx: { fontWeight: 600, lineHeight: 1.3 } }, team.team_alias || "Untitled team"), /* @__PURE__ */ React6.createElement(
2241
+ Typography6,
1329
2242
  {
1330
2243
  variant: "caption",
1331
2244
  color: "text.secondary",
1332
- sx: { fontFamily: "monospace" }
2245
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 }
1333
2246
  },
1334
- m.user_id
1335
- )) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", sx: { fontFamily: "monospace" } }, m.user_id)), /* @__PURE__ */ React5.createElement(TableCell3, null, /* @__PURE__ */ React5.createElement(
1336
- Chip4,
2247
+ team.team_id
2248
+ )), isOver && /* @__PURE__ */ React6.createElement(StatusPill, { label: "Over budget", tone: "danger" }), isNear && /* @__PURE__ */ React6.createElement(StatusPill, { label: "Near limit", tone: "warning" }), /* @__PURE__ */ React6.createElement(
2249
+ IconButton3,
1337
2250
  {
1338
- label: m.role,
1339
2251
  size: "small",
1340
- color: m.role === "admin" ? "primary" : "default"
1341
- }
1342
- ))))))) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
2252
+ onClick: () => setExpanded((e) => !e),
2253
+ "aria-label": expanded ? "Hide team details" : "Show team details",
2254
+ "aria-expanded": expanded,
2255
+ sx: (theme) => ({
2256
+ color: theme.palette.text.secondary,
2257
+ transform: expanded ? "rotate(180deg)" : "none",
2258
+ transition: theme.transitions.create("transform")
2259
+ })
2260
+ },
2261
+ /* @__PURE__ */ React6.createElement(ExpandMore, null)
2262
+ )), /* @__PURE__ */ React6.createElement(
2263
+ Box6,
2264
+ {
2265
+ sx: {
2266
+ display: "grid",
2267
+ gridTemplateColumns: { xs: "repeat(3, minmax(0, 1fr))", sm: "repeat(6, minmax(0, 1fr))" },
2268
+ gap: 2,
2269
+ mt: 2.5,
2270
+ maxWidth: 640
2271
+ }
2272
+ },
2273
+ /* @__PURE__ */ React6.createElement(Stat, { label: "Members", value: team.members_with_roles?.length ?? "\u2014" }),
2274
+ /* @__PURE__ */ React6.createElement(Stat, { label: "Models", value: team.models?.length ? team.models.length : "All" }),
2275
+ /* @__PURE__ */ React6.createElement(Stat, { label: "Budget", value: budget > 0 ? fmtUsd2(budget) : "Unlimited" }),
2276
+ /* @__PURE__ */ React6.createElement(Stat, { label: "Spend", value: fmtUsd2(spend) }),
2277
+ /* @__PURE__ */ React6.createElement(Stat, { label: "TPM", value: team.tpm_limit && team.tpm_limit > 0 ? team.tpm_limit.toLocaleString() : "\u2014" }),
2278
+ /* @__PURE__ */ React6.createElement(Stat, { label: "RPM", value: team.rpm_limit && team.rpm_limit > 0 ? team.rpm_limit.toLocaleString() : "\u2014" })
2279
+ ), budget > 0 && /* @__PURE__ */ React6.createElement(Box6, { mt: 2, maxWidth: 640 }, /* @__PURE__ */ React6.createElement(Box6, { display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 0.75 }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, fmtUsd2(spend), " of ", fmtUsd2(budget)), /* @__PURE__ */ React6.createElement(Typography6, { variant: "caption", color: "text.secondary", sx: { fontVariantNumeric: "tabular-nums" } }, budgetPct.toFixed(0), "%")), /* @__PURE__ */ React6.createElement(Meter, { value: budgetPct, tone: budgetTone2(isOver, isNear), height: 5 })), /* @__PURE__ */ React6.createElement(Collapse, { in: expanded, unmountOnExit: true }, /* @__PURE__ */ React6.createElement(Divider2, { sx: { my: 2.5 } }), /* @__PURE__ */ React6.createElement(Box6, { mb: 2.5 }, sectionLabel("Models"), team.models?.length ? /* @__PURE__ */ React6.createElement(Box6, { display: "flex", gap: 0.5, flexWrap: "wrap" }, team.models.map((m) => /* @__PURE__ */ React6.createElement(TagChip, { key: m, label: m, title: m }))) : /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", color: "text.secondary" }, "All models allowed")), renderDailySpendSection(), /* @__PURE__ */ React6.createElement(Box6, null, sectionLabel("Members"), team.members_with_roles?.length ? /* @__PURE__ */ React6.createElement(
2280
+ TableContainer3,
2281
+ {
2282
+ component: Paper4,
2283
+ variant: "outlined",
2284
+ sx: { borderRadius: 1.5 }
2285
+ },
2286
+ /* @__PURE__ */ React6.createElement(Table3, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React6.createElement(TableHead3, null, /* @__PURE__ */ React6.createElement(TableRow3, null, /* @__PURE__ */ React6.createElement(TableCell3, null, "User"), /* @__PURE__ */ React6.createElement(TableCell3, { align: "right" }, "Role"))), /* @__PURE__ */ React6.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React6.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React6.createElement(TableCell3, null, m.user_email ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2" }, m.user_email), /* @__PURE__ */ React6.createElement(
2287
+ Typography6,
2288
+ {
2289
+ variant: "caption",
2290
+ color: "text.secondary",
2291
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 }
2292
+ },
2293
+ m.user_id
2294
+ )) : /* @__PURE__ */ React6.createElement(
2295
+ Typography6,
2296
+ {
2297
+ variant: "body2",
2298
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }
2299
+ },
2300
+ m.user_id
2301
+ )), /* @__PURE__ */ React6.createElement(TableCell3, { align: "right" }, /* @__PURE__ */ React6.createElement(
2302
+ StatusPill,
2303
+ {
2304
+ label: m.role,
2305
+ tone: m.role === "admin" ? "accent" : "neutral",
2306
+ dot: false
2307
+ }
2308
+ ))))))
2309
+ ) : /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
1343
2310
  };
1344
2311
  TeamUsage = ({
1345
2312
  teams,
@@ -1348,12 +2315,18 @@ var init_TeamUsage = __esm({
1348
2315
  getTeamUsageLoading
1349
2316
  }) => {
1350
2317
  if (loading) {
1351
- return /* @__PURE__ */ React5.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React5.createElement(LinearProgress4, null));
2318
+ return /* @__PURE__ */ React6.createElement(SectionCard, { title: "Teams" }, /* @__PURE__ */ React6.createElement(Skeleton4, { variant: "rounded", height: 120, sx: { mb: 2 } }), /* @__PURE__ */ React6.createElement(Skeleton4, { variant: "rounded", height: 120 }));
1352
2319
  }
1353
2320
  if (!teams.length) {
1354
- return /* @__PURE__ */ React5.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "flex-start", gap: 1.5 }, /* @__PURE__ */ React5.createElement(Group, { color: "disabled", sx: { mt: 0.5 } }), /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Typography5, { color: "text.secondary", variant: "body2" }, "You're not a member of any LiteLLM team yet."), /* @__PURE__ */ React5.createElement(Typography5, { color: "text.secondary", variant: "body2", mt: 0.5 }, "That's fine \u2014 your account is provisioned, so you can still generate personal keys and use models. If you need a shared budget with colleagues, ask an admin to add you to a team."))));
2321
+ return /* @__PURE__ */ React6.createElement(SectionCard, { title: "Teams" }, /* @__PURE__ */ React6.createElement(
2322
+ EmptyState,
2323
+ {
2324
+ message: "You're not a member of any LiteLLM team yet.",
2325
+ hint: "Your account is provisioned, so you can still generate personal keys and use models. Ask an admin to add you to a team if you need a shared budget."
2326
+ }
2327
+ ));
1355
2328
  }
1356
- return /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ React5.createElement(
2329
+ return /* @__PURE__ */ React6.createElement(SectionCard, { title: "Teams", subtitle: `${teams.length} team${teams.length === 1 ? "" : "s"}` }, /* @__PURE__ */ React6.createElement(Box6, { sx: { display: "flex", flexDirection: "column", gap: 2 } }, teams.map((team) => /* @__PURE__ */ React6.createElement(
1357
2330
  TeamCard,
1358
2331
  {
1359
2332
  key: team.team_id,
@@ -1361,41 +2334,42 @@ var init_TeamUsage = __esm({
1361
2334
  usage: getTeamUsage(team.team_id),
1362
2335
  usageLoading: getTeamUsageLoading(team.team_id)
1363
2336
  }
1364
- )));
2337
+ ))));
1365
2338
  };
1366
2339
  }
1367
2340
  });
1368
2341
 
1369
2342
  // src/components/AuditLog.tsx
1370
- import React6, { useState as useState5, useCallback } from "react";
1371
- import {
1372
- Paper as Paper5,
1373
- Box as Box6,
1374
- Typography as Typography6,
1375
- Table as Table4,
1376
- TableBody as TableBody4,
1377
- TableCell as TableCell4,
1378
- TableContainer as TableContainer4,
1379
- TableHead as TableHead4,
1380
- TableRow as TableRow4,
1381
- TablePagination,
1382
- TextField as TextField3,
1383
- MenuItem as MenuItem3,
1384
- Chip as Chip5,
1385
- CircularProgress as CircularProgress4,
1386
- Collapse as Collapse2,
1387
- IconButton as IconButton4,
1388
- Alert as Alert2
1389
- } from "@mui/material";
1390
- import { KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
2343
+ import React7, { useState as useState5, useCallback } from "react";
2344
+ import Box7 from "@mui/material/Box";
2345
+ import Typography7 from "@mui/material/Typography";
2346
+ import Table4 from "@mui/material/Table";
2347
+ import TableBody4 from "@mui/material/TableBody";
2348
+ import TableCell4 from "@mui/material/TableCell";
2349
+ import TableContainer4 from "@mui/material/TableContainer";
2350
+ import TableHead4 from "@mui/material/TableHead";
2351
+ import TableRow4 from "@mui/material/TableRow";
2352
+ import TablePagination from "@mui/material/TablePagination";
2353
+ import TextField4 from "@mui/material/TextField";
2354
+ import MenuItem3 from "@mui/material/MenuItem";
2355
+ import Skeleton5 from "@mui/material/Skeleton";
2356
+ import Collapse2 from "@mui/material/Collapse";
2357
+ import IconButton4 from "@mui/material/IconButton";
2358
+ import Alert2 from "@mui/material/Alert";
2359
+ import { alpha as alpha5 } from "@mui/material/styles";
2360
+ import { KeyboardArrowDown } from "@mui/icons-material";
1391
2361
  import { useAsync } from "react-use";
1392
- function actionColor(action) {
1393
- if (!action) return "default";
1394
- for (const [key, color] of Object.entries(ACTION_COLORS)) {
1395
- if (action.toLowerCase().includes(key)) return color;
2362
+ function actionTone(action) {
2363
+ if (!action) return "neutral";
2364
+ for (const [key, tone] of Object.entries(ACTION_TONES)) {
2365
+ if (action.toLowerCase().includes(key)) return tone;
1396
2366
  }
1397
2367
  return "info";
1398
2368
  }
2369
+ function prettyTableName(name) {
2370
+ if (!name) return "\u2014";
2371
+ return TABLE_LABELS[name] ?? name;
2372
+ }
1399
2373
  function formatDateTime(iso) {
1400
2374
  try {
1401
2375
  return new Date(iso).toLocaleString();
@@ -1403,21 +2377,101 @@ function formatDateTime(iso) {
1403
2377
  return iso;
1404
2378
  }
1405
2379
  }
1406
- var ACTION_COLORS, DetailRow, AuditLog;
2380
+ function renderAuditLogBody(loading, entries) {
2381
+ if (loading) {
2382
+ return /* @__PURE__ */ React7.createElement(TableRow4, null, /* @__PURE__ */ React7.createElement(TableCell4, { colSpan: 6, sx: { py: 2 } }, /* @__PURE__ */ React7.createElement(Skeleton5, { variant: "rounded", height: 160 })));
2383
+ }
2384
+ if (entries.length === 0) {
2385
+ return /* @__PURE__ */ React7.createElement(TableRow4, null, /* @__PURE__ */ React7.createElement(TableCell4, { colSpan: 6 }, /* @__PURE__ */ React7.createElement(
2386
+ EmptyState,
2387
+ {
2388
+ message: "No audit events found",
2389
+ hint: "Try widening the filters or a different time window."
2390
+ }
2391
+ )));
2392
+ }
2393
+ return entries.map((entry) => /* @__PURE__ */ React7.createElement(DetailRow, { key: entry.id, entry }));
2394
+ }
2395
+ var ACTION_TONES, TABLE_LABELS, DetailRow, AuditLog;
1407
2396
  var init_AuditLog = __esm({
1408
2397
  "src/components/AuditLog.tsx"() {
1409
2398
  "use strict";
1410
- ACTION_COLORS = {
2399
+ init_ui();
2400
+ ACTION_TONES = {
1411
2401
  created: "success",
1412
- deleted: "error",
2402
+ deleted: "danger",
1413
2403
  updated: "warning",
1414
- blocked: "error",
2404
+ blocked: "danger",
1415
2405
  unblocked: "success"
1416
2406
  };
2407
+ TABLE_LABELS = {
2408
+ LiteLLM_VerificationToken: "Key",
2409
+ LiteLLM_TeamTable: "Team",
2410
+ LiteLLM_UserTable: "User"
2411
+ };
1417
2412
  DetailRow = ({ entry }) => {
1418
2413
  const [open, setOpen] = useState5(false);
1419
2414
  const hasDetail = entry.before_value || entry.updated_values;
1420
- return /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(TableRow4, { hover: true }, /* @__PURE__ */ React6.createElement(TableCell4, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ React6.createElement(IconButton4, { size: "small", onClick: () => setOpen((o) => !o) }, open ? /* @__PURE__ */ React6.createElement(KeyboardArrowUp, { fontSize: "small" }) : /* @__PURE__ */ React6.createElement(KeyboardArrowDown, { fontSize: "small" }))), /* @__PURE__ */ React6.createElement(TableCell4, { sx: { whiteSpace: "nowrap" } }, formatDateTime(entry.updated_at)), /* @__PURE__ */ React6.createElement(TableCell4, null, entry.action && /* @__PURE__ */ React6.createElement(Chip5, { label: entry.action, color: actionColor(entry.action), size: "small" })), /* @__PURE__ */ React6.createElement(TableCell4, null, entry.table_name ?? "-"), /* @__PURE__ */ React6.createElement(TableCell4, null, /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", component: "code", sx: { fontFamily: "monospace", fontSize: "0.75rem" } }, entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "-")), /* @__PURE__ */ React6.createElement(TableCell4, null, entry.changed_by ?? "-")), hasDetail && /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { colSpan: 6, sx: { py: 0 } }, /* @__PURE__ */ React6.createElement(Collapse2, { in: open, unmountOnExit: true }, /* @__PURE__ */ React6.createElement(Box6, { p: 2, display: "flex", gap: 2, flexWrap: "wrap" }, entry.before_value && /* @__PURE__ */ React6.createElement(Box6, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "Before"), /* @__PURE__ */ React6.createElement(Typography6, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.before_value, null, 2))), entry.updated_values && /* @__PURE__ */ React6.createElement(Box6, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "After"), /* @__PURE__ */ React6.createElement(Typography6, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.updated_values, null, 2))))))));
2415
+ return /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(TableRow4, null, /* @__PURE__ */ React7.createElement(TableCell4, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ React7.createElement(
2416
+ IconButton4,
2417
+ {
2418
+ size: "small",
2419
+ onClick: () => setOpen((o) => !o),
2420
+ "aria-label": open ? "Hide changes" : "Show changes",
2421
+ "aria-expanded": open,
2422
+ sx: (theme) => ({
2423
+ color: theme.palette.text.secondary,
2424
+ transform: open ? "rotate(180deg)" : "none",
2425
+ transition: theme.transitions.create("transform")
2426
+ })
2427
+ },
2428
+ /* @__PURE__ */ React7.createElement(KeyboardArrowDown, { fontSize: "small" })
2429
+ )), /* @__PURE__ */ React7.createElement(TableCell4, { sx: { whiteSpace: "nowrap" } }, /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", color: "text.secondary" }, formatDateTime(entry.updated_at))), /* @__PURE__ */ React7.createElement(TableCell4, null, entry.action && /* @__PURE__ */ React7.createElement(StatusPill, { label: entry.action, tone: actionTone(entry.action) })), /* @__PURE__ */ React7.createElement(TableCell4, null, /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2" }, prettyTableName(entry.table_name))), /* @__PURE__ */ React7.createElement(TableCell4, null, /* @__PURE__ */ React7.createElement(
2430
+ Typography7,
2431
+ {
2432
+ variant: "body2",
2433
+ component: "code",
2434
+ color: "text.secondary",
2435
+ title: entry.object_id ?? void 0,
2436
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }
2437
+ },
2438
+ entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "\u2014"
2439
+ )), /* @__PURE__ */ React7.createElement(TableCell4, null, entry.changed_by ?? "\u2014")), hasDetail && /* @__PURE__ */ React7.createElement(TableRow4, { sx: { "&&:hover": { bgcolor: "transparent" } } }, /* @__PURE__ */ React7.createElement(TableCell4, { colSpan: 6, sx: { "&&": { py: 0, border: 0 } } }, /* @__PURE__ */ React7.createElement(Collapse2, { in: open, unmountOnExit: true }, /* @__PURE__ */ React7.createElement(
2440
+ Box7,
2441
+ {
2442
+ display: "flex",
2443
+ gap: 2,
2444
+ flexWrap: "wrap",
2445
+ sx: (theme) => ({
2446
+ p: 2,
2447
+ mb: 1.5,
2448
+ borderRadius: 1.5,
2449
+ bgcolor: alpha5(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.05 : 0.03)
2450
+ })
2451
+ },
2452
+ entry.before_value && /* @__PURE__ */ React7.createElement(Box7, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React7.createElement(
2453
+ Typography7,
2454
+ {
2455
+ variant: "caption",
2456
+ color: "text.secondary",
2457
+ display: "block",
2458
+ mb: 0.75,
2459
+ sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" }
2460
+ },
2461
+ "Before"
2462
+ ), /* @__PURE__ */ React7.createElement(Typography7, { component: "pre", variant: "caption", sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0 } }, JSON.stringify(entry.before_value, null, 2))),
2463
+ entry.updated_values && /* @__PURE__ */ React7.createElement(Box7, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React7.createElement(
2464
+ Typography7,
2465
+ {
2466
+ variant: "caption",
2467
+ color: "text.secondary",
2468
+ display: "block",
2469
+ mb: 0.75,
2470
+ sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" }
2471
+ },
2472
+ "After"
2473
+ ), /* @__PURE__ */ React7.createElement(Typography7, { component: "pre", variant: "caption", sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0 } }, JSON.stringify(entry.updated_values, null, 2)))
2474
+ )))));
1421
2475
  };
1422
2476
  AuditLog = ({ api }) => {
1423
2477
  const [page, setPage] = useState5(0);
@@ -1433,68 +2487,80 @@ var init_AuditLog = __esm({
1433
2487
  );
1434
2488
  const entries = value?.audit_logs ?? [];
1435
2489
  const total = value?.total ?? 0;
1436
- return /* @__PURE__ */ React6.createElement(Paper5, null, /* @__PURE__ */ React6.createElement(Box6, { p: 2, display: "flex", gap: 2, flexWrap: "wrap", alignItems: "center" }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "h6", sx: { flex: "0 0 auto", mr: 1 } }, "Audit Log"), /* @__PURE__ */ React6.createElement(
1437
- TextField3,
1438
- {
1439
- size: "small",
1440
- label: "Action",
1441
- select: true,
1442
- value: filters.action ?? "",
1443
- onChange: (e) => {
1444
- setFilters((f) => ({ ...f, action: e.target.value || void 0 }));
1445
- setPage(0);
1446
- },
1447
- sx: { minWidth: 160 }
1448
- },
1449
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "" }, "All actions"),
1450
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "created" }, "Created"),
1451
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "updated" }, "Updated"),
1452
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "deleted" }, "Deleted"),
1453
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "blocked" }, "Blocked")
1454
- ), /* @__PURE__ */ React6.createElement(
1455
- TextField3,
2490
+ return /* @__PURE__ */ React7.createElement(
2491
+ SectionCard,
1456
2492
  {
1457
- size: "small",
1458
- label: "Table",
1459
- select: true,
1460
- value: filters.table_name ?? "",
1461
- onChange: (e) => {
1462
- setFilters((f) => ({ ...f, table_name: e.target.value || void 0 }));
1463
- setPage(0);
1464
- },
1465
- sx: { minWidth: 160 }
2493
+ title: "Audit Log",
2494
+ subtitle: loading ? "Loading\u2026" : `${total.toLocaleString()} event${total === 1 ? "" : "s"}`,
2495
+ flush: true,
2496
+ actions: /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(
2497
+ TextField4,
2498
+ {
2499
+ size: "small",
2500
+ label: "Action",
2501
+ select: true,
2502
+ value: filters.action ?? "",
2503
+ onChange: (e) => {
2504
+ setFilters((f) => ({ ...f, action: e.target.value || void 0 }));
2505
+ setPage(0);
2506
+ },
2507
+ sx: { minWidth: 150 }
2508
+ },
2509
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "" }, "All actions"),
2510
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "created" }, "Created"),
2511
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "updated" }, "Updated"),
2512
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "deleted" }, "Deleted"),
2513
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "blocked" }, "Blocked")
2514
+ ), /* @__PURE__ */ React7.createElement(
2515
+ TextField4,
2516
+ {
2517
+ size: "small",
2518
+ label: "Table",
2519
+ select: true,
2520
+ value: filters.table_name ?? "",
2521
+ onChange: (e) => {
2522
+ setFilters((f) => ({ ...f, table_name: e.target.value || void 0 }));
2523
+ setPage(0);
2524
+ },
2525
+ sx: { minWidth: 150 }
2526
+ },
2527
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "" }, "All tables"),
2528
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "LiteLLM_VerificationToken" }, "Key"),
2529
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "LiteLLM_TeamTable" }, "Team"),
2530
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "LiteLLM_UserTable" }, "User")
2531
+ ), /* @__PURE__ */ React7.createElement(
2532
+ TextField4,
2533
+ {
2534
+ size: "small",
2535
+ label: "Changed by",
2536
+ value: filters.changed_by ?? "",
2537
+ onChange: (e) => {
2538
+ setFilters((f) => ({ ...f, changed_by: e.target.value || void 0 }));
2539
+ setPage(0);
2540
+ },
2541
+ sx: { minWidth: 180 }
2542
+ }
2543
+ ))
1466
2544
  },
1467
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "" }, "All tables"),
1468
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "LiteLLM_VerificationToken" }, "Key"),
1469
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "LiteLLM_TeamTable" }, "Team"),
1470
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "LiteLLM_UserTable" }, "User")
1471
- ), /* @__PURE__ */ React6.createElement(
1472
- TextField3,
1473
- {
1474
- size: "small",
1475
- label: "Changed by",
1476
- value: filters.changed_by ?? "",
1477
- onChange: (e) => {
1478
- setFilters((f) => ({ ...f, changed_by: e.target.value || void 0 }));
1479
- setPage(0);
1480
- },
1481
- sx: { minWidth: 200 }
1482
- }
1483
- )), error && /* @__PURE__ */ React6.createElement(Box6, { px: 2, pb: 2 }, /* @__PURE__ */ React6.createElement(Alert2, { severity: "error" }, error.message)), /* @__PURE__ */ React6.createElement(TableContainer4, null, /* @__PURE__ */ React6.createElement(Table4, { size: "small" }, /* @__PURE__ */ React6.createElement(TableHead4, null, /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { sx: { width: 40 } }), /* @__PURE__ */ React6.createElement(TableCell4, null, "Time"), /* @__PURE__ */ React6.createElement(TableCell4, null, "Action"), /* @__PURE__ */ React6.createElement(TableCell4, null, "Table"), /* @__PURE__ */ React6.createElement(TableCell4, null, "Object ID"), /* @__PURE__ */ React6.createElement(TableCell4, null, "Changed By"))), /* @__PURE__ */ React6.createElement(TableBody4, null, loading ? /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ React6.createElement(CircularProgress4, { size: 24 }))) : entries.length === 0 ? /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ React6.createElement(Typography6, { color: "text.secondary" }, "No audit events found"))) : entries.map((entry) => /* @__PURE__ */ React6.createElement(DetailRow, { key: entry.id, entry }))))), /* @__PURE__ */ React6.createElement(
1484
- TablePagination,
1485
- {
1486
- component: "div",
1487
- count: total,
1488
- page,
1489
- onPageChange: (_, p) => setPage(p),
1490
- rowsPerPage: pageSize,
1491
- onRowsPerPageChange: (e) => {
1492
- setPageSize(Number(e.target.value));
1493
- setPage(0);
1494
- },
1495
- rowsPerPageOptions: [10, 25, 50]
1496
- }
1497
- ));
2545
+ error && /* @__PURE__ */ React7.createElement(Box7, { px: 2.5, pb: 2 }, /* @__PURE__ */ React7.createElement(Alert2, { severity: "error" }, error.message)),
2546
+ /* @__PURE__ */ React7.createElement(TableContainer4, null, /* @__PURE__ */ React7.createElement(Table4, { size: "small", sx: dataTableSx }, /* @__PURE__ */ React7.createElement(TableHead4, null, /* @__PURE__ */ React7.createElement(TableRow4, null, /* @__PURE__ */ React7.createElement(TableCell4, { sx: { width: 40 } }), /* @__PURE__ */ React7.createElement(TableCell4, null, "Time"), /* @__PURE__ */ React7.createElement(TableCell4, null, "Action"), /* @__PURE__ */ React7.createElement(TableCell4, null, "Table"), /* @__PURE__ */ React7.createElement(TableCell4, null, "Object ID"), /* @__PURE__ */ React7.createElement(TableCell4, null, "Changed By"))), /* @__PURE__ */ React7.createElement(TableBody4, null, renderAuditLogBody(loading, entries)))),
2547
+ /* @__PURE__ */ React7.createElement(
2548
+ TablePagination,
2549
+ {
2550
+ component: "div",
2551
+ count: total,
2552
+ page,
2553
+ onPageChange: (_, p) => setPage(p),
2554
+ rowsPerPage: pageSize,
2555
+ onRowsPerPageChange: (e) => {
2556
+ setPageSize(Number(e.target.value));
2557
+ setPage(0);
2558
+ },
2559
+ rowsPerPageOptions: [10, 25, 50],
2560
+ sx: (theme) => ({ borderTop: `1px solid ${theme.palette.divider}` })
2561
+ }
2562
+ )
2563
+ );
1498
2564
  };
1499
2565
  }
1500
2566
  });
@@ -1504,8 +2570,15 @@ var LiteLLMPage_exports = {};
1504
2570
  __export(LiteLLMPage_exports, {
1505
2571
  LiteLLMPage: () => LiteLLMPage
1506
2572
  });
1507
- import React7, { useState as useState6, useCallback as useCallback2, useMemo as useMemo5 } from "react";
1508
- import { Box as Box7, Snackbar, Alert as Alert3, CircularProgress as CircularProgress5, Typography as Typography7, Paper as Paper6, Tabs as Tabs3, Tab as Tab3 } from "@mui/material";
2573
+ import React8, { useState as useState6, useCallback as useCallback2, useMemo as useMemo5 } from "react";
2574
+ import Box8 from "@mui/material/Box";
2575
+ import Snackbar from "@mui/material/Snackbar";
2576
+ import Alert3 from "@mui/material/Alert";
2577
+ import CircularProgress4 from "@mui/material/CircularProgress";
2578
+ import Typography8 from "@mui/material/Typography";
2579
+ import Paper5 from "@mui/material/Paper";
2580
+ import Tabs2 from "@mui/material/Tabs";
2581
+ import Tab2 from "@mui/material/Tab";
1509
2582
  import { useAsync as useAsync2, useAsyncRetry } from "react-use";
1510
2583
  import { useApi } from "@backstage/core-plugin-api";
1511
2584
  function initDateRange() {
@@ -1710,34 +2783,42 @@ var init_LiteLLMPage = __esm({
1710
2783
  );
1711
2784
  const isInitialLoading = userLoading && !userInfo;
1712
2785
  if (isInitialLoading) {
1713
- return /* @__PURE__ */ React7.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React7.createElement(CircularProgress5, null));
2786
+ return /* @__PURE__ */ React8.createElement(Box8, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React8.createElement(CircularProgress4, null));
1714
2787
  }
1715
2788
  if (userError || !userInfo) {
1716
2789
  const isProvisioningEnabled = userError?.body?.provisioning === true;
1717
2790
  const hint = userError?.body?.hint;
1718
- return /* @__PURE__ */ React7.createElement(Box7, { p: 3 }, /* @__PURE__ */ React7.createElement(Paper6, { sx: { p: 3 } }, /* @__PURE__ */ React7.createElement(Typography7, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React7.createElement(Typography7, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
2791
+ return /* @__PURE__ */ React8.createElement(Box8, { p: 3 }, /* @__PURE__ */ React8.createElement(Paper5, { sx: { p: 3 } }, /* @__PURE__ */ React8.createElement(Typography8, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React8.createElement(Typography8, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React8.createElement(Typography8, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
1719
2792
  }
1720
- return /* @__PURE__ */ React7.createElement(Box7, { p: 3 }, /* @__PURE__ */ React7.createElement(Box7, { mb: 2 }, /* @__PURE__ */ React7.createElement(
2793
+ const pageTabs = /* @__PURE__ */ React8.createElement(
2794
+ Tabs2,
2795
+ {
2796
+ value: activeTab,
2797
+ onChange: (_, v) => setActiveTab(v),
2798
+ variant: "scrollable",
2799
+ scrollButtons: "auto",
2800
+ sx: {
2801
+ minHeight: 44,
2802
+ '& [class*="MuiTabs-indicator"]': { height: 2, borderRadius: "2px 2px 0 0" },
2803
+ '& [class*="MuiTab-root"]': { minHeight: 44, textTransform: "none", fontSize: 14 }
2804
+ }
2805
+ },
2806
+ /* @__PURE__ */ React8.createElement(Tab2, { label: "Overview", value: "overview" }),
2807
+ /* @__PURE__ */ React8.createElement(Tab2, { label: "Keys", value: "keys" }),
2808
+ /* @__PURE__ */ React8.createElement(Tab2, { label: "Teams", value: "teams" }),
2809
+ userInfo.can_view_audit && /* @__PURE__ */ React8.createElement(Tab2, { label: "Audit Log", value: "audit" })
2810
+ );
2811
+ return /* @__PURE__ */ React8.createElement(Box8, { sx: { p: 3, display: "flex", flexDirection: "column", gap: 2 } }, /* @__PURE__ */ React8.createElement(
1721
2812
  DashboardHeader,
1722
2813
  {
1723
2814
  userInfo,
1724
2815
  teams: teams ?? [],
1725
2816
  keys: keys ?? [],
1726
2817
  loading: userLoading || teamsLoading,
1727
- onGenerateKeyClick: () => setGenerateDialogOpen(true)
2818
+ onGenerateKeyClick: () => setGenerateDialogOpen(true),
2819
+ tabs: pageTabs
1728
2820
  }
1729
- )), /* @__PURE__ */ React7.createElement(
1730
- Tabs3,
1731
- {
1732
- value: activeTab,
1733
- onChange: (_, v) => setActiveTab(v),
1734
- sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
1735
- },
1736
- /* @__PURE__ */ React7.createElement(Tab3, { label: "Overview", value: "overview" }),
1737
- /* @__PURE__ */ React7.createElement(Tab3, { label: "Keys", value: "keys" }),
1738
- /* @__PURE__ */ React7.createElement(Tab3, { label: "Teams", value: "teams" }),
1739
- userInfo.can_view_audit && /* @__PURE__ */ React7.createElement(Tab3, { label: "Audit Log", value: "audit" })
1740
- ), activeTab === "overview" && /* @__PURE__ */ React7.createElement(
2821
+ ), activeTab === "overview" && /* @__PURE__ */ React8.createElement(
1741
2822
  UsageStats,
1742
2823
  {
1743
2824
  usage: usage ?? null,
@@ -1747,7 +2828,7 @@ var init_LiteLLMPage = __esm({
1747
2828
  loading: usageLoading,
1748
2829
  userInfo
1749
2830
  }
1750
- ), activeTab === "keys" && /* @__PURE__ */ React7.createElement(
2831
+ ), activeTab === "keys" && /* @__PURE__ */ React8.createElement(
1751
2832
  KeysTable,
1752
2833
  {
1753
2834
  keys: keys ?? [],
@@ -1761,7 +2842,7 @@ var init_LiteLLMPage = __esm({
1761
2842
  onDeleteKey: handleDeleteKey,
1762
2843
  onPruneExpiredKeys: handlePruneExpiredKeys
1763
2844
  }
1764
- ), activeTab === "teams" && /* @__PURE__ */ React7.createElement(
2845
+ ), activeTab === "teams" && /* @__PURE__ */ React8.createElement(
1765
2846
  TeamUsage,
1766
2847
  {
1767
2848
  teams: teams ?? [],
@@ -1772,7 +2853,7 @@ var init_LiteLLMPage = __esm({
1772
2853
  },
1773
2854
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
1774
2855
  }
1775
- ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ React7.createElement(AuditLog, { api }), /* @__PURE__ */ React7.createElement(
2856
+ ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ React8.createElement(AuditLog, { api }), /* @__PURE__ */ React8.createElement(
1776
2857
  GenerateKeyDialog,
1777
2858
  {
1778
2859
  open: generateDialogOpen,
@@ -1785,7 +2866,7 @@ var init_LiteLLMPage = __esm({
1785
2866
  onGenerateKey: handleGenerateKey,
1786
2867
  onGetConfig: () => api.getConfig()
1787
2868
  }
1788
- ), /* @__PURE__ */ React7.createElement(
2869
+ ), /* @__PURE__ */ React8.createElement(
1789
2870
  Snackbar,
1790
2871
  {
1791
2872
  open: !!snackbar,
@@ -1793,7 +2874,7 @@ var init_LiteLLMPage = __esm({
1793
2874
  onClose: () => setSnackbar(null),
1794
2875
  anchorOrigin: { vertical: "bottom", horizontal: "right" }
1795
2876
  },
1796
- snackbar ? /* @__PURE__ */ React7.createElement(Alert3, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
2877
+ snackbar ? /* @__PURE__ */ React8.createElement(Alert3, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
1797
2878
  ));
1798
2879
  };
1799
2880
  }
@@ -1801,7 +2882,7 @@ var init_LiteLLMPage = __esm({
1801
2882
 
1802
2883
  // src/plugin.tsx
1803
2884
  init_api();
1804
- import React8 from "react";
2885
+ import React9 from "react";
1805
2886
  import { TrendingUp as TrendingUpIcon } from "@mui/icons-material";
1806
2887
  import {
1807
2888
  createFrontendPlugin,
@@ -1820,10 +2901,10 @@ var liteLlmPage = PageBlueprint.make({
1820
2901
  params: {
1821
2902
  path: "/litellm",
1822
2903
  title: "LiteLLM",
1823
- icon: /* @__PURE__ */ React8.createElement(TrendingUpIcon, null),
2904
+ icon: /* @__PURE__ */ React9.createElement(TrendingUpIcon, null),
1824
2905
  loader: async () => {
1825
2906
  const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
1826
- return /* @__PURE__ */ React8.createElement(LiteLLMPage2, null);
2907
+ return /* @__PURE__ */ React9.createElement(LiteLLMPage2, null);
1827
2908
  }
1828
2909
  }
1829
2910
  });
@@ -1842,18 +2923,16 @@ init_TeamUsage();
1842
2923
  // src/components/LiteLLMHomeWidget.tsx
1843
2924
  init_api();
1844
2925
  init_format();
1845
- import React9, { useState as useState7, useEffect as useEffect2 } from "react";
1846
- import {
1847
- Paper as Paper7,
1848
- Box as Box8,
1849
- Typography as Typography8,
1850
- FormControl as FormControl2,
1851
- Select as Select2,
1852
- MenuItem as MenuItem4,
1853
- Grid as Grid2,
1854
- CircularProgress as CircularProgress6,
1855
- Alert as Alert4
1856
- } from "@mui/material";
2926
+ import React10, { useState as useState7, useEffect as useEffect2 } from "react";
2927
+ import Paper6 from "@mui/material/Paper";
2928
+ import Box9 from "@mui/material/Box";
2929
+ import Typography9 from "@mui/material/Typography";
2930
+ import FormControl from "@mui/material/FormControl";
2931
+ import Select from "@mui/material/Select";
2932
+ import MenuItem4 from "@mui/material/MenuItem";
2933
+ import Grid2 from "@mui/material/Grid";
2934
+ import CircularProgress5 from "@mui/material/CircularProgress";
2935
+ import Alert4 from "@mui/material/Alert";
1857
2936
  import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
1858
2937
  import { useApi as useApi2 } from "@backstage/core-plugin-api";
1859
2938
  function presetToDateRange(preset) {
@@ -1868,7 +2947,7 @@ function presetToDateRange(preset) {
1868
2947
  }
1869
2948
  return { start, end };
1870
2949
  }
1871
- var Kpi = ({ label, value }) => /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Typography8, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React9.createElement(Typography8, { variant: "subtitle1", fontWeight: 600 }, value));
2950
+ var Kpi = ({ label, value }) => /* @__PURE__ */ React10.createElement(Box9, null, /* @__PURE__ */ React10.createElement(Typography9, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React10.createElement(Typography9, { variant: "subtitle1", fontWeight: 600 }, value));
1872
2951
  var LiteLLMHomeWidget = ({
1873
2952
  defaultPeriod = "7d",
1874
2953
  title = "LiteLLM Usage"
@@ -1917,17 +2996,17 @@ var LiteLLMHomeWidget = ({
1917
2996
  spend: d.spend
1918
2997
  }));
1919
2998
  const hasSparkline = dailyData.length > 0;
1920
- return /* @__PURE__ */ React9.createElement(Paper7, { sx: { p: 2 } }, /* @__PURE__ */ React9.createElement(Box8, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ React9.createElement(Typography8, { variant: "h6" }, title), /* @__PURE__ */ React9.createElement(FormControl2, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ React9.createElement(
1921
- Select2,
2999
+ return /* @__PURE__ */ React10.createElement(Paper6, { sx: { p: 2 } }, /* @__PURE__ */ React10.createElement(Box9, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ React10.createElement(Typography9, { variant: "h6" }, title), /* @__PURE__ */ React10.createElement(FormControl, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ React10.createElement(
3000
+ Select,
1922
3001
  {
1923
3002
  value: period,
1924
3003
  onChange: (e) => setPeriod(e.target.value),
1925
3004
  displayEmpty: true
1926
3005
  },
1927
- /* @__PURE__ */ React9.createElement(MenuItem4, { value: "today" }, "Today"),
1928
- /* @__PURE__ */ React9.createElement(MenuItem4, { value: "7d" }, "7d"),
1929
- /* @__PURE__ */ React9.createElement(MenuItem4, { value: "30d" }, "30d")
1930
- ))), loading && /* @__PURE__ */ React9.createElement(Box8, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React9.createElement(CircularProgress6, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ React9.createElement(Alert4, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ React9.createElement(React9.Fragment, null, partialFailure && /* @__PURE__ */ React9.createElement(Alert4, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ React9.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React9.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React9.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React9.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React9.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React9.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React9.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React9.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React9.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ React9.createElement(Box8, { height: 120 }, /* @__PURE__ */ React9.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React9.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React9.createElement(
3006
+ /* @__PURE__ */ React10.createElement(MenuItem4, { value: "today" }, "Today"),
3007
+ /* @__PURE__ */ React10.createElement(MenuItem4, { value: "7d" }, "7d"),
3008
+ /* @__PURE__ */ React10.createElement(MenuItem4, { value: "30d" }, "30d")
3009
+ ))), loading && /* @__PURE__ */ React10.createElement(Box9, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React10.createElement(CircularProgress5, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ React10.createElement(Alert4, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ React10.createElement(React10.Fragment, null, partialFailure && /* @__PURE__ */ React10.createElement(Alert4, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ React10.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React10.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React10.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React10.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React10.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React10.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React10.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React10.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React10.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ React10.createElement(Box9, { height: 120 }, /* @__PURE__ */ React10.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React10.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React10.createElement(
1931
3010
  Area3,
1932
3011
  {
1933
3012
  type: "monotone",