@acarmisc/backstage-plugin-litellm 0.12.4 → 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,79 +152,682 @@ var init_api = __esm({
152
152
  }
153
153
  });
154
154
 
155
- // src/components/DashboardHeader.tsx
156
- import React, { useMemo } from "react";
155
+ // src/components/ui.tsx
156
+ import React from "react";
157
157
  import Box from "@mui/material/Box";
158
- import Typography from "@mui/material/Typography";
159
- import LinearProgress from "@mui/material/LinearProgress";
158
+ import ButtonBase from "@mui/material/ButtonBase";
160
159
  import Paper from "@mui/material/Paper";
161
- import Chip from "@mui/material/Chip";
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
+
681
+ // src/components/DashboardHeader.tsx
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";
162
687
  import Button from "@mui/material/Button";
163
- import Stack from "@mui/material/Stack";
164
- import { Add, Key, Warning, Schedule } from "@mui/icons-material";
165
- var DashboardHeader;
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;
166
698
  var init_DashboardHeader = __esm({
167
699
  "src/components/DashboardHeader.tsx"() {
168
700
  "use strict";
169
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
+ };
170
733
  DashboardHeader = ({
171
734
  userInfo,
172
735
  teams,
173
736
  keys,
174
737
  loading,
175
- onGenerateKeyClick
738
+ onGenerateKeyClick,
739
+ tabs
176
740
  }) => {
177
741
  const counts = useMemo(() => {
178
742
  const expired = keys.filter((k) => expiryStatus(k.expires_at) === "expired").length;
179
743
  const expiringSoon = keys.filter((k) => expiryStatus(k.expires_at) === "soon").length;
180
744
  return { total: keys.length, expired, expiringSoon };
181
745
  }, [keys]);
182
- if (loading) {
183
- return /* @__PURE__ */ React.createElement(Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React.createElement(LinearProgress, null));
184
- }
185
746
  const displayName = userInfo.user_email ?? userInfo.email ?? userInfo.user_id;
186
- 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(
187
- Chip,
188
- {
189
- key: team.team_id,
190
- label: team.team_alias || team.team_id,
191
- size: "small",
192
- variant: "outlined",
193
- color: "primary"
194
- }
195
- )))), /* @__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(
196
- Chip,
747
+ return /* @__PURE__ */ React2.createElement(Paper2, { sx: { borderRadius: 2, overflow: "hidden" } }, /* @__PURE__ */ React2.createElement(
748
+ Box2,
197
749
  {
198
- icon: /* @__PURE__ */ React.createElement(Warning, { fontSize: "small" }),
199
- label: `${counts.expired} expired`,
200
- size: "small",
201
- color: counts.expired > 0 ? "error" : "default"
202
- }
203
- ), /* @__PURE__ */ React.createElement(
204
- Chip,
205
- {
206
- icon: /* @__PURE__ */ React.createElement(Schedule, { fontSize: "small" }),
207
- label: `${counts.expiringSoon} expiring soon`,
208
- size: "small",
209
- color: counts.expiringSoon > 0 ? "warning" : "default"
210
- }
211
- )), /* @__PURE__ */ React.createElement(
212
- 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,
213
809
  {
214
- variant: "contained",
215
- color: "primary",
216
- startIcon: /* @__PURE__ */ React.createElement(Add, null),
217
- 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
+ }
218
818
  },
219
- "Generate New Key"
220
- ))));
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)));
221
825
  };
222
826
  }
223
827
  });
224
828
 
225
829
  // src/components/KeysTable.tsx
226
- import React2, { useState, useMemo as useMemo2 } from "react";
227
- import Paper2 from "@mui/material/Paper";
830
+ import React3, { useState, useMemo as useMemo2 } from "react";
228
831
  import Table from "@mui/material/Table";
229
832
  import TableBody from "@mui/material/TableBody";
230
833
  import TableCell from "@mui/material/TableCell";
@@ -233,52 +836,62 @@ import TableHead from "@mui/material/TableHead";
233
836
  import TableRow from "@mui/material/TableRow";
234
837
  import Button2 from "@mui/material/Button";
235
838
  import IconButton from "@mui/material/IconButton";
236
- import Box2 from "@mui/material/Box";
237
- import Typography2 from "@mui/material/Typography";
839
+ import Box3 from "@mui/material/Box";
840
+ import Typography3 from "@mui/material/Typography";
238
841
  import Dialog from "@mui/material/Dialog";
239
842
  import DialogTitle from "@mui/material/DialogTitle";
240
843
  import DialogContent from "@mui/material/DialogContent";
241
844
  import DialogActions from "@mui/material/DialogActions";
242
845
  import TextField from "@mui/material/TextField";
243
- import Chip2 from "@mui/material/Chip";
244
846
  import CircularProgress from "@mui/material/CircularProgress";
245
847
  import Autocomplete from "@mui/material/Autocomplete";
246
- import LinearProgress2 from "@mui/material/LinearProgress";
848
+ import Skeleton2 from "@mui/material/Skeleton";
247
849
  import InputAdornment from "@mui/material/InputAdornment";
248
- import { ContentCopy, Delete, Add as Add2, Edit, Autorenew, Search, Warning as Warning2, Lock, LockOpen } from "@mui/icons-material";
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";
249
852
  function expiryChipLabel(status, expiresAt) {
250
853
  if (status === "expired") return "Expired";
251
854
  if (status === "soon") return `${Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 864e5)}d left`;
252
855
  return formatDate(expiresAt);
253
856
  }
254
- function expiryChipColor(status) {
255
- if (status === "expired") return "error";
857
+ function expiryTone(status) {
858
+ if (status === "expired") return "danger";
256
859
  if (status === "soon") return "warning";
257
- return "default";
860
+ return "neutral";
258
861
  }
259
- function ExpiryChip({ expiresAt }) {
862
+ function ExpiryCell({ expiresAt }) {
260
863
  const status = expiryStatus(expiresAt);
261
- if (!status) return /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, "-");
262
- const label = expiryChipLabel(status, expiresAt);
263
- const color = expiryChipColor(status);
264
- const icon = status === "expired" || status === "soon" ? /* @__PURE__ */ React2.createElement(Warning2, { fontSize: "small" }) : void 0;
265
- 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
+ );
266
878
  }
267
- function budgetColor(pct) {
268
- if (pct >= 100) return "error";
879
+ function budgetTone(pct) {
880
+ if (pct >= 100) return "danger";
269
881
  if (pct >= 80) return "warning";
270
- return "primary";
882
+ return "accent";
271
883
  }
272
884
  function keyBlockIcon(isBlocking, blocked) {
273
- if (isBlocking) return /* @__PURE__ */ React2.createElement(CircularProgress, { size: 18 });
274
- if (blocked) return /* @__PURE__ */ React2.createElement(LockOpen, { fontSize: "small" });
275
- return /* @__PURE__ */ React2.createElement(Lock, { fontSize: "small" });
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" });
276
888
  }
277
889
  function BudgetCell({ spend, maxBudget }) {
278
- 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
+ }
279
893
  const pct = Math.min(100, spend / maxBudget * 100);
280
- const color = budgetColor(pct);
281
- 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 }));
282
895
  }
283
896
  function fmtCost(perToken) {
284
897
  if (!perToken) return null;
@@ -305,6 +918,7 @@ var init_KeysTable = __esm({
305
918
  "src/components/KeysTable.tsx"() {
306
919
  "use strict";
307
920
  init_api();
921
+ init_ui();
308
922
  maskKey = (key) => {
309
923
  if (key.length <= 8) return "***";
310
924
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
@@ -436,97 +1050,179 @@ var init_KeysTable = __esm({
436
1050
  const inCost = fmtCost(m.input_cost_per_token);
437
1051
  const outCost = fmtCost(m.output_cost_per_token);
438
1052
  const ctx = formatContextWindow(m.max_input_tokens, m.max_output_tokens);
439
- 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"));
440
1054
  };
441
1055
  const renderTableBody = () => {
442
1056
  if (loading) {
443
- return /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 })));
1057
+ return /* @__PURE__ */ React3.createElement(TableRow, null, /* @__PURE__ */ React3.createElement(TableCell, { colSpan: 8, sx: { py: 2 } }, /* @__PURE__ */ React3.createElement(Skeleton2, { variant: "rounded", height: 140 })));
444
1058
  }
445
1059
  if (filteredKeys.length === 0) {
446
- return /* @__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(
447
- Button2,
1060
+ return /* @__PURE__ */ React3.createElement(TableRow, null, /* @__PURE__ */ React3.createElement(TableCell, { colSpan: 8 }, filterText ? /* @__PURE__ */ React3.createElement(
1061
+ EmptyState,
448
1062
  {
449
- variant: "contained",
450
- color: "primary",
451
- size: "small",
452
- startIcon: /* @__PURE__ */ React2.createElement(Add2, null),
453
- onClick: onGenerateKeyClick
454
- },
455
- "Generate Your First Key"
456
- ))));
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
+ )));
457
1086
  }
458
1087
  return filteredKeys.map((key) => {
459
1088
  const keyId = key.token ?? key.key;
460
1089
  const isBlocking = blockingKeyId === keyId;
461
- 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(
462
- Typography2,
1090
+ const keyModels = key.models ?? [];
1091
+ return /* @__PURE__ */ React3.createElement(
1092
+ TableRow,
463
1093
  {
464
- variant: "body2",
465
- component: "code",
466
- color: "text.secondary",
467
- title: keyId,
468
- sx: {
469
- fontFamily: "monospace",
470
- backgroundColor: "background.default",
471
- px: 1,
472
- py: 0.5,
473
- borderRadius: 1
474
- }
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
475
1101
  },
476
- shortKeyId(keyId)
477
- ), /* @__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(
478
- IconButton,
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,
1176
+ {
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
+ }
1191
+ }
1192
+ ), expiredKeys.length > 0 && /* @__PURE__ */ React3.createElement(
1193
+ Button2,
479
1194
  {
480
- onClick: () => handleToggleBlock(key),
481
- disabled: isBlocking,
482
- color: key.blocked ? "warning" : "default",
483
- title: key.blocked ? "Unblock key" : "Block key \u2014 suspends without revoking"
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
+ })
484
1208
  },
485
- keyBlockIcon(isBlocking, key.blocked)
486
- ), /* @__PURE__ */ React2.createElement(
487
- IconButton,
1209
+ "Prune expired (",
1210
+ expiredKeys.length,
1211
+ ")"
1212
+ ), /* @__PURE__ */ React3.createElement(
1213
+ Button2,
488
1214
  {
489
- color: "error",
490
- onClick: () => setDeleteConfirmId(keyId),
491
- title: "Revoke key"
1215
+ variant: "contained",
1216
+ color: "primary",
1217
+ disableElevation: true,
1218
+ startIcon: /* @__PURE__ */ React3.createElement(Add2, null),
1219
+ onClick: onGenerateKeyClick
492
1220
  },
493
- /* @__PURE__ */ React2.createElement(Delete, null)
494
- )));
495
- });
496
- };
497
- 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(
498
- TextField,
499
- {
500
- size: "small",
501
- placeholder: "Filter by alias or model\u2026",
502
- value: filterText,
503
- onChange: (e) => setFilterText(e.target.value),
504
- sx: { minWidth: 240 },
505
- InputProps: {
506
- startAdornment: /* @__PURE__ */ React2.createElement(InputAdornment, { position: "start" }, /* @__PURE__ */ React2.createElement(Search, { fontSize: "small" }))
507
- }
508
- }
509
- ), expiredKeys.length > 0 && /* @__PURE__ */ React2.createElement(
510
- Button2,
511
- {
512
- variant: "outlined",
513
- color: "error",
514
- startIcon: /* @__PURE__ */ React2.createElement(Autorenew, null),
515
- onClick: () => setPruneConfirmCount(expiredKeys.length)
516
- },
517
- "Prune expired (",
518
- expiredKeys.length,
519
- ")"
520
- ), /* @__PURE__ */ React2.createElement(
521
- Button2,
522
- {
523
- variant: "contained",
524
- color: "primary",
525
- startIcon: /* @__PURE__ */ React2.createElement(Add2, null),
526
- onClick: onGenerateKeyClick
1221
+ "Generate New Key"
1222
+ ))
527
1223
  },
528
- "Generate New Key"
529
- ))), /* @__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, renderTableBody())))), /* @__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(
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(
530
1226
  TextField,
531
1227
  {
532
1228
  label: "Alias",
@@ -534,7 +1230,7 @@ var init_KeysTable = __esm({
534
1230
  onChange: (e) => setEditForm({ ...editForm, key_alias: e.target.value }),
535
1231
  fullWidth: true
536
1232
  }
537
- ), models.length > 0 && /* @__PURE__ */ React2.createElement(
1233
+ ), models.length > 0 && /* @__PURE__ */ React3.createElement(
538
1234
  Autocomplete,
539
1235
  {
540
1236
  multiple: true,
@@ -543,10 +1239,10 @@ var init_KeysTable = __esm({
543
1239
  getOptionLabel: (m) => m.model_name,
544
1240
  value: editSelectedModels,
545
1241
  onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
546
- renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
547
- 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 })
548
1244
  }
549
- ), /* @__PURE__ */ React2.createElement(
1245
+ ), /* @__PURE__ */ React3.createElement(
550
1246
  TextField,
551
1247
  {
552
1248
  label: "Max Budget (USD)",
@@ -555,7 +1251,7 @@ var init_KeysTable = __esm({
555
1251
  onChange: (e) => setEditForm({ ...editForm, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
556
1252
  fullWidth: true
557
1253
  }
558
- ), /* @__PURE__ */ React2.createElement(
1254
+ ), /* @__PURE__ */ React3.createElement(
559
1255
  TextField,
560
1256
  {
561
1257
  label: "TPM Limit",
@@ -565,7 +1261,7 @@ var init_KeysTable = __esm({
565
1261
  helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
566
1262
  fullWidth: true
567
1263
  }
568
- ), /* @__PURE__ */ React2.createElement(
1264
+ ), /* @__PURE__ */ React3.createElement(
569
1265
  TextField,
570
1266
  {
571
1267
  label: "RPM Limit",
@@ -575,7 +1271,7 @@ var init_KeysTable = __esm({
575
1271
  helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
576
1272
  fullWidth: true
577
1273
  }
578
- ), /* @__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(
579
1275
  Button2,
580
1276
  {
581
1277
  size: "small",
@@ -584,7 +1280,7 @@ var init_KeysTable = __esm({
584
1280
  onClick: () => setResetSpendConfirm(true)
585
1281
  },
586
1282
  "Reset Spend to $0"
587
- ) : /* @__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(
588
1284
  Button2,
589
1285
  {
590
1286
  size: "small",
@@ -593,8 +1289,8 @@ var init_KeysTable = __esm({
593
1289
  disabled: resetSpendSubmitting,
594
1290
  onClick: handleResetSpend
595
1291
  },
596
- resetSpendSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 16 }) : "Confirm"
597
- ), /* @__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(
598
1294
  Button2,
599
1295
  {
600
1296
  onClick: handleConfirmDelete,
@@ -602,8 +1298,8 @@ var init_KeysTable = __esm({
602
1298
  color: "error",
603
1299
  disabled: deleteSubmitting
604
1300
  },
605
- deleteSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Revoke"
606
- ))), /* @__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(
607
1303
  Button2,
608
1304
  {
609
1305
  onClick: handlePruneExpired,
@@ -611,7 +1307,7 @@ var init_KeysTable = __esm({
611
1307
  color: "error",
612
1308
  disabled: pruneSubmitting
613
1309
  },
614
- pruneSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Prune"
1310
+ pruneSubmitting ? /* @__PURE__ */ React3.createElement(CircularProgress, { size: 20 }) : "Prune"
615
1311
  ))));
616
1312
  };
617
1313
  }
@@ -632,9 +1328,9 @@ var init_format = __esm({
632
1328
  });
633
1329
 
634
1330
  // src/components/GenerateKeyDialog.tsx
635
- import React3, { useState as useState2, useEffect, useMemo as useMemo3 } from "react";
636
- import Box3 from "@mui/material/Box";
637
- import Typography3 from "@mui/material/Typography";
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";
638
1334
  import Dialog2 from "@mui/material/Dialog";
639
1335
  import DialogTitle2 from "@mui/material/DialogTitle";
640
1336
  import DialogContent2 from "@mui/material/DialogContent";
@@ -736,14 +1432,14 @@ var init_GenerateKeyDialog = __esm({
736
1432
  SnippetTabs = ({ snippets, model, onCopy }) => {
737
1433
  const [tab, setTab] = useState2("curl");
738
1434
  const code = tab === "curl" ? snippets.curl : snippets.openai;
739
- 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(
740
- Box3,
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,
741
1437
  {
742
1438
  position: "relative",
743
1439
  p: 1.5,
744
1440
  sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
745
1441
  },
746
- /* @__PURE__ */ React3.createElement(
1442
+ /* @__PURE__ */ React4.createElement(
747
1443
  IconButton2,
748
1444
  {
749
1445
  size: "small",
@@ -751,10 +1447,10 @@ var init_GenerateKeyDialog = __esm({
751
1447
  title: "Copy snippet",
752
1448
  sx: { position: "absolute", top: 4, right: 4 }
753
1449
  },
754
- /* @__PURE__ */ React3.createElement(ContentCopy2, { fontSize: "small" })
1450
+ /* @__PURE__ */ React4.createElement(ContentCopy2, { fontSize: "small" })
755
1451
  ),
756
- /* @__PURE__ */ React3.createElement(
757
- Typography3,
1452
+ /* @__PURE__ */ React4.createElement(
1453
+ Typography4,
758
1454
  {
759
1455
  component: "pre",
760
1456
  sx: {
@@ -768,7 +1464,7 @@ var init_GenerateKeyDialog = __esm({
768
1464
  },
769
1465
  code
770
1466
  ),
771
- 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.")
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.")
772
1468
  ));
773
1469
  };
774
1470
  GenerateKeyDialog = ({
@@ -864,11 +1560,11 @@ var init_GenerateKeyDialog = __esm({
864
1560
  const inCost = fmtCost2(m.input_cost_per_token);
865
1561
  const outCost = fmtCost2(m.output_cost_per_token);
866
1562
  const ctx = formatContextWindow2(m.max_input_tokens, m.max_output_tokens);
867
- 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"));
868
1564
  };
869
1565
  const renderTeamField = () => {
870
1566
  if (teams.length > 0) {
871
- return /* @__PURE__ */ React3.createElement(
1567
+ return /* @__PURE__ */ React4.createElement(
872
1568
  Autocomplete2,
873
1569
  {
874
1570
  options: teams,
@@ -879,7 +1575,7 @@ var init_GenerateKeyDialog = __esm({
879
1575
  const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
880
1576
  setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
881
1577
  },
882
- renderInput: (params) => /* @__PURE__ */ React3.createElement(
1578
+ renderInput: (params) => /* @__PURE__ */ React4.createElement(
883
1579
  TextField2,
884
1580
  {
885
1581
  ...params,
@@ -894,12 +1590,12 @@ var init_GenerateKeyDialog = __esm({
894
1590
  );
895
1591
  }
896
1592
  if (teamRequired) {
897
- return /* @__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.");
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.");
898
1594
  }
899
1595
  return null;
900
1596
  };
901
- 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(
902
- 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,
903
1599
  {
904
1600
  display: "flex",
905
1601
  alignItems: "center",
@@ -913,8 +1609,8 @@ var init_GenerateKeyDialog = __esm({
913
1609
  borderRadius: 1
914
1610
  }
915
1611
  },
916
- /* @__PURE__ */ React3.createElement(
917
- Typography3,
1612
+ /* @__PURE__ */ React4.createElement(
1613
+ Typography4,
918
1614
  {
919
1615
  component: "code",
920
1616
  color: "text.primary",
@@ -922,8 +1618,8 @@ var init_GenerateKeyDialog = __esm({
922
1618
  },
923
1619
  newKeyValue
924
1620
  ),
925
- /* @__PURE__ */ React3.createElement(IconButton2, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React3.createElement(ContentCopy2, null))
926
- ), 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(
927
1623
  TextField2,
928
1624
  {
929
1625
  label: "Alias",
@@ -938,7 +1634,7 @@ var init_GenerateKeyDialog = __esm({
938
1634
  required: true,
939
1635
  fullWidth: true
940
1636
  }
941
- ), /* @__PURE__ */ React3.createElement(
1637
+ ), /* @__PURE__ */ React4.createElement(
942
1638
  TextField2,
943
1639
  {
944
1640
  select: true,
@@ -947,12 +1643,12 @@ var init_GenerateKeyDialog = __esm({
947
1643
  onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
948
1644
  fullWidth: true
949
1645
  },
950
- /* @__PURE__ */ React3.createElement(MenuItem, { value: "1d" }, "1 Day"),
951
- /* @__PURE__ */ React3.createElement(MenuItem, { value: "7d" }, "7 Days"),
952
- /* @__PURE__ */ React3.createElement(MenuItem, { value: "30d" }, "30 Days"),
953
- /* @__PURE__ */ React3.createElement(MenuItem, { value: "90d" }, "90 Days"),
954
- /* @__PURE__ */ React3.createElement(MenuItem, { value: "1y" }, "1 Year")
955
- ), renderTeamField(), 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(
956
1652
  Autocomplete2,
957
1653
  {
958
1654
  multiple: true,
@@ -961,8 +1657,8 @@ var init_GenerateKeyDialog = __esm({
961
1657
  getOptionLabel: (m) => m.model_name,
962
1658
  value: selectedModels,
963
1659
  onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
964
- renderOption: (props, m) => /* @__PURE__ */ React3.createElement("li", { ...props }, modelOption(m)),
965
- renderInput: (params) => /* @__PURE__ */ React3.createElement(
1660
+ renderOption: (props, m) => /* @__PURE__ */ React4.createElement("li", { ...props }, modelOption(m)),
1661
+ renderInput: (params) => /* @__PURE__ */ React4.createElement(
966
1662
  TextField2,
967
1663
  {
968
1664
  ...params,
@@ -972,10 +1668,10 @@ var init_GenerateKeyDialog = __esm({
972
1668
  }
973
1669
  )
974
1670
  }
975
- ), allowUnlimitedBudget && /* @__PURE__ */ React3.createElement(
1671
+ ), allowUnlimitedBudget && /* @__PURE__ */ React4.createElement(
976
1672
  FormControlLabel,
977
1673
  {
978
- control: /* @__PURE__ */ React3.createElement(
1674
+ control: /* @__PURE__ */ React4.createElement(
979
1675
  Checkbox,
980
1676
  {
981
1677
  checked: unlimitedBudget,
@@ -991,7 +1687,7 @@ var init_GenerateKeyDialog = __esm({
991
1687
  ),
992
1688
  label: "Unlimited budget"
993
1689
  }
994
- ), /* @__PURE__ */ React3.createElement(
1690
+ ), /* @__PURE__ */ React4.createElement(
995
1691
  TextField2,
996
1692
  {
997
1693
  label: "Max Budget (USD)",
@@ -1004,7 +1700,7 @@ var init_GenerateKeyDialog = __esm({
1004
1700
  required: true,
1005
1701
  fullWidth: true
1006
1702
  }
1007
- ), /* @__PURE__ */ React3.createElement(
1703
+ ), /* @__PURE__ */ React4.createElement(
1008
1704
  TextField2,
1009
1705
  {
1010
1706
  label: "TPM Limit",
@@ -1014,7 +1710,7 @@ var init_GenerateKeyDialog = __esm({
1014
1710
  helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
1015
1711
  fullWidth: true
1016
1712
  }
1017
- ), /* @__PURE__ */ React3.createElement(
1713
+ ), /* @__PURE__ */ React4.createElement(
1018
1714
  TextField2,
1019
1715
  {
1020
1716
  label: "RPM Limit",
@@ -1024,7 +1720,7 @@ var init_GenerateKeyDialog = __esm({
1024
1720
  helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
1025
1721
  fullWidth: true
1026
1722
  }
1027
- ))), /* @__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(
1028
1724
  Button3,
1029
1725
  {
1030
1726
  onClick: handleGenerate,
@@ -1032,34 +1728,27 @@ var init_GenerateKeyDialog = __esm({
1032
1728
  color: "primary",
1033
1729
  disabled: !canGenerate
1034
1730
  },
1035
- submitting ? /* @__PURE__ */ React3.createElement(CircularProgress2, { size: 24 }) : "Generate"
1731
+ submitting ? /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 }) : "Generate"
1036
1732
  ))));
1037
1733
  };
1038
1734
  }
1039
1735
  });
1040
1736
 
1041
1737
  // src/components/UsageStats.tsx
1042
- import React4, { useMemo as useMemo4, useState as useState3 } from "react";
1738
+ import React5, { useMemo as useMemo4, useState as useState3 } from "react";
1043
1739
  import Paper3 from "@mui/material/Paper";
1044
- import Box4 from "@mui/material/Box";
1045
- import Typography4 from "@mui/material/Typography";
1046
- import FormControl from "@mui/material/FormControl";
1047
- import InputLabel from "@mui/material/InputLabel";
1048
- import Select from "@mui/material/Select";
1740
+ import Box5 from "@mui/material/Box";
1741
+ import Typography5 from "@mui/material/Typography";
1742
+ import TextField3 from "@mui/material/TextField";
1049
1743
  import MenuItem2 from "@mui/material/MenuItem";
1050
1744
  import Grid from "@mui/material/Grid";
1051
- import Tabs2 from "@mui/material/Tabs";
1052
- import Tab2 from "@mui/material/Tab";
1053
1745
  import Table2 from "@mui/material/Table";
1054
1746
  import TableBody2 from "@mui/material/TableBody";
1055
1747
  import TableCell2 from "@mui/material/TableCell";
1056
1748
  import TableContainer2 from "@mui/material/TableContainer";
1057
1749
  import TableHead2 from "@mui/material/TableHead";
1058
1750
  import TableRow2 from "@mui/material/TableRow";
1059
- import Chip3 from "@mui/material/Chip";
1060
- import LinearProgress3 from "@mui/material/LinearProgress";
1061
- import Skeleton from "@mui/material/Skeleton";
1062
- import { useTheme } from "@mui/material/styles";
1751
+ import Skeleton3 from "@mui/material/Skeleton";
1063
1752
  import {
1064
1753
  AreaChart,
1065
1754
  Area,
@@ -1073,48 +1762,46 @@ import {
1073
1762
  Tooltip,
1074
1763
  ResponsiveContainer,
1075
1764
  Legend,
1076
- ReferenceLine
1765
+ ReferenceLine,
1766
+ Cell
1077
1767
  } from "recharts";
1078
- function modelColor(model) {
1079
- let h = 5381;
1080
- for (let i = 0; i < model.length; i++) h = (h << 5) + h + model.charCodeAt(i) >>> 0;
1081
- 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";
1082
1772
  }
1083
- var TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtPct, KpiCard, ChartSkeleton, EmptyChart, ChartOrFallback, UsageStats;
1773
+ var TOP_N_MODELS, PERIOD_LS_KEY, PRESET_LABELS, fmtPct, ChartSkeleton, ChartOrFallback, SuccessRateCell, UsageStats;
1084
1774
  var init_UsageStats = __esm({
1085
1775
  "src/components/UsageStats.tsx"() {
1086
1776
  "use strict";
1087
1777
  init_format();
1778
+ init_ui();
1088
1779
  TOP_N_MODELS = 6;
1089
1780
  PERIOD_LS_KEY = "litellm_usage_period";
1090
- MODEL_COLORS = [
1091
- "#8884d8",
1092
- "#82ca9d",
1093
- "#ffc658",
1094
- "#ff7300",
1095
- "#0088fe",
1096
- "#00C49F",
1097
- "#FFBB28",
1098
- "#FF8042",
1099
- "#a4de6c",
1100
- "#d0ed57"
1101
- ];
1781
+ PRESET_LABELS = {
1782
+ today: "Today",
1783
+ "24h": "Last 24 hours",
1784
+ "7d": "Last 7 days",
1785
+ "30d": "Last 30 days"
1786
+ };
1102
1787
  fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
1103
- 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);
1104
- ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */ React4.createElement(Skeleton, { variant: "rectangular", height, sx: { borderRadius: 1 } });
1105
- EmptyChart = ({
1106
- height = 260,
1107
- message = "No data for this period"
1108
- }) => /* @__PURE__ */ React4.createElement(Box4, { height, display: "flex", alignItems: "center", justifyContent: "center" }, /* @__PURE__ */ React4.createElement(Typography4, { color: "text.secondary", variant: "body2" }, message));
1109
- ChartOrFallback = ({
1110
- loading,
1111
- empty,
1112
- height,
1113
- children
1114
- }) => {
1115
- if (loading) return /* @__PURE__ */ React4.createElement(ChartSkeleton, { height });
1116
- if (empty) return /* @__PURE__ */ React4.createElement(EmptyChart, { height });
1117
- return /* @__PURE__ */ React4.createElement(React4.Fragment, null, children);
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
+ ));
1118
1805
  };
1119
1806
  UsageStats = ({
1120
1807
  usage,
@@ -1124,14 +1811,9 @@ var init_UsageStats = __esm({
1124
1811
  loading,
1125
1812
  userInfo
1126
1813
  }) => {
1127
- const theme = useTheme();
1814
+ const chart = useChartTheme();
1128
1815
  const [selectedModel, setSelectedModel] = useState3("all");
1129
1816
  const [tab, setTab] = useState3("costs");
1130
- const gridStroke = theme.palette.divider;
1131
- const tickFill = theme.palette.text.secondary;
1132
- const tickStyle = { fontSize: 12, fill: tickFill };
1133
- const tickStyleSmall = { fontSize: 11, fill: tickFill };
1134
- const tickStyleTiny = { fontSize: 10, fill: tickFill };
1135
1817
  const selectedPreset = useMemo4(() => {
1136
1818
  if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
1137
1819
  const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
@@ -1239,90 +1921,225 @@ var init_UsageStats = __esm({
1239
1921
  const overallSuccessRate = totalRequests > 0 ? (usage?.successful_requests ?? 0) / totalRequests : 0;
1240
1922
  const maxBudget = userInfo?.max_budget ?? 0;
1241
1923
  const totalCumSpend = cumulativeData[cumulativeData.length - 1]?.cumulative ?? 0;
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 = [
1927
+ {
1928
+ label: "Total spend",
1929
+ value: fmtUsd(usage?.total_spend ?? 0),
1930
+ hint: maxBudget > 0 ? `of ${fmtUsd(maxBudget)} budget` : void 0,
1931
+ tone: "accent"
1932
+ },
1933
+ {
1934
+ label: "Requests",
1935
+ value: fmtInt(totalRequests),
1936
+ hint: `${fmtInt(usage?.failed_requests ?? 0)} failed`,
1937
+ tone: "info"
1938
+ },
1939
+ {
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
+ },
1945
+ {
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"
1950
+ }
1951
+ ];
1242
1952
  const renderKeyRows = () => {
1243
1953
  if (loading) {
1244
- return /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React4.createElement(LinearProgress3, null)));
1954
+ return /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, { colSpan: 7, sx: { py: 3 } }, /* @__PURE__ */ React5.createElement(Skeleton3, { variant: "rounded", height: 80 })));
1245
1955
  }
1246
1956
  if (keyRows.length === 0) {
1247
- return /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 7, align: "center" }, "No key activity"));
1957
+ return /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React5.createElement(EmptyState, { message: "No key activity in this period" })));
1248
1958
  }
1249
- return [...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(
1250
- LinearProgress3,
1251
- {
1252
- variant: "determinate",
1253
- value: r.successRate * 100,
1254
- sx: { flex: 1, height: 6, borderRadius: 3 }
1255
- }
1256
- ), /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")));
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 }))));
1257
1960
  };
1258
1961
  const renderModelRows = () => {
1259
1962
  if (loading) {
1260
- return /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React4.createElement(LinearProgress3, null)));
1963
+ return /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, { colSpan: 8, sx: { py: 3 } }, /* @__PURE__ */ React5.createElement(Skeleton3, { variant: "rounded", height: 80 })));
1261
1964
  }
1262
1965
  if (modelRows.length === 0) {
1263
- return /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 8, align: "center" }, "No model activity"));
1966
+ return /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React5.createElement(EmptyState, { message: "No model activity in this period" })));
1264
1967
  }
1265
- return [...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(
1266
- LinearProgress3,
1267
- {
1268
- variant: "determinate",
1269
- value: r.successRate * 100,
1270
- sx: { flex: 1, height: 6, borderRadius: 3 }
1271
- }
1272
- ), /* @__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 }))));
1273
1969
  };
1274
- 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(
1275
- Select,
1970
+ const periodSelect = /* @__PURE__ */ React5.createElement(
1971
+ TextField3,
1276
1972
  {
1277
- value: selectedPreset,
1973
+ select: true,
1974
+ size: "small",
1278
1975
  label: "Period",
1279
- onChange: (e) => handlePresetChange(e.target.value)
1976
+ value: selectedPreset,
1977
+ onChange: (e) => handlePresetChange(e.target.value),
1978
+ sx: { minWidth: 160 }
1280
1979
  },
1281
- /* @__PURE__ */ React4.createElement(MenuItem2, { value: "today" }, "Today"),
1282
- /* @__PURE__ */ React4.createElement(MenuItem2, { value: "24h" }, "Last 24h"),
1283
- /* @__PURE__ */ React4.createElement(MenuItem2, { value: "7d" }, "Last 7 days"),
1284
- /* @__PURE__ */ React4.createElement(MenuItem2, { value: "30d" }, "Last 30 days")
1285
- )), tab === "models" && /* @__PURE__ */ React4.createElement(FormControl, { size: "small", sx: { minWidth: 180 } }, /* @__PURE__ */ React4.createElement(InputLabel, null, "Model"), /* @__PURE__ */ React4.createElement(
1286
- Select,
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,
1287
1984
  {
1288
- value: selectedModel,
1289
- label: "Model",
1290
- onChange: (e) => setSelectedModel(e.target.value)
1985
+ title: "Usage Analytics",
1986
+ subtitle: `${PRESET_LABELS[selectedPreset]} \xB7 ${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
1987
+ actions: periodSelect
1291
1988
  },
1292
- /* @__PURE__ */ React4.createElement(MenuItem2, { value: "all" }, "All Models"),
1293
- models.map((m) => /* @__PURE__ */ React4.createElement(MenuItem2, { key: m.model_name, value: m.model_name }, m.model_name))
1294
- )))), /* @__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(
1295
- KpiCard,
1296
- {
1297
- label: "Total Tokens",
1298
- value: fmtInt(usage?.total_tokens ?? 0),
1299
- hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
1300
- }
1301
- ))), /* @__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"), /* @__PURE__ */ React4.createElement(ChartOrFallback, { loading, empty: modelSpendByDate.length === 0 }, /* @__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(
1302
- Area,
1303
- {
1304
- key: m,
1305
- type: "monotone",
1306
- dataKey: m,
1307
- stackId: "spend",
1308
- stroke: modelColor(m),
1309
- fill: modelColor(m),
1310
- fillOpacity: 0.6
1311
- }
1312
- ))))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Token Usage"), /* @__PURE__ */ React4.createElement(ChartOrFallback, { loading, empty: dailyData.length === 0 }, /* @__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"), /* @__PURE__ */ React4.createElement(ChartOrFallback, { loading, empty: dailyData.length === 0 }, /* @__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"), /* @__PURE__ */ React4.createElement(ChartOrFallback, { loading, empty: successRateData.length === 0 }, /* @__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")), /* @__PURE__ */ React4.createElement(ChartOrFallback, { loading, empty: cumulativeData.length === 0, 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"), /* @__PURE__ */ React4.createElement(ChartOrFallback, { loading, empty: topModelSpendBars.length === 0 }, /* @__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"), /* @__PURE__ */ React4.createElement(ChartOrFallback, { loading, empty: modelRows.length === 0 }, /* @__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, renderModelRows()))))), 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, renderKeyRows()))))));
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
+ );
1313
2132
  };
1314
2133
  }
1315
2134
  });
1316
2135
 
1317
2136
  // src/components/TeamUsage.tsx
1318
- import React5, { useState as useState4 } from "react";
2137
+ import React6, { useState as useState4 } from "react";
1319
2138
  import Paper4 from "@mui/material/Paper";
1320
- import Box5 from "@mui/material/Box";
1321
- import Typography5 from "@mui/material/Typography";
1322
- import LinearProgress4 from "@mui/material/LinearProgress";
1323
- import Chip4 from "@mui/material/Chip";
1324
- import Divider from "@mui/material/Divider";
1325
- import Stack2 from "@mui/material/Stack";
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";
1326
2143
  import Table3 from "@mui/material/Table";
1327
2144
  import TableBody3 from "@mui/material/TableBody";
1328
2145
  import TableCell3 from "@mui/material/TableCell";
@@ -1332,8 +2149,8 @@ import TableContainer3 from "@mui/material/TableContainer";
1332
2149
  import Collapse from "@mui/material/Collapse";
1333
2150
  import IconButton3 from "@mui/material/IconButton";
1334
2151
  import CircularProgress3 from "@mui/material/CircularProgress";
1335
- import { useTheme as useTheme2 } from "@mui/material/styles";
1336
- import { ExpandMore, ExpandLess, Group, Speed, Memory } from "@mui/icons-material";
2152
+ import { alpha as alpha4 } from "@mui/material/styles";
2153
+ import { ExpandMore, Group } from "@mui/icons-material";
1337
2154
  import {
1338
2155
  AreaChart as AreaChart2,
1339
2156
  Area as Area2,
@@ -1343,21 +2160,20 @@ import {
1343
2160
  Tooltip as Tooltip2,
1344
2161
  ResponsiveContainer as ResponsiveContainer2
1345
2162
  } from "recharts";
1346
- function budgetBarColor(isOver, isNear) {
1347
- if (isOver) return "error";
2163
+ function budgetTone2(isOver, isNear) {
2164
+ if (isOver) return "danger";
1348
2165
  if (isNear) return "warning";
1349
- return "primary";
2166
+ return "accent";
1350
2167
  }
1351
- var Stat, TeamCard, TeamUsage;
2168
+ var fmtUsd2, TeamCard, TeamUsage;
1352
2169
  var init_TeamUsage = __esm({
1353
2170
  "src/components/TeamUsage.tsx"() {
1354
2171
  "use strict";
1355
- 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)}`;
1356
2174
  TeamCard = ({ team, usage, usageLoading }) => {
1357
2175
  const [expanded, setExpanded] = useState4(false);
1358
- const theme = useTheme2();
1359
- const gridStroke = theme.palette.divider;
1360
- const tickFill = theme.palette.text.secondary;
2176
+ const chart = useChartTheme();
1361
2177
  const budget = team.max_budget ?? 0;
1362
2178
  const spend = team.spend ?? 0;
1363
2179
  const budgetPct = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
@@ -1366,73 +2182,131 @@ var init_TeamUsage = __esm({
1366
2182
  const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
1367
2183
  const renderDailySpendSection = () => {
1368
2184
  if (usageLoading) {
1369
- return /* @__PURE__ */ React5.createElement(Box5, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React5.createElement(CircularProgress3, { size: 24 }));
1370
- }
1371
- if (dailyData.length > 0) {
1372
- return /* @__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 } }));
2185
+ return /* @__PURE__ */ React6.createElement(Box6, { display: "flex", justifyContent: "center", py: 3 }, /* @__PURE__ */ React6.createElement(CircularProgress3, { size: 22 }));
1373
2186
  }
1374
- return null;
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
+ )))));
1375
2214
  };
1376
- 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(
1377
- Stat,
1378
- {
1379
- label: "Members",
1380
- value: team.members_with_roles?.length ? String(team.members_with_roles.length) : "\u2014"
1381
- }
1382
- ), /* @__PURE__ */ React5.createElement(
1383
- Stat,
1384
- {
1385
- label: "Models",
1386
- value: team.models?.length ? String(team.models.length) : "All"
1387
- }
1388
- ), /* @__PURE__ */ React5.createElement(
1389
- Stat,
1390
- {
1391
- label: "Budget",
1392
- value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited"
1393
- }
1394
- ), /* @__PURE__ */ React5.createElement(
1395
- Stat,
1396
- {
1397
- label: "Spend",
1398
- value: `$${spend.toFixed(2)}`
1399
- }
1400
- ), /* @__PURE__ */ React5.createElement(
1401
- Stat,
2215
+ const sectionLabel = (label) => /* @__PURE__ */ React6.createElement(
2216
+ Typography6,
1402
2217
  {
1403
- label: "TPM",
1404
- value: typeof team.tpm_limit === "number" && team.tpm_limit > 0 ? String(team.tpm_limit) : "\u2014"
1405
- }
1406
- ), /* @__PURE__ */ React5.createElement(
1407
- Stat,
1408
- {
1409
- label: "RPM",
1410
- value: typeof team.rpm_limit === "number" && team.rpm_limit > 0 ? String(team.rpm_limit) : "\u2014"
1411
- }
1412
- )), 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(
1413
- 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,
1414
2226
  {
1415
- variant: "determinate",
1416
- value: budgetPct,
1417
- color: budgetBarColor(isOver, isNear),
1418
- sx: { height: 6, borderRadius: 1 }
1419
- }
1420
- )), /* @__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: typeof team.tpm_limit === "number" && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ React5.createElement(Stat, { label: "RPM limit", value: typeof team.rpm_limit === "number" && 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 } }), renderDailySpendSection(), /* @__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(
1421
- 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,
1422
2242
  {
1423
2243
  variant: "caption",
1424
2244
  color: "text.secondary",
1425
- sx: { fontFamily: "monospace" }
2245
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 }
1426
2246
  },
1427
- m.user_id
1428
- )) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", sx: { fontFamily: "monospace" } }, m.user_id)), /* @__PURE__ */ React5.createElement(TableCell3, null, /* @__PURE__ */ React5.createElement(
1429
- 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,
1430
2250
  {
1431
- label: m.role,
1432
2251
  size: "small",
1433
- color: m.role === "admin" ? "primary" : "default"
1434
- }
1435
- ))))))) : /* @__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."))));
1436
2310
  };
1437
2311
  TeamUsage = ({
1438
2312
  teams,
@@ -1441,12 +2315,18 @@ var init_TeamUsage = __esm({
1441
2315
  getTeamUsageLoading
1442
2316
  }) => {
1443
2317
  if (loading) {
1444
- 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 }));
1445
2319
  }
1446
2320
  if (!teams.length) {
1447
- 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
+ ));
1448
2328
  }
1449
- 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(
1450
2330
  TeamCard,
1451
2331
  {
1452
2332
  key: team.team_id,
@@ -1454,16 +2334,15 @@ var init_TeamUsage = __esm({
1454
2334
  usage: getTeamUsage(team.team_id),
1455
2335
  usageLoading: getTeamUsageLoading(team.team_id)
1456
2336
  }
1457
- )));
2337
+ ))));
1458
2338
  };
1459
2339
  }
1460
2340
  });
1461
2341
 
1462
2342
  // src/components/AuditLog.tsx
1463
- import React6, { useState as useState5, useCallback } from "react";
1464
- import Paper5 from "@mui/material/Paper";
1465
- import Box6 from "@mui/material/Box";
1466
- import Typography6 from "@mui/material/Typography";
2343
+ import React7, { useState as useState5, useCallback } from "react";
2344
+ import Box7 from "@mui/material/Box";
2345
+ import Typography7 from "@mui/material/Typography";
1467
2346
  import Table4 from "@mui/material/Table";
1468
2347
  import TableBody4 from "@mui/material/TableBody";
1469
2348
  import TableCell4 from "@mui/material/TableCell";
@@ -1471,22 +2350,26 @@ import TableContainer4 from "@mui/material/TableContainer";
1471
2350
  import TableHead4 from "@mui/material/TableHead";
1472
2351
  import TableRow4 from "@mui/material/TableRow";
1473
2352
  import TablePagination from "@mui/material/TablePagination";
1474
- import TextField3 from "@mui/material/TextField";
2353
+ import TextField4 from "@mui/material/TextField";
1475
2354
  import MenuItem3 from "@mui/material/MenuItem";
1476
- import Chip5 from "@mui/material/Chip";
1477
- import CircularProgress4 from "@mui/material/CircularProgress";
2355
+ import Skeleton5 from "@mui/material/Skeleton";
1478
2356
  import Collapse2 from "@mui/material/Collapse";
1479
2357
  import IconButton4 from "@mui/material/IconButton";
1480
2358
  import Alert2 from "@mui/material/Alert";
1481
- import { KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
2359
+ import { alpha as alpha5 } from "@mui/material/styles";
2360
+ import { KeyboardArrowDown } from "@mui/icons-material";
1482
2361
  import { useAsync } from "react-use";
1483
- function actionColor(action) {
1484
- if (!action) return "default";
1485
- for (const [key, color] of Object.entries(ACTION_COLORS)) {
1486
- 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;
1487
2366
  }
1488
2367
  return "info";
1489
2368
  }
2369
+ function prettyTableName(name) {
2370
+ if (!name) return "\u2014";
2371
+ return TABLE_LABELS[name] ?? name;
2372
+ }
1490
2373
  function formatDateTime(iso) {
1491
2374
  try {
1492
2375
  return new Date(iso).toLocaleString();
@@ -1496,28 +2379,99 @@ function formatDateTime(iso) {
1496
2379
  }
1497
2380
  function renderAuditLogBody(loading, entries) {
1498
2381
  if (loading) {
1499
- return /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ React6.createElement(CircularProgress4, { size: 24 })));
2382
+ return /* @__PURE__ */ React7.createElement(TableRow4, null, /* @__PURE__ */ React7.createElement(TableCell4, { colSpan: 6, sx: { py: 2 } }, /* @__PURE__ */ React7.createElement(Skeleton5, { variant: "rounded", height: 160 })));
1500
2383
  }
1501
2384
  if (entries.length === 0) {
1502
- return /* @__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")));
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
+ )));
1503
2392
  }
1504
- return entries.map((entry) => /* @__PURE__ */ React6.createElement(DetailRow, { key: entry.id, entry }));
2393
+ return entries.map((entry) => /* @__PURE__ */ React7.createElement(DetailRow, { key: entry.id, entry }));
1505
2394
  }
1506
- var ACTION_COLORS, DetailRow, AuditLog;
2395
+ var ACTION_TONES, TABLE_LABELS, DetailRow, AuditLog;
1507
2396
  var init_AuditLog = __esm({
1508
2397
  "src/components/AuditLog.tsx"() {
1509
2398
  "use strict";
1510
- ACTION_COLORS = {
2399
+ init_ui();
2400
+ ACTION_TONES = {
1511
2401
  created: "success",
1512
- deleted: "error",
2402
+ deleted: "danger",
1513
2403
  updated: "warning",
1514
- blocked: "error",
2404
+ blocked: "danger",
1515
2405
  unblocked: "success"
1516
2406
  };
2407
+ TABLE_LABELS = {
2408
+ LiteLLM_VerificationToken: "Key",
2409
+ LiteLLM_TeamTable: "Team",
2410
+ LiteLLM_UserTable: "User"
2411
+ };
1517
2412
  DetailRow = ({ entry }) => {
1518
2413
  const [open, setOpen] = useState5(false);
1519
2414
  const hasDetail = entry.before_value || entry.updated_values;
1520
- 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
+ )))));
1521
2475
  };
1522
2476
  AuditLog = ({ api }) => {
1523
2477
  const [page, setPage] = useState5(0);
@@ -1533,68 +2487,80 @@ var init_AuditLog = __esm({
1533
2487
  );
1534
2488
  const entries = value?.audit_logs ?? [];
1535
2489
  const total = value?.total ?? 0;
1536
- 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(
1537
- TextField3,
1538
- {
1539
- size: "small",
1540
- label: "Action",
1541
- select: true,
1542
- value: filters.action ?? "",
1543
- onChange: (e) => {
1544
- setFilters((f) => ({ ...f, action: e.target.value || void 0 }));
1545
- setPage(0);
1546
- },
1547
- sx: { minWidth: 160 }
1548
- },
1549
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "" }, "All actions"),
1550
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "created" }, "Created"),
1551
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "updated" }, "Updated"),
1552
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "deleted" }, "Deleted"),
1553
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "blocked" }, "Blocked")
1554
- ), /* @__PURE__ */ React6.createElement(
1555
- TextField3,
2490
+ return /* @__PURE__ */ React7.createElement(
2491
+ SectionCard,
1556
2492
  {
1557
- size: "small",
1558
- label: "Table",
1559
- select: true,
1560
- value: filters.table_name ?? "",
1561
- onChange: (e) => {
1562
- setFilters((f) => ({ ...f, table_name: e.target.value || void 0 }));
1563
- setPage(0);
1564
- },
1565
- 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
+ ))
1566
2544
  },
1567
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "" }, "All tables"),
1568
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "LiteLLM_VerificationToken" }, "Key"),
1569
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "LiteLLM_TeamTable" }, "Team"),
1570
- /* @__PURE__ */ React6.createElement(MenuItem3, { value: "LiteLLM_UserTable" }, "User")
1571
- ), /* @__PURE__ */ React6.createElement(
1572
- TextField3,
1573
- {
1574
- size: "small",
1575
- label: "Changed by",
1576
- value: filters.changed_by ?? "",
1577
- onChange: (e) => {
1578
- setFilters((f) => ({ ...f, changed_by: e.target.value || void 0 }));
1579
- setPage(0);
1580
- },
1581
- sx: { minWidth: 200 }
1582
- }
1583
- )), 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, renderAuditLogBody(loading, entries)))), /* @__PURE__ */ React6.createElement(
1584
- TablePagination,
1585
- {
1586
- component: "div",
1587
- count: total,
1588
- page,
1589
- onPageChange: (_, p) => setPage(p),
1590
- rowsPerPage: pageSize,
1591
- onRowsPerPageChange: (e) => {
1592
- setPageSize(Number(e.target.value));
1593
- setPage(0);
1594
- },
1595
- rowsPerPageOptions: [10, 25, 50]
1596
- }
1597
- ));
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
+ );
1598
2564
  };
1599
2565
  }
1600
2566
  });
@@ -1604,15 +2570,15 @@ var LiteLLMPage_exports = {};
1604
2570
  __export(LiteLLMPage_exports, {
1605
2571
  LiteLLMPage: () => LiteLLMPage
1606
2572
  });
1607
- import React7, { useState as useState6, useCallback as useCallback2, useMemo as useMemo5 } from "react";
1608
- import Box7 from "@mui/material/Box";
2573
+ import React8, { useState as useState6, useCallback as useCallback2, useMemo as useMemo5 } from "react";
2574
+ import Box8 from "@mui/material/Box";
1609
2575
  import Snackbar from "@mui/material/Snackbar";
1610
2576
  import Alert3 from "@mui/material/Alert";
1611
- import CircularProgress5 from "@mui/material/CircularProgress";
1612
- import Typography7 from "@mui/material/Typography";
1613
- import Paper6 from "@mui/material/Paper";
1614
- import Tabs3 from "@mui/material/Tabs";
1615
- import Tab3 from "@mui/material/Tab";
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";
1616
2582
  import { useAsync as useAsync2, useAsyncRetry } from "react-use";
1617
2583
  import { useApi } from "@backstage/core-plugin-api";
1618
2584
  function initDateRange() {
@@ -1817,34 +2783,42 @@ var init_LiteLLMPage = __esm({
1817
2783
  );
1818
2784
  const isInitialLoading = userLoading && !userInfo;
1819
2785
  if (isInitialLoading) {
1820
- 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));
1821
2787
  }
1822
2788
  if (userError || !userInfo) {
1823
2789
  const isProvisioningEnabled = userError?.body?.provisioning === true;
1824
2790
  const hint = userError?.body?.hint;
1825
- 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.")));
1826
2792
  }
1827
- 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(
1828
2812
  DashboardHeader,
1829
2813
  {
1830
2814
  userInfo,
1831
2815
  teams: teams ?? [],
1832
2816
  keys: keys ?? [],
1833
2817
  loading: userLoading || teamsLoading,
1834
- onGenerateKeyClick: () => setGenerateDialogOpen(true)
2818
+ onGenerateKeyClick: () => setGenerateDialogOpen(true),
2819
+ tabs: pageTabs
1835
2820
  }
1836
- )), /* @__PURE__ */ React7.createElement(
1837
- Tabs3,
1838
- {
1839
- value: activeTab,
1840
- onChange: (_, v) => setActiveTab(v),
1841
- sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
1842
- },
1843
- /* @__PURE__ */ React7.createElement(Tab3, { label: "Overview", value: "overview" }),
1844
- /* @__PURE__ */ React7.createElement(Tab3, { label: "Keys", value: "keys" }),
1845
- /* @__PURE__ */ React7.createElement(Tab3, { label: "Teams", value: "teams" }),
1846
- userInfo.can_view_audit && /* @__PURE__ */ React7.createElement(Tab3, { label: "Audit Log", value: "audit" })
1847
- ), activeTab === "overview" && /* @__PURE__ */ React7.createElement(
2821
+ ), activeTab === "overview" && /* @__PURE__ */ React8.createElement(
1848
2822
  UsageStats,
1849
2823
  {
1850
2824
  usage: usage ?? null,
@@ -1854,7 +2828,7 @@ var init_LiteLLMPage = __esm({
1854
2828
  loading: usageLoading,
1855
2829
  userInfo
1856
2830
  }
1857
- ), activeTab === "keys" && /* @__PURE__ */ React7.createElement(
2831
+ ), activeTab === "keys" && /* @__PURE__ */ React8.createElement(
1858
2832
  KeysTable,
1859
2833
  {
1860
2834
  keys: keys ?? [],
@@ -1868,7 +2842,7 @@ var init_LiteLLMPage = __esm({
1868
2842
  onDeleteKey: handleDeleteKey,
1869
2843
  onPruneExpiredKeys: handlePruneExpiredKeys
1870
2844
  }
1871
- ), activeTab === "teams" && /* @__PURE__ */ React7.createElement(
2845
+ ), activeTab === "teams" && /* @__PURE__ */ React8.createElement(
1872
2846
  TeamUsage,
1873
2847
  {
1874
2848
  teams: teams ?? [],
@@ -1879,7 +2853,7 @@ var init_LiteLLMPage = __esm({
1879
2853
  },
1880
2854
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
1881
2855
  }
1882
- ), 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(
1883
2857
  GenerateKeyDialog,
1884
2858
  {
1885
2859
  open: generateDialogOpen,
@@ -1892,7 +2866,7 @@ var init_LiteLLMPage = __esm({
1892
2866
  onGenerateKey: handleGenerateKey,
1893
2867
  onGetConfig: () => api.getConfig()
1894
2868
  }
1895
- ), /* @__PURE__ */ React7.createElement(
2869
+ ), /* @__PURE__ */ React8.createElement(
1896
2870
  Snackbar,
1897
2871
  {
1898
2872
  open: !!snackbar,
@@ -1900,7 +2874,7 @@ var init_LiteLLMPage = __esm({
1900
2874
  onClose: () => setSnackbar(null),
1901
2875
  anchorOrigin: { vertical: "bottom", horizontal: "right" }
1902
2876
  },
1903
- 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
1904
2878
  ));
1905
2879
  };
1906
2880
  }
@@ -1908,7 +2882,7 @@ var init_LiteLLMPage = __esm({
1908
2882
 
1909
2883
  // src/plugin.tsx
1910
2884
  init_api();
1911
- import React8 from "react";
2885
+ import React9 from "react";
1912
2886
  import { TrendingUp as TrendingUpIcon } from "@mui/icons-material";
1913
2887
  import {
1914
2888
  createFrontendPlugin,
@@ -1927,10 +2901,10 @@ var liteLlmPage = PageBlueprint.make({
1927
2901
  params: {
1928
2902
  path: "/litellm",
1929
2903
  title: "LiteLLM",
1930
- icon: /* @__PURE__ */ React8.createElement(TrendingUpIcon, null),
2904
+ icon: /* @__PURE__ */ React9.createElement(TrendingUpIcon, null),
1931
2905
  loader: async () => {
1932
2906
  const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
1933
- return /* @__PURE__ */ React8.createElement(LiteLLMPage2, null);
2907
+ return /* @__PURE__ */ React9.createElement(LiteLLMPage2, null);
1934
2908
  }
1935
2909
  }
1936
2910
  });
@@ -1949,15 +2923,15 @@ init_TeamUsage();
1949
2923
  // src/components/LiteLLMHomeWidget.tsx
1950
2924
  init_api();
1951
2925
  init_format();
1952
- import React9, { useState as useState7, useEffect as useEffect2 } from "react";
1953
- import Paper7 from "@mui/material/Paper";
1954
- import Box8 from "@mui/material/Box";
1955
- import Typography8 from "@mui/material/Typography";
1956
- import FormControl2 from "@mui/material/FormControl";
1957
- import Select2 from "@mui/material/Select";
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";
1958
2932
  import MenuItem4 from "@mui/material/MenuItem";
1959
2933
  import Grid2 from "@mui/material/Grid";
1960
- import CircularProgress6 from "@mui/material/CircularProgress";
2934
+ import CircularProgress5 from "@mui/material/CircularProgress";
1961
2935
  import Alert4 from "@mui/material/Alert";
1962
2936
  import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
1963
2937
  import { useApi as useApi2 } from "@backstage/core-plugin-api";
@@ -1973,7 +2947,7 @@ function presetToDateRange(preset) {
1973
2947
  }
1974
2948
  return { start, end };
1975
2949
  }
1976
- 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));
1977
2951
  var LiteLLMHomeWidget = ({
1978
2952
  defaultPeriod = "7d",
1979
2953
  title = "LiteLLM Usage"
@@ -2022,17 +2996,17 @@ var LiteLLMHomeWidget = ({
2022
2996
  spend: d.spend
2023
2997
  }));
2024
2998
  const hasSparkline = dailyData.length > 0;
2025
- 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(
2026
- 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,
2027
3001
  {
2028
3002
  value: period,
2029
3003
  onChange: (e) => setPeriod(e.target.value),
2030
3004
  displayEmpty: true
2031
3005
  },
2032
- /* @__PURE__ */ React9.createElement(MenuItem4, { value: "today" }, "Today"),
2033
- /* @__PURE__ */ React9.createElement(MenuItem4, { value: "7d" }, "7d"),
2034
- /* @__PURE__ */ React9.createElement(MenuItem4, { value: "30d" }, "30d")
2035
- ))), 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(
2036
3010
  Area3,
2037
3011
  {
2038
3012
  type: "monotone",