@acarmisc/backstage-plugin-litellm 0.12.4 → 0.14.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,
197
- {
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,
747
+ return /* @__PURE__ */ React2.createElement(Paper2, { sx: { borderRadius: 2, overflow: "hidden" } }, /* @__PURE__ */ React2.createElement(
748
+ Box2,
205
749
  {
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,
479
1182
  {
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"
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,
1194
+ {
1195
+ variant: "outlined",
1196
+ color: "inherit",
1197
+ startIcon: /* @__PURE__ */ React3.createElement(Autorenew, null),
1198
+ onClick: () => setPruneConfirmCount(expiredKeys.length),
1199
+ sx: (theme) => ({
1200
+ color: theme.palette.text.secondary,
1201
+ borderColor: theme.palette.divider,
1202
+ "&:hover": {
1203
+ color: theme.palette.error.main,
1204
+ borderColor: alpha3(theme.palette.error.main, 0.5),
1205
+ bgcolor: alpha3(theme.palette.error.main, 0.06)
1206
+ }
1207
+ })
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)
1221
+ "Generate New Key"
1222
+ ))
516
1223
  },
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
527
- },
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";
@@ -654,10 +1350,18 @@ import { ContentCopy as ContentCopy2, Code } from "@mui/icons-material";
654
1350
  function trimSlash(url) {
655
1351
  return url.replace(/\/+$/, "");
656
1352
  }
1353
+ function isModelAllowedByTeam(model, teamModels) {
1354
+ if (!teamModels || teamModels.length === 0) return true;
1355
+ if (teamModels.includes(ALL_PROXY_MODELS)) return true;
1356
+ if (teamModels.includes(model.model_name)) return true;
1357
+ return !!model.access_groups?.some((group) => teamModels.includes(group));
1358
+ }
657
1359
  function buildSnippets(baseUrl, key, model) {
658
1360
  const base = trimSlash(baseUrl);
1361
+ const apiBase = `${base}/v1`;
659
1362
  return {
660
- curl: `curl ${base}/v1/chat/completions \\
1363
+ publicEndpoint: apiBase,
1364
+ curl: `curl ${apiBase}/chat/completions \\
661
1365
  -H "Authorization: Bearer ${key}" \\
662
1366
  -H "Content-Type: application/json" \\
663
1367
  -d '{
@@ -668,14 +1372,40 @@ function buildSnippets(baseUrl, key, model) {
668
1372
 
669
1373
  client = OpenAI(
670
1374
  api_key="${key}",
671
- base_url="${base}/v1",
1375
+ base_url="${apiBase}",
672
1376
  )
673
1377
 
674
1378
  response = client.chat.completions.create(
675
1379
  model="${model}",
676
1380
  messages=[{ "role": "user", "content": "Hello!" }],
677
1381
  )
678
- print(response.choices[0].message.content)`
1382
+ print(response.choices[0].message.content)`,
1383
+ opencode: `{
1384
+ "$schema": "https://opencode.ai/config.json",
1385
+ "provider": {
1386
+ "litellm": {
1387
+ "npm": "@ai-sdk/openai-compatible",
1388
+ "name": "LiteLLM",
1389
+ "options": {
1390
+ "baseURL": "${apiBase}",
1391
+ "apiKey": "${key}"
1392
+ },
1393
+ "models": {
1394
+ "${model}": {}
1395
+ }
1396
+ }
1397
+ }
1398
+ }`,
1399
+ pi: `{
1400
+ "litellm": {
1401
+ "baseUrl": "${apiBase}",
1402
+ "apiKey": "${key}",
1403
+ "api": "openai-completions",
1404
+ "models": [
1405
+ { "id": "${model}", "name": "${model}" }
1406
+ ]
1407
+ }
1408
+ }`
679
1409
  };
680
1410
  }
681
1411
  function aliasHelperText(aliasError, aliasDuplicate) {
@@ -713,7 +1443,7 @@ function formatContextWindow2(maxInput, maxOutput) {
713
1443
  if (outPart) return `ctx ${outPart} out`;
714
1444
  return null;
715
1445
  }
716
- var generateDefaultAlias, emptyForm, SnippetTabs, GenerateKeyDialog;
1446
+ var generateDefaultAlias, emptyForm, ALL_PROXY_MODELS, SNIPPET_FILE_HINTS, SnippetTabs, GenerateKeyDialog;
717
1447
  var init_GenerateKeyDialog = __esm({
718
1448
  "src/components/GenerateKeyDialog.tsx"() {
719
1449
  "use strict";
@@ -733,17 +1463,23 @@ var init_GenerateKeyDialog = __esm({
733
1463
  team_id: void 0,
734
1464
  key_type: "llm_api"
735
1465
  });
1466
+ ALL_PROXY_MODELS = "all-proxy-models";
1467
+ SNIPPET_FILE_HINTS = {
1468
+ opencode: "Add to ~/.config/opencode/opencode.json",
1469
+ pi: "Add to ~/.pi/agent/models.json"
1470
+ };
736
1471
  SnippetTabs = ({ snippets, model, onCopy }) => {
737
1472
  const [tab, setTab] = useState2("curl");
738
- 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,
1473
+ const code = snippets[tab];
1474
+ const fileHint = SNIPPET_FILE_HINTS[tab];
1475
+ return /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 }, variant: "scrollable" }, /* @__PURE__ */ React4.createElement(Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ React4.createElement(Tab, { label: "OpenAI SDK", value: "openai" }), /* @__PURE__ */ React4.createElement(Tab, { label: "opencode", value: "opencode" }), /* @__PURE__ */ React4.createElement(Tab, { label: "pi", value: "pi" })), fileHint && /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, fileHint), /* @__PURE__ */ React4.createElement(
1476
+ Box4,
741
1477
  {
742
1478
  position: "relative",
743
1479
  p: 1.5,
744
1480
  sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
745
1481
  },
746
- /* @__PURE__ */ React3.createElement(
1482
+ /* @__PURE__ */ React4.createElement(
747
1483
  IconButton2,
748
1484
  {
749
1485
  size: "small",
@@ -751,10 +1487,10 @@ var init_GenerateKeyDialog = __esm({
751
1487
  title: "Copy snippet",
752
1488
  sx: { position: "absolute", top: 4, right: 4 }
753
1489
  },
754
- /* @__PURE__ */ React3.createElement(ContentCopy2, { fontSize: "small" })
1490
+ /* @__PURE__ */ React4.createElement(ContentCopy2, { fontSize: "small" })
755
1491
  ),
756
- /* @__PURE__ */ React3.createElement(
757
- Typography3,
1492
+ /* @__PURE__ */ React4.createElement(
1493
+ Typography4,
758
1494
  {
759
1495
  component: "pre",
760
1496
  sx: {
@@ -768,7 +1504,7 @@ var init_GenerateKeyDialog = __esm({
768
1504
  },
769
1505
  code
770
1506
  ),
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.")
1507
+ 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
1508
  ));
773
1509
  };
774
1510
  GenerateKeyDialog = ({
@@ -803,12 +1539,7 @@ var init_GenerateKeyDialog = __esm({
803
1539
  const teamRequired = keyGenerationSettings?.teamRequired ?? true;
804
1540
  const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
805
1541
  const availableModels = useMemo3(() => {
806
- const teamModels = selectedTeam?.models;
807
- if (teamModels && teamModels.length > 0) {
808
- const allowed = new Set(teamModels);
809
- return models.filter((m) => allowed.has(m.model_name));
810
- }
811
- return models;
1542
+ return models.filter((m) => isModelAllowedByTeam(m, selectedTeam?.models));
812
1543
  }, [models, selectedTeam]);
813
1544
  const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
814
1545
  const aliasError = !(formData.alias || "").trim();
@@ -832,12 +1563,12 @@ var init_GenerateKeyDialog = __esm({
832
1563
  max_budget: unlimitedBudget ? null : formData.max_budget
833
1564
  };
834
1565
  const response = await onGenerateKey(request);
1566
+ const model = formData.models?.[0] ?? availableModels[0]?.model_name ?? "";
835
1567
  setNewKeyValue(response.key);
836
- setNewKeyModel(formData.models?.[0] ?? "");
1568
+ setNewKeyModel(model);
837
1569
  setNewKeySnippets(null);
838
1570
  try {
839
1571
  const config = await onGetConfig();
840
- const model = formData.models?.[0] ?? "";
841
1572
  setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
842
1573
  } catch {
843
1574
  }
@@ -864,22 +1595,24 @@ var init_GenerateKeyDialog = __esm({
864
1595
  const inCost = fmtCost2(m.input_cost_per_token);
865
1596
  const outCost = fmtCost2(m.output_cost_per_token);
866
1597
  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"));
1598
+ 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
1599
  };
869
1600
  const renderTeamField = () => {
870
1601
  if (teams.length > 0) {
871
- return /* @__PURE__ */ React3.createElement(
1602
+ return /* @__PURE__ */ React4.createElement(
872
1603
  Autocomplete2,
873
1604
  {
874
1605
  options: teams,
875
1606
  getOptionLabel: (t) => t.team_alias || t.team_id,
876
1607
  value: selectedTeam,
877
1608
  onChange: (_e, team) => {
878
- const teamModels = team?.models;
879
- const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
1609
+ const restrictedModels = (formData.models || []).filter((name) => {
1610
+ const model = models.find((m) => m.model_name === name);
1611
+ return model ? isModelAllowedByTeam(model, team?.models) : false;
1612
+ });
880
1613
  setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
881
1614
  },
882
- renderInput: (params) => /* @__PURE__ */ React3.createElement(
1615
+ renderInput: (params) => /* @__PURE__ */ React4.createElement(
883
1616
  TextField2,
884
1617
  {
885
1618
  ...params,
@@ -894,12 +1627,12 @@ var init_GenerateKeyDialog = __esm({
894
1627
  );
895
1628
  }
896
1629
  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.");
1630
+ 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
1631
  }
899
1632
  return null;
900
1633
  };
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,
1634
+ 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(
1635
+ Box4,
903
1636
  {
904
1637
  display: "flex",
905
1638
  alignItems: "center",
@@ -913,8 +1646,8 @@ var init_GenerateKeyDialog = __esm({
913
1646
  borderRadius: 1
914
1647
  }
915
1648
  },
916
- /* @__PURE__ */ React3.createElement(
917
- Typography3,
1649
+ /* @__PURE__ */ React4.createElement(
1650
+ Typography4,
918
1651
  {
919
1652
  component: "code",
920
1653
  color: "text.primary",
@@ -922,8 +1655,32 @@ var init_GenerateKeyDialog = __esm({
922
1655
  },
923
1656
  newKeyValue
924
1657
  ),
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(
1658
+ /* @__PURE__ */ React4.createElement(IconButton2, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React4.createElement(ContentCopy2, null))
1659
+ ), newKeySnippets && /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", display: "block", gutterBottom: true }, "Public endpoint \u2014 paste into any tool's base URL / API base field"), /* @__PURE__ */ React4.createElement(
1660
+ Box4,
1661
+ {
1662
+ display: "flex",
1663
+ alignItems: "center",
1664
+ gap: 1,
1665
+ p: 1.5,
1666
+ sx: {
1667
+ backgroundColor: "action.hover",
1668
+ border: "1px solid",
1669
+ borderColor: "divider",
1670
+ borderRadius: 1
1671
+ }
1672
+ },
1673
+ /* @__PURE__ */ React4.createElement(
1674
+ Typography4,
1675
+ {
1676
+ component: "code",
1677
+ color: "text.primary",
1678
+ sx: { fontFamily: "monospace", fontSize: 13, wordBreak: "break-all", flex: 1 }
1679
+ },
1680
+ newKeySnippets.publicEndpoint
1681
+ ),
1682
+ /* @__PURE__ */ React4.createElement(IconButton2, { size: "small", onClick: () => copyToClipboard(newKeySnippets.publicEndpoint) }, /* @__PURE__ */ React4.createElement(ContentCopy2, { fontSize: "small" }))
1683
+ )), 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
1684
  TextField2,
928
1685
  {
929
1686
  label: "Alias",
@@ -938,7 +1695,7 @@ var init_GenerateKeyDialog = __esm({
938
1695
  required: true,
939
1696
  fullWidth: true
940
1697
  }
941
- ), /* @__PURE__ */ React3.createElement(
1698
+ ), /* @__PURE__ */ React4.createElement(
942
1699
  TextField2,
943
1700
  {
944
1701
  select: true,
@@ -947,12 +1704,12 @@ var init_GenerateKeyDialog = __esm({
947
1704
  onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
948
1705
  fullWidth: true
949
1706
  },
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(
1707
+ /* @__PURE__ */ React4.createElement(MenuItem, { value: "1d" }, "1 Day"),
1708
+ /* @__PURE__ */ React4.createElement(MenuItem, { value: "7d" }, "7 Days"),
1709
+ /* @__PURE__ */ React4.createElement(MenuItem, { value: "30d" }, "30 Days"),
1710
+ /* @__PURE__ */ React4.createElement(MenuItem, { value: "90d" }, "90 Days"),
1711
+ /* @__PURE__ */ React4.createElement(MenuItem, { value: "1y" }, "1 Year")
1712
+ ), renderTeamField(), availableModels.length > 0 && /* @__PURE__ */ React4.createElement(
956
1713
  Autocomplete2,
957
1714
  {
958
1715
  multiple: true,
@@ -961,8 +1718,8 @@ var init_GenerateKeyDialog = __esm({
961
1718
  getOptionLabel: (m) => m.model_name,
962
1719
  value: selectedModels,
963
1720
  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(
1721
+ renderOption: (props, m) => /* @__PURE__ */ React4.createElement("li", { ...props }, modelOption(m)),
1722
+ renderInput: (params) => /* @__PURE__ */ React4.createElement(
966
1723
  TextField2,
967
1724
  {
968
1725
  ...params,
@@ -972,10 +1729,10 @@ var init_GenerateKeyDialog = __esm({
972
1729
  }
973
1730
  )
974
1731
  }
975
- ), allowUnlimitedBudget && /* @__PURE__ */ React3.createElement(
1732
+ ), allowUnlimitedBudget && /* @__PURE__ */ React4.createElement(
976
1733
  FormControlLabel,
977
1734
  {
978
- control: /* @__PURE__ */ React3.createElement(
1735
+ control: /* @__PURE__ */ React4.createElement(
979
1736
  Checkbox,
980
1737
  {
981
1738
  checked: unlimitedBudget,
@@ -991,7 +1748,7 @@ var init_GenerateKeyDialog = __esm({
991
1748
  ),
992
1749
  label: "Unlimited budget"
993
1750
  }
994
- ), /* @__PURE__ */ React3.createElement(
1751
+ ), /* @__PURE__ */ React4.createElement(
995
1752
  TextField2,
996
1753
  {
997
1754
  label: "Max Budget (USD)",
@@ -1004,7 +1761,7 @@ var init_GenerateKeyDialog = __esm({
1004
1761
  required: true,
1005
1762
  fullWidth: true
1006
1763
  }
1007
- ), /* @__PURE__ */ React3.createElement(
1764
+ ), /* @__PURE__ */ React4.createElement(
1008
1765
  TextField2,
1009
1766
  {
1010
1767
  label: "TPM Limit",
@@ -1014,7 +1771,7 @@ var init_GenerateKeyDialog = __esm({
1014
1771
  helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
1015
1772
  fullWidth: true
1016
1773
  }
1017
- ), /* @__PURE__ */ React3.createElement(
1774
+ ), /* @__PURE__ */ React4.createElement(
1018
1775
  TextField2,
1019
1776
  {
1020
1777
  label: "RPM Limit",
@@ -1024,7 +1781,7 @@ var init_GenerateKeyDialog = __esm({
1024
1781
  helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
1025
1782
  fullWidth: true
1026
1783
  }
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(
1784
+ ))), /* @__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
1785
  Button3,
1029
1786
  {
1030
1787
  onClick: handleGenerate,
@@ -1032,34 +1789,27 @@ var init_GenerateKeyDialog = __esm({
1032
1789
  color: "primary",
1033
1790
  disabled: !canGenerate
1034
1791
  },
1035
- submitting ? /* @__PURE__ */ React3.createElement(CircularProgress2, { size: 24 }) : "Generate"
1792
+ submitting ? /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 }) : "Generate"
1036
1793
  ))));
1037
1794
  };
1038
1795
  }
1039
1796
  });
1040
1797
 
1041
1798
  // src/components/UsageStats.tsx
1042
- import React4, { useMemo as useMemo4, useState as useState3 } from "react";
1799
+ import React5, { useMemo as useMemo4, useState as useState3 } from "react";
1043
1800
  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";
1801
+ import Box5 from "@mui/material/Box";
1802
+ import Typography5 from "@mui/material/Typography";
1803
+ import TextField3 from "@mui/material/TextField";
1049
1804
  import MenuItem2 from "@mui/material/MenuItem";
1050
1805
  import Grid from "@mui/material/Grid";
1051
- import Tabs2 from "@mui/material/Tabs";
1052
- import Tab2 from "@mui/material/Tab";
1053
1806
  import Table2 from "@mui/material/Table";
1054
1807
  import TableBody2 from "@mui/material/TableBody";
1055
1808
  import TableCell2 from "@mui/material/TableCell";
1056
1809
  import TableContainer2 from "@mui/material/TableContainer";
1057
1810
  import TableHead2 from "@mui/material/TableHead";
1058
1811
  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";
1812
+ import Skeleton3 from "@mui/material/Skeleton";
1063
1813
  import {
1064
1814
  AreaChart,
1065
1815
  Area,
@@ -1073,48 +1823,46 @@ import {
1073
1823
  Tooltip,
1074
1824
  ResponsiveContainer,
1075
1825
  Legend,
1076
- ReferenceLine
1826
+ ReferenceLine,
1827
+ Cell
1077
1828
  } 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];
1829
+ function rateTone(rate) {
1830
+ if (rate >= 0.99) return "success";
1831
+ if (rate >= 0.9) return "warning";
1832
+ return "danger";
1082
1833
  }
1083
- var TOP_N_MODELS, PERIOD_LS_KEY, MODEL_COLORS, fmtPct, KpiCard, ChartSkeleton, EmptyChart, ChartOrFallback, UsageStats;
1834
+ var TOP_N_MODELS, PERIOD_LS_KEY, PRESET_LABELS, fmtPct, ChartSkeleton, ChartOrFallback, SuccessRateCell, UsageStats;
1084
1835
  var init_UsageStats = __esm({
1085
1836
  "src/components/UsageStats.tsx"() {
1086
1837
  "use strict";
1087
1838
  init_format();
1839
+ init_ui();
1088
1840
  TOP_N_MODELS = 6;
1089
1841
  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
- ];
1842
+ PRESET_LABELS = {
1843
+ today: "Today",
1844
+ "24h": "Last 24 hours",
1845
+ "7d": "Last 7 days",
1846
+ "30d": "Last 30 days"
1847
+ };
1102
1848
  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);
1849
+ ChartSkeleton = ({ height = 240 }) => /* @__PURE__ */ React5.createElement(Skeleton3, { variant: "rounded", height });
1850
+ ChartOrFallback = ({ loading, empty, height = 240, children }) => {
1851
+ if (loading) return /* @__PURE__ */ React5.createElement(ChartSkeleton, { height });
1852
+ if (empty) return /* @__PURE__ */ React5.createElement(EmptyState, { message: "No data for this period", height });
1853
+ return /* @__PURE__ */ React5.createElement(React5.Fragment, null, children);
1854
+ };
1855
+ SuccessRateCell = ({ rate, requests }) => {
1856
+ if (requests === 0) return /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary" }, "\u2014");
1857
+ const tone = rateTone(rate);
1858
+ 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(
1859
+ Typography5,
1860
+ {
1861
+ variant: "caption",
1862
+ sx: { fontVariantNumeric: "tabular-nums", minWidth: 44 }
1863
+ },
1864
+ fmtPct(rate)
1865
+ ));
1118
1866
  };
1119
1867
  UsageStats = ({
1120
1868
  usage,
@@ -1124,14 +1872,9 @@ var init_UsageStats = __esm({
1124
1872
  loading,
1125
1873
  userInfo
1126
1874
  }) => {
1127
- const theme = useTheme();
1875
+ const chart = useChartTheme();
1128
1876
  const [selectedModel, setSelectedModel] = useState3("all");
1129
1877
  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
1878
  const selectedPreset = useMemo4(() => {
1136
1879
  if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
1137
1880
  const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
@@ -1239,90 +1982,225 @@ var init_UsageStats = __esm({
1239
1982
  const overallSuccessRate = totalRequests > 0 ? (usage?.successful_requests ?? 0) / totalRequests : 0;
1240
1983
  const maxBudget = userInfo?.max_budget ?? 0;
1241
1984
  const totalCumSpend = cumulativeData[cumulativeData.length - 1]?.cumulative ?? 0;
1985
+ const cumulativeDomain = maxBudget > 0 ? [0, (dataMax) => Math.max(dataMax, maxBudget) * 1.05] : [0, "auto"];
1986
+ const successTone = totalRequests === 0 ? "neutral" : rateTone(overallSuccessRate);
1987
+ const metrics = [
1988
+ {
1989
+ label: "Total spend",
1990
+ value: fmtUsd(usage?.total_spend ?? 0),
1991
+ hint: maxBudget > 0 ? `of ${fmtUsd(maxBudget)} budget` : void 0,
1992
+ tone: "accent"
1993
+ },
1994
+ {
1995
+ label: "Requests",
1996
+ value: fmtInt(totalRequests),
1997
+ hint: `${fmtInt(usage?.failed_requests ?? 0)} failed`,
1998
+ tone: "info"
1999
+ },
2000
+ {
2001
+ label: "Success rate",
2002
+ value: totalRequests > 0 ? fmtPct(overallSuccessRate) : "\u2014",
2003
+ hint: totalRequests > 0 ? `${fmtInt(usage?.successful_requests ?? 0)} succeeded` : void 0,
2004
+ tone: successTone
2005
+ },
2006
+ {
2007
+ label: "Tokens",
2008
+ value: fmtCompact(usage?.total_tokens ?? 0),
2009
+ hint: `${fmtCompact(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtCompact(usage?.completion_tokens ?? 0)} out`,
2010
+ tone: "success"
2011
+ }
2012
+ ];
1242
2013
  const renderKeyRows = () => {
1243
2014
  if (loading) {
1244
- return /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React4.createElement(LinearProgress3, null)));
2015
+ return /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, { colSpan: 7, sx: { py: 3 } }, /* @__PURE__ */ React5.createElement(Skeleton3, { variant: "rounded", height: 80 })));
1245
2016
  }
1246
2017
  if (keyRows.length === 0) {
1247
- return /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 7, align: "center" }, "No key activity"));
2018
+ return /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React5.createElement(EmptyState, { message: "No key activity in this period" })));
1248
2019
  }
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")));
2020
+ 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
2021
  };
1258
2022
  const renderModelRows = () => {
1259
2023
  if (loading) {
1260
- return /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React4.createElement(LinearProgress3, null)));
2024
+ return /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, { colSpan: 8, sx: { py: 3 } }, /* @__PURE__ */ React5.createElement(Skeleton3, { variant: "rounded", height: 80 })));
1261
2025
  }
1262
2026
  if (modelRows.length === 0) {
1263
- return /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 8, align: "center" }, "No model activity"));
2027
+ return /* @__PURE__ */ React5.createElement(TableRow2, null, /* @__PURE__ */ React5.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React5.createElement(EmptyState, { message: "No model activity in this period" })));
1264
2028
  }
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")));
2029
+ 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
2030
  };
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,
2031
+ const periodSelect = /* @__PURE__ */ React5.createElement(
2032
+ TextField3,
1276
2033
  {
1277
- value: selectedPreset,
2034
+ select: true,
2035
+ size: "small",
1278
2036
  label: "Period",
1279
- onChange: (e) => handlePresetChange(e.target.value)
2037
+ value: selectedPreset,
2038
+ onChange: (e) => handlePresetChange(e.target.value),
2039
+ sx: { minWidth: 160 }
1280
2040
  },
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,
2041
+ Object.keys(PRESET_LABELS).map((p) => /* @__PURE__ */ React5.createElement(MenuItem2, { key: p, value: p }, PRESET_LABELS[p]))
2042
+ );
2043
+ return /* @__PURE__ */ React5.createElement(
2044
+ SectionCard,
1287
2045
  {
1288
- value: selectedModel,
1289
- label: "Model",
1290
- onChange: (e) => setSelectedModel(e.target.value)
2046
+ title: "Usage Analytics",
2047
+ subtitle: `${PRESET_LABELS[selectedPreset]} \xB7 ${dateRange.start.toLocaleDateString()} \u2013 ${dateRange.end.toLocaleDateString()}`,
2048
+ actions: periodSelect
1291
2049
  },
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()))))));
2050
+ /* @__PURE__ */ React5.createElement(MetricStrip, { metrics }),
2051
+ /* @__PURE__ */ React5.createElement(
2052
+ Box5,
2053
+ {
2054
+ sx: {
2055
+ display: "flex",
2056
+ alignItems: "center",
2057
+ justifyContent: "space-between",
2058
+ gap: 2,
2059
+ flexWrap: "wrap",
2060
+ mt: 2.5,
2061
+ mb: 2
2062
+ }
2063
+ },
2064
+ /* @__PURE__ */ React5.createElement(
2065
+ SegmentedControl,
2066
+ {
2067
+ value: tab,
2068
+ onChange: setTab,
2069
+ options: [
2070
+ { value: "costs", label: "Costs" },
2071
+ { value: "models", label: "Model Activity" },
2072
+ { value: "keys", label: "Key Activity" }
2073
+ ]
2074
+ }
2075
+ ),
2076
+ tab === "models" && /* @__PURE__ */ React5.createElement(
2077
+ TextField3,
2078
+ {
2079
+ select: true,
2080
+ size: "small",
2081
+ label: "Model",
2082
+ value: selectedModel,
2083
+ onChange: (e) => setSelectedModel(e.target.value),
2084
+ sx: { minWidth: 220 }
2085
+ },
2086
+ /* @__PURE__ */ React5.createElement(MenuItem2, { value: "all" }, "All models"),
2087
+ models.map((m) => /* @__PURE__ */ React5.createElement(MenuItem2, { key: m.model_name, value: m.model_name }, m.model_name))
2088
+ )
2089
+ ),
2090
+ 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(
2091
+ Tooltip,
2092
+ {
2093
+ cursor: chart.cursor,
2094
+ content: /* @__PURE__ */ React5.createElement(ChartTooltip, { valueFormatter: fmtUsd, hideZero: true })
2095
+ }
2096
+ ), topSpendModels.map((m) => /* @__PURE__ */ React5.createElement(
2097
+ Area,
2098
+ {
2099
+ key: m,
2100
+ type: "monotone",
2101
+ dataKey: m,
2102
+ stackId: "spend",
2103
+ stroke: chartColor(m),
2104
+ strokeWidth: 1.5,
2105
+ fill: chartColor(m),
2106
+ fillOpacity: 0.28
2107
+ }
2108
+ ))))), !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(
2109
+ Tooltip,
2110
+ {
2111
+ cursor: chart.cursor,
2112
+ content: /* @__PURE__ */ React5.createElement(ChartTooltip, { valueFormatter: (v) => `${v}%` })
2113
+ }
2114
+ ), /* @__PURE__ */ React5.createElement(ReferenceLine, { y: 100, stroke: SERIES.success, strokeDasharray: "4 4", strokeOpacity: 0.5 }), /* @__PURE__ */ React5.createElement(
2115
+ Line,
2116
+ {
2117
+ type: "monotone",
2118
+ dataKey: "successRate",
2119
+ name: "Success rate",
2120
+ stroke: SERIES.input,
2121
+ strokeWidth: 2,
2122
+ dot: { r: 2.5, strokeWidth: 0, fill: SERIES.input },
2123
+ activeDot: { r: 4 }
2124
+ }
2125
+ )))))), /* @__PURE__ */ React5.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React5.createElement(
2126
+ ChartCard,
2127
+ {
2128
+ title: maxBudget > 0 ? "Cumulative spend vs budget" : "Cumulative spend",
2129
+ meta: maxBudget > 0 ? `${fmtUsd(totalCumSpend)} used \xB7 ${fmtUsd(Math.max(0, maxBudget - totalCumSpend))} left` : void 0
2130
+ },
2131
+ /* @__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(
2132
+ YAxis,
2133
+ {
2134
+ tickFormatter: fmtUsdCompact,
2135
+ width: 56,
2136
+ domain: cumulativeDomain,
2137
+ ...chart.axis
2138
+ }
2139
+ ), /* @__PURE__ */ React5.createElement(
2140
+ Tooltip,
2141
+ {
2142
+ cursor: chart.cursor,
2143
+ content: /* @__PURE__ */ React5.createElement(ChartTooltip, { valueFormatter: fmtUsd })
2144
+ }
2145
+ ), maxBudget > 0 && /* @__PURE__ */ React5.createElement(
2146
+ ReferenceLine,
2147
+ {
2148
+ y: maxBudget,
2149
+ stroke: SERIES.budget,
2150
+ strokeDasharray: "5 4",
2151
+ label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 10, fill: SERIES.budget }
2152
+ }
2153
+ ), /* @__PURE__ */ React5.createElement(
2154
+ Area,
2155
+ {
2156
+ type: "monotone",
2157
+ dataKey: "cumulative",
2158
+ name: "Cumulative spend",
2159
+ stroke: SERIES.spend,
2160
+ strokeWidth: 2,
2161
+ fill: SERIES.spend,
2162
+ fillOpacity: 0.18
2163
+ }
2164
+ ))))
2165
+ ))),
2166
+ 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(
2167
+ ChartOrFallback,
2168
+ {
2169
+ loading,
2170
+ empty: topModelSpendBars.length === 0,
2171
+ height: Math.max(200, topModelSpendBars.length * 34)
2172
+ },
2173
+ /* @__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) })))))
2174
+ ))), /* @__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(
2175
+ ChartOrFallback,
2176
+ {
2177
+ loading,
2178
+ empty: modelRows.length === 0,
2179
+ height: Math.max(200, topModelSpendBars.length * 34)
2180
+ },
2181
+ /* @__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] })))
2182
+ ))), /* @__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()))))),
2183
+ 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(
2184
+ ChartOrFallback,
2185
+ {
2186
+ loading,
2187
+ empty: topKeySpendBars.length === 0,
2188
+ height: Math.max(160, topKeySpendBars.length * 32)
2189
+ },
2190
+ /* @__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 })))
2191
+ ))), /* @__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())))))
2192
+ );
1313
2193
  };
1314
2194
  }
1315
2195
  });
1316
2196
 
1317
2197
  // src/components/TeamUsage.tsx
1318
- import React5, { useState as useState4 } from "react";
2198
+ import React6, { useState as useState4 } from "react";
1319
2199
  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";
2200
+ import Box6 from "@mui/material/Box";
2201
+ import Typography6 from "@mui/material/Typography";
2202
+ import Skeleton4 from "@mui/material/Skeleton";
2203
+ import Divider2 from "@mui/material/Divider";
1326
2204
  import Table3 from "@mui/material/Table";
1327
2205
  import TableBody3 from "@mui/material/TableBody";
1328
2206
  import TableCell3 from "@mui/material/TableCell";
@@ -1332,8 +2210,8 @@ import TableContainer3 from "@mui/material/TableContainer";
1332
2210
  import Collapse from "@mui/material/Collapse";
1333
2211
  import IconButton3 from "@mui/material/IconButton";
1334
2212
  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";
2213
+ import { alpha as alpha4 } from "@mui/material/styles";
2214
+ import { ExpandMore, Group } from "@mui/icons-material";
1337
2215
  import {
1338
2216
  AreaChart as AreaChart2,
1339
2217
  Area as Area2,
@@ -1343,21 +2221,20 @@ import {
1343
2221
  Tooltip as Tooltip2,
1344
2222
  ResponsiveContainer as ResponsiveContainer2
1345
2223
  } from "recharts";
1346
- function budgetBarColor(isOver, isNear) {
1347
- if (isOver) return "error";
2224
+ function budgetTone2(isOver, isNear) {
2225
+ if (isOver) return "danger";
1348
2226
  if (isNear) return "warning";
1349
- return "primary";
2227
+ return "accent";
1350
2228
  }
1351
- var Stat, TeamCard, TeamUsage;
2229
+ var fmtUsd2, TeamCard, TeamUsage;
1352
2230
  var init_TeamUsage = __esm({
1353
2231
  "src/components/TeamUsage.tsx"() {
1354
2232
  "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));
2233
+ init_ui();
2234
+ fmtUsd2 = (n) => `$${(n ?? 0).toFixed(2)}`;
1356
2235
  TeamCard = ({ team, usage, usageLoading }) => {
1357
2236
  const [expanded, setExpanded] = useState4(false);
1358
- const theme = useTheme2();
1359
- const gridStroke = theme.palette.divider;
1360
- const tickFill = theme.palette.text.secondary;
2237
+ const chart = useChartTheme();
1361
2238
  const budget = team.max_budget ?? 0;
1362
2239
  const spend = team.spend ?? 0;
1363
2240
  const budgetPct = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
@@ -1366,73 +2243,131 @@ var init_TeamUsage = __esm({
1366
2243
  const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
1367
2244
  const renderDailySpendSection = () => {
1368
2245
  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 } }));
2246
+ return /* @__PURE__ */ React6.createElement(Box6, { display: "flex", justifyContent: "center", py: 3 }, /* @__PURE__ */ React6.createElement(CircularProgress3, { size: 22 }));
1373
2247
  }
1374
- return null;
2248
+ if (dailyData.length === 0) return null;
2249
+ return /* @__PURE__ */ React6.createElement(Box6, { mb: 2.5 }, /* @__PURE__ */ React6.createElement(
2250
+ Typography6,
2251
+ {
2252
+ variant: "caption",
2253
+ color: "text.secondary",
2254
+ sx: { display: "block", fontSize: 11, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase", mb: 1 }
2255
+ },
2256
+ "Daily spend"
2257
+ ), /* @__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(
2258
+ Tooltip2,
2259
+ {
2260
+ cursor: chart.cursor,
2261
+ content: /* @__PURE__ */ React6.createElement(ChartTooltip, { valueFormatter: (v) => `$${v.toFixed(4)}` })
2262
+ }
2263
+ ), /* @__PURE__ */ React6.createElement(
2264
+ Area2,
2265
+ {
2266
+ type: "monotone",
2267
+ dataKey: "spend",
2268
+ name: "Spend",
2269
+ stroke: SERIES.spend,
2270
+ strokeWidth: 2,
2271
+ fill: SERIES.spend,
2272
+ fillOpacity: 0.18
2273
+ }
2274
+ )))));
1375
2275
  };
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,
1402
- {
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,
2276
+ const sectionLabel = (label) => /* @__PURE__ */ React6.createElement(
2277
+ Typography6,
1408
2278
  {
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,
2279
+ variant: "caption",
2280
+ color: "text.secondary",
2281
+ sx: { display: "block", fontSize: 11, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase", mb: 1 }
2282
+ },
2283
+ label
2284
+ );
2285
+ 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(
2286
+ Box6,
1414
2287
  {
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,
2288
+ sx: (theme) => ({
2289
+ width: 36,
2290
+ height: 36,
2291
+ borderRadius: 1.5,
2292
+ flexShrink: 0,
2293
+ display: "flex",
2294
+ alignItems: "center",
2295
+ justifyContent: "center",
2296
+ bgcolor: alpha4(theme.palette.primary.main, 0.1),
2297
+ color: theme.palette.primary.main
2298
+ })
2299
+ },
2300
+ /* @__PURE__ */ React6.createElement(Group, { fontSize: "small" })
2301
+ ), /* @__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(
2302
+ Typography6,
1422
2303
  {
1423
2304
  variant: "caption",
1424
2305
  color: "text.secondary",
1425
- sx: { fontFamily: "monospace" }
2306
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 }
1426
2307
  },
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,
2308
+ team.team_id
2309
+ )), isOver && /* @__PURE__ */ React6.createElement(StatusPill, { label: "Over budget", tone: "danger" }), isNear && /* @__PURE__ */ React6.createElement(StatusPill, { label: "Near limit", tone: "warning" }), /* @__PURE__ */ React6.createElement(
2310
+ IconButton3,
1430
2311
  {
1431
- label: m.role,
1432
2312
  size: "small",
1433
- color: m.role === "admin" ? "primary" : "default"
1434
- }
1435
- ))))))) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
2313
+ onClick: () => setExpanded((e) => !e),
2314
+ "aria-label": expanded ? "Hide team details" : "Show team details",
2315
+ "aria-expanded": expanded,
2316
+ sx: (theme) => ({
2317
+ color: theme.palette.text.secondary,
2318
+ transform: expanded ? "rotate(180deg)" : "none",
2319
+ transition: theme.transitions.create("transform")
2320
+ })
2321
+ },
2322
+ /* @__PURE__ */ React6.createElement(ExpandMore, null)
2323
+ )), /* @__PURE__ */ React6.createElement(
2324
+ Box6,
2325
+ {
2326
+ sx: {
2327
+ display: "grid",
2328
+ gridTemplateColumns: { xs: "repeat(3, minmax(0, 1fr))", sm: "repeat(6, minmax(0, 1fr))" },
2329
+ gap: 2,
2330
+ mt: 2.5,
2331
+ maxWidth: 640
2332
+ }
2333
+ },
2334
+ /* @__PURE__ */ React6.createElement(Stat, { label: "Members", value: team.members_with_roles?.length ?? "\u2014" }),
2335
+ /* @__PURE__ */ React6.createElement(Stat, { label: "Models", value: team.models?.length ? team.models.length : "All" }),
2336
+ /* @__PURE__ */ React6.createElement(Stat, { label: "Budget", value: budget > 0 ? fmtUsd2(budget) : "Unlimited" }),
2337
+ /* @__PURE__ */ React6.createElement(Stat, { label: "Spend", value: fmtUsd2(spend) }),
2338
+ /* @__PURE__ */ React6.createElement(Stat, { label: "TPM", value: team.tpm_limit && team.tpm_limit > 0 ? team.tpm_limit.toLocaleString() : "\u2014" }),
2339
+ /* @__PURE__ */ React6.createElement(Stat, { label: "RPM", value: team.rpm_limit && team.rpm_limit > 0 ? team.rpm_limit.toLocaleString() : "\u2014" })
2340
+ ), 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(
2341
+ TableContainer3,
2342
+ {
2343
+ component: Paper4,
2344
+ variant: "outlined",
2345
+ sx: { borderRadius: 1.5 }
2346
+ },
2347
+ /* @__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(
2348
+ Typography6,
2349
+ {
2350
+ variant: "caption",
2351
+ color: "text.secondary",
2352
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 }
2353
+ },
2354
+ m.user_id
2355
+ )) : /* @__PURE__ */ React6.createElement(
2356
+ Typography6,
2357
+ {
2358
+ variant: "body2",
2359
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }
2360
+ },
2361
+ m.user_id
2362
+ )), /* @__PURE__ */ React6.createElement(TableCell3, { align: "right" }, /* @__PURE__ */ React6.createElement(
2363
+ StatusPill,
2364
+ {
2365
+ label: m.role,
2366
+ tone: m.role === "admin" ? "accent" : "neutral",
2367
+ dot: false
2368
+ }
2369
+ ))))))
2370
+ ) : /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
1436
2371
  };
1437
2372
  TeamUsage = ({
1438
2373
  teams,
@@ -1441,12 +2376,18 @@ var init_TeamUsage = __esm({
1441
2376
  getTeamUsageLoading
1442
2377
  }) => {
1443
2378
  if (loading) {
1444
- return /* @__PURE__ */ React5.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React5.createElement(LinearProgress4, null));
2379
+ 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
2380
  }
1446
2381
  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."))));
2382
+ return /* @__PURE__ */ React6.createElement(SectionCard, { title: "Teams" }, /* @__PURE__ */ React6.createElement(
2383
+ EmptyState,
2384
+ {
2385
+ message: "You're not a member of any LiteLLM team yet.",
2386
+ 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."
2387
+ }
2388
+ ));
1448
2389
  }
1449
- return /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ React5.createElement(
2390
+ 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
2391
  TeamCard,
1451
2392
  {
1452
2393
  key: team.team_id,
@@ -1454,16 +2395,15 @@ var init_TeamUsage = __esm({
1454
2395
  usage: getTeamUsage(team.team_id),
1455
2396
  usageLoading: getTeamUsageLoading(team.team_id)
1456
2397
  }
1457
- )));
2398
+ ))));
1458
2399
  };
1459
2400
  }
1460
2401
  });
1461
2402
 
1462
2403
  // 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";
2404
+ import React7, { useState as useState5, useCallback } from "react";
2405
+ import Box7 from "@mui/material/Box";
2406
+ import Typography7 from "@mui/material/Typography";
1467
2407
  import Table4 from "@mui/material/Table";
1468
2408
  import TableBody4 from "@mui/material/TableBody";
1469
2409
  import TableCell4 from "@mui/material/TableCell";
@@ -1471,22 +2411,26 @@ import TableContainer4 from "@mui/material/TableContainer";
1471
2411
  import TableHead4 from "@mui/material/TableHead";
1472
2412
  import TableRow4 from "@mui/material/TableRow";
1473
2413
  import TablePagination from "@mui/material/TablePagination";
1474
- import TextField3 from "@mui/material/TextField";
2414
+ import TextField4 from "@mui/material/TextField";
1475
2415
  import MenuItem3 from "@mui/material/MenuItem";
1476
- import Chip5 from "@mui/material/Chip";
1477
- import CircularProgress4 from "@mui/material/CircularProgress";
2416
+ import Skeleton5 from "@mui/material/Skeleton";
1478
2417
  import Collapse2 from "@mui/material/Collapse";
1479
2418
  import IconButton4 from "@mui/material/IconButton";
1480
2419
  import Alert2 from "@mui/material/Alert";
1481
- import { KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
2420
+ import { alpha as alpha5 } from "@mui/material/styles";
2421
+ import { KeyboardArrowDown } from "@mui/icons-material";
1482
2422
  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;
2423
+ function actionTone(action) {
2424
+ if (!action) return "neutral";
2425
+ for (const [key, tone] of Object.entries(ACTION_TONES)) {
2426
+ if (action.toLowerCase().includes(key)) return tone;
1487
2427
  }
1488
2428
  return "info";
1489
2429
  }
2430
+ function prettyTableName(name) {
2431
+ if (!name) return "\u2014";
2432
+ return TABLE_LABELS[name] ?? name;
2433
+ }
1490
2434
  function formatDateTime(iso) {
1491
2435
  try {
1492
2436
  return new Date(iso).toLocaleString();
@@ -1496,28 +2440,99 @@ function formatDateTime(iso) {
1496
2440
  }
1497
2441
  function renderAuditLogBody(loading, entries) {
1498
2442
  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 })));
2443
+ return /* @__PURE__ */ React7.createElement(TableRow4, null, /* @__PURE__ */ React7.createElement(TableCell4, { colSpan: 6, sx: { py: 2 } }, /* @__PURE__ */ React7.createElement(Skeleton5, { variant: "rounded", height: 160 })));
1500
2444
  }
1501
2445
  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")));
2446
+ return /* @__PURE__ */ React7.createElement(TableRow4, null, /* @__PURE__ */ React7.createElement(TableCell4, { colSpan: 6 }, /* @__PURE__ */ React7.createElement(
2447
+ EmptyState,
2448
+ {
2449
+ message: "No audit events found",
2450
+ hint: "Try widening the filters or a different time window."
2451
+ }
2452
+ )));
1503
2453
  }
1504
- return entries.map((entry) => /* @__PURE__ */ React6.createElement(DetailRow, { key: entry.id, entry }));
2454
+ return entries.map((entry) => /* @__PURE__ */ React7.createElement(DetailRow, { key: entry.id, entry }));
1505
2455
  }
1506
- var ACTION_COLORS, DetailRow, AuditLog;
2456
+ var ACTION_TONES, TABLE_LABELS, DetailRow, AuditLog;
1507
2457
  var init_AuditLog = __esm({
1508
2458
  "src/components/AuditLog.tsx"() {
1509
2459
  "use strict";
1510
- ACTION_COLORS = {
2460
+ init_ui();
2461
+ ACTION_TONES = {
1511
2462
  created: "success",
1512
- deleted: "error",
2463
+ deleted: "danger",
1513
2464
  updated: "warning",
1514
- blocked: "error",
2465
+ blocked: "danger",
1515
2466
  unblocked: "success"
1516
2467
  };
2468
+ TABLE_LABELS = {
2469
+ LiteLLM_VerificationToken: "Key",
2470
+ LiteLLM_TeamTable: "Team",
2471
+ LiteLLM_UserTable: "User"
2472
+ };
1517
2473
  DetailRow = ({ entry }) => {
1518
2474
  const [open, setOpen] = useState5(false);
1519
2475
  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))))))));
2476
+ 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(
2477
+ IconButton4,
2478
+ {
2479
+ size: "small",
2480
+ onClick: () => setOpen((o) => !o),
2481
+ "aria-label": open ? "Hide changes" : "Show changes",
2482
+ "aria-expanded": open,
2483
+ sx: (theme) => ({
2484
+ color: theme.palette.text.secondary,
2485
+ transform: open ? "rotate(180deg)" : "none",
2486
+ transition: theme.transitions.create("transform")
2487
+ })
2488
+ },
2489
+ /* @__PURE__ */ React7.createElement(KeyboardArrowDown, { fontSize: "small" })
2490
+ )), /* @__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(
2491
+ Typography7,
2492
+ {
2493
+ variant: "body2",
2494
+ component: "code",
2495
+ color: "text.secondary",
2496
+ title: entry.object_id ?? void 0,
2497
+ sx: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }
2498
+ },
2499
+ entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "\u2014"
2500
+ )), /* @__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(
2501
+ Box7,
2502
+ {
2503
+ display: "flex",
2504
+ gap: 2,
2505
+ flexWrap: "wrap",
2506
+ sx: (theme) => ({
2507
+ p: 2,
2508
+ mb: 1.5,
2509
+ borderRadius: 1.5,
2510
+ bgcolor: alpha5(theme.palette.text.primary, theme.palette.mode === "dark" ? 0.05 : 0.03)
2511
+ })
2512
+ },
2513
+ entry.before_value && /* @__PURE__ */ React7.createElement(Box7, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React7.createElement(
2514
+ Typography7,
2515
+ {
2516
+ variant: "caption",
2517
+ color: "text.secondary",
2518
+ display: "block",
2519
+ mb: 0.75,
2520
+ sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" }
2521
+ },
2522
+ "Before"
2523
+ ), /* @__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))),
2524
+ entry.updated_values && /* @__PURE__ */ React7.createElement(Box7, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React7.createElement(
2525
+ Typography7,
2526
+ {
2527
+ variant: "caption",
2528
+ color: "text.secondary",
2529
+ display: "block",
2530
+ mb: 0.75,
2531
+ sx: { fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase" }
2532
+ },
2533
+ "After"
2534
+ ), /* @__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)))
2535
+ )))));
1521
2536
  };
1522
2537
  AuditLog = ({ api }) => {
1523
2538
  const [page, setPage] = useState5(0);
@@ -1533,68 +2548,80 @@ var init_AuditLog = __esm({
1533
2548
  );
1534
2549
  const entries = value?.audit_logs ?? [];
1535
2550
  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,
2551
+ return /* @__PURE__ */ React7.createElement(
2552
+ SectionCard,
1538
2553
  {
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,
1556
- {
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 }
2554
+ title: "Audit Log",
2555
+ subtitle: loading ? "Loading\u2026" : `${total.toLocaleString()} event${total === 1 ? "" : "s"}`,
2556
+ flush: true,
2557
+ actions: /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(
2558
+ TextField4,
2559
+ {
2560
+ size: "small",
2561
+ label: "Action",
2562
+ select: true,
2563
+ value: filters.action ?? "",
2564
+ onChange: (e) => {
2565
+ setFilters((f) => ({ ...f, action: e.target.value || void 0 }));
2566
+ setPage(0);
2567
+ },
2568
+ sx: { minWidth: 150 }
2569
+ },
2570
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "" }, "All actions"),
2571
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "created" }, "Created"),
2572
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "updated" }, "Updated"),
2573
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "deleted" }, "Deleted"),
2574
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "blocked" }, "Blocked")
2575
+ ), /* @__PURE__ */ React7.createElement(
2576
+ TextField4,
2577
+ {
2578
+ size: "small",
2579
+ label: "Table",
2580
+ select: true,
2581
+ value: filters.table_name ?? "",
2582
+ onChange: (e) => {
2583
+ setFilters((f) => ({ ...f, table_name: e.target.value || void 0 }));
2584
+ setPage(0);
2585
+ },
2586
+ sx: { minWidth: 150 }
2587
+ },
2588
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "" }, "All tables"),
2589
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "LiteLLM_VerificationToken" }, "Key"),
2590
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "LiteLLM_TeamTable" }, "Team"),
2591
+ /* @__PURE__ */ React7.createElement(MenuItem3, { value: "LiteLLM_UserTable" }, "User")
2592
+ ), /* @__PURE__ */ React7.createElement(
2593
+ TextField4,
2594
+ {
2595
+ size: "small",
2596
+ label: "Changed by",
2597
+ value: filters.changed_by ?? "",
2598
+ onChange: (e) => {
2599
+ setFilters((f) => ({ ...f, changed_by: e.target.value || void 0 }));
2600
+ setPage(0);
2601
+ },
2602
+ sx: { minWidth: 180 }
2603
+ }
2604
+ ))
1566
2605
  },
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
- ));
2606
+ error && /* @__PURE__ */ React7.createElement(Box7, { px: 2.5, pb: 2 }, /* @__PURE__ */ React7.createElement(Alert2, { severity: "error" }, error.message)),
2607
+ /* @__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)))),
2608
+ /* @__PURE__ */ React7.createElement(
2609
+ TablePagination,
2610
+ {
2611
+ component: "div",
2612
+ count: total,
2613
+ page,
2614
+ onPageChange: (_, p) => setPage(p),
2615
+ rowsPerPage: pageSize,
2616
+ onRowsPerPageChange: (e) => {
2617
+ setPageSize(Number(e.target.value));
2618
+ setPage(0);
2619
+ },
2620
+ rowsPerPageOptions: [10, 25, 50],
2621
+ sx: (theme) => ({ borderTop: `1px solid ${theme.palette.divider}` })
2622
+ }
2623
+ )
2624
+ );
1598
2625
  };
1599
2626
  }
1600
2627
  });
@@ -1604,15 +2631,15 @@ var LiteLLMPage_exports = {};
1604
2631
  __export(LiteLLMPage_exports, {
1605
2632
  LiteLLMPage: () => LiteLLMPage
1606
2633
  });
1607
- import React7, { useState as useState6, useCallback as useCallback2, useMemo as useMemo5 } from "react";
1608
- import Box7 from "@mui/material/Box";
2634
+ import React8, { useState as useState6, useCallback as useCallback2, useMemo as useMemo5 } from "react";
2635
+ import Box8 from "@mui/material/Box";
1609
2636
  import Snackbar from "@mui/material/Snackbar";
1610
2637
  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";
2638
+ import CircularProgress4 from "@mui/material/CircularProgress";
2639
+ import Typography8 from "@mui/material/Typography";
2640
+ import Paper5 from "@mui/material/Paper";
2641
+ import Tabs2 from "@mui/material/Tabs";
2642
+ import Tab2 from "@mui/material/Tab";
1616
2643
  import { useAsync as useAsync2, useAsyncRetry } from "react-use";
1617
2644
  import { useApi } from "@backstage/core-plugin-api";
1618
2645
  function initDateRange() {
@@ -1817,34 +2844,42 @@ var init_LiteLLMPage = __esm({
1817
2844
  );
1818
2845
  const isInitialLoading = userLoading && !userInfo;
1819
2846
  if (isInitialLoading) {
1820
- return /* @__PURE__ */ React7.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React7.createElement(CircularProgress5, null));
2847
+ return /* @__PURE__ */ React8.createElement(Box8, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React8.createElement(CircularProgress4, null));
1821
2848
  }
1822
2849
  if (userError || !userInfo) {
1823
2850
  const isProvisioningEnabled = userError?.body?.provisioning === true;
1824
2851
  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.")));
2852
+ 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
2853
  }
1827
- return /* @__PURE__ */ React7.createElement(Box7, { p: 3 }, /* @__PURE__ */ React7.createElement(Box7, { mb: 2 }, /* @__PURE__ */ React7.createElement(
2854
+ const pageTabs = /* @__PURE__ */ React8.createElement(
2855
+ Tabs2,
2856
+ {
2857
+ value: activeTab,
2858
+ onChange: (_, v) => setActiveTab(v),
2859
+ variant: "scrollable",
2860
+ scrollButtons: "auto",
2861
+ sx: {
2862
+ minHeight: 44,
2863
+ '& [class*="MuiTabs-indicator"]': { height: 2, borderRadius: "2px 2px 0 0" },
2864
+ '& [class*="MuiTab-root"]': { minHeight: 44, textTransform: "none", fontSize: 14 }
2865
+ }
2866
+ },
2867
+ /* @__PURE__ */ React8.createElement(Tab2, { label: "Overview", value: "overview" }),
2868
+ /* @__PURE__ */ React8.createElement(Tab2, { label: "Keys", value: "keys" }),
2869
+ /* @__PURE__ */ React8.createElement(Tab2, { label: "Teams", value: "teams" }),
2870
+ userInfo.can_view_audit && /* @__PURE__ */ React8.createElement(Tab2, { label: "Audit Log", value: "audit" })
2871
+ );
2872
+ return /* @__PURE__ */ React8.createElement(Box8, { sx: { p: 3, display: "flex", flexDirection: "column", gap: 2 } }, /* @__PURE__ */ React8.createElement(
1828
2873
  DashboardHeader,
1829
2874
  {
1830
2875
  userInfo,
1831
2876
  teams: teams ?? [],
1832
2877
  keys: keys ?? [],
1833
2878
  loading: userLoading || teamsLoading,
1834
- onGenerateKeyClick: () => setGenerateDialogOpen(true)
2879
+ onGenerateKeyClick: () => setGenerateDialogOpen(true),
2880
+ tabs: pageTabs
1835
2881
  }
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(
2882
+ ), activeTab === "overview" && /* @__PURE__ */ React8.createElement(
1848
2883
  UsageStats,
1849
2884
  {
1850
2885
  usage: usage ?? null,
@@ -1854,7 +2889,7 @@ var init_LiteLLMPage = __esm({
1854
2889
  loading: usageLoading,
1855
2890
  userInfo
1856
2891
  }
1857
- ), activeTab === "keys" && /* @__PURE__ */ React7.createElement(
2892
+ ), activeTab === "keys" && /* @__PURE__ */ React8.createElement(
1858
2893
  KeysTable,
1859
2894
  {
1860
2895
  keys: keys ?? [],
@@ -1868,7 +2903,7 @@ var init_LiteLLMPage = __esm({
1868
2903
  onDeleteKey: handleDeleteKey,
1869
2904
  onPruneExpiredKeys: handlePruneExpiredKeys
1870
2905
  }
1871
- ), activeTab === "teams" && /* @__PURE__ */ React7.createElement(
2906
+ ), activeTab === "teams" && /* @__PURE__ */ React8.createElement(
1872
2907
  TeamUsage,
1873
2908
  {
1874
2909
  teams: teams ?? [],
@@ -1879,7 +2914,7 @@ var init_LiteLLMPage = __esm({
1879
2914
  },
1880
2915
  getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
1881
2916
  }
1882
- ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ React7.createElement(AuditLog, { api }), /* @__PURE__ */ React7.createElement(
2917
+ ), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ React8.createElement(AuditLog, { api }), /* @__PURE__ */ React8.createElement(
1883
2918
  GenerateKeyDialog,
1884
2919
  {
1885
2920
  open: generateDialogOpen,
@@ -1892,7 +2927,7 @@ var init_LiteLLMPage = __esm({
1892
2927
  onGenerateKey: handleGenerateKey,
1893
2928
  onGetConfig: () => api.getConfig()
1894
2929
  }
1895
- ), /* @__PURE__ */ React7.createElement(
2930
+ ), /* @__PURE__ */ React8.createElement(
1896
2931
  Snackbar,
1897
2932
  {
1898
2933
  open: !!snackbar,
@@ -1900,7 +2935,7 @@ var init_LiteLLMPage = __esm({
1900
2935
  onClose: () => setSnackbar(null),
1901
2936
  anchorOrigin: { vertical: "bottom", horizontal: "right" }
1902
2937
  },
1903
- snackbar ? /* @__PURE__ */ React7.createElement(Alert3, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
2938
+ snackbar ? /* @__PURE__ */ React8.createElement(Alert3, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
1904
2939
  ));
1905
2940
  };
1906
2941
  }
@@ -1908,7 +2943,7 @@ var init_LiteLLMPage = __esm({
1908
2943
 
1909
2944
  // src/plugin.tsx
1910
2945
  init_api();
1911
- import React8 from "react";
2946
+ import React9 from "react";
1912
2947
  import { TrendingUp as TrendingUpIcon } from "@mui/icons-material";
1913
2948
  import {
1914
2949
  createFrontendPlugin,
@@ -1927,10 +2962,10 @@ var liteLlmPage = PageBlueprint.make({
1927
2962
  params: {
1928
2963
  path: "/litellm",
1929
2964
  title: "LiteLLM",
1930
- icon: /* @__PURE__ */ React8.createElement(TrendingUpIcon, null),
2965
+ icon: /* @__PURE__ */ React9.createElement(TrendingUpIcon, null),
1931
2966
  loader: async () => {
1932
2967
  const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
1933
- return /* @__PURE__ */ React8.createElement(LiteLLMPage2, null);
2968
+ return /* @__PURE__ */ React9.createElement(LiteLLMPage2, null);
1934
2969
  }
1935
2970
  }
1936
2971
  });
@@ -1949,15 +2984,15 @@ init_TeamUsage();
1949
2984
  // src/components/LiteLLMHomeWidget.tsx
1950
2985
  init_api();
1951
2986
  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";
2987
+ import React10, { useState as useState7, useEffect as useEffect2 } from "react";
2988
+ import Paper6 from "@mui/material/Paper";
2989
+ import Box9 from "@mui/material/Box";
2990
+ import Typography9 from "@mui/material/Typography";
2991
+ import FormControl from "@mui/material/FormControl";
2992
+ import Select from "@mui/material/Select";
1958
2993
  import MenuItem4 from "@mui/material/MenuItem";
1959
2994
  import Grid2 from "@mui/material/Grid";
1960
- import CircularProgress6 from "@mui/material/CircularProgress";
2995
+ import CircularProgress5 from "@mui/material/CircularProgress";
1961
2996
  import Alert4 from "@mui/material/Alert";
1962
2997
  import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
1963
2998
  import { useApi as useApi2 } from "@backstage/core-plugin-api";
@@ -1973,7 +3008,7 @@ function presetToDateRange(preset) {
1973
3008
  }
1974
3009
  return { start, end };
1975
3010
  }
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));
3011
+ 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
3012
  var LiteLLMHomeWidget = ({
1978
3013
  defaultPeriod = "7d",
1979
3014
  title = "LiteLLM Usage"
@@ -2022,17 +3057,17 @@ var LiteLLMHomeWidget = ({
2022
3057
  spend: d.spend
2023
3058
  }));
2024
3059
  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,
3060
+ 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(
3061
+ Select,
2027
3062
  {
2028
3063
  value: period,
2029
3064
  onChange: (e) => setPeriod(e.target.value),
2030
3065
  displayEmpty: true
2031
3066
  },
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(
3067
+ /* @__PURE__ */ React10.createElement(MenuItem4, { value: "today" }, "Today"),
3068
+ /* @__PURE__ */ React10.createElement(MenuItem4, { value: "7d" }, "7d"),
3069
+ /* @__PURE__ */ React10.createElement(MenuItem4, { value: "30d" }, "30d")
3070
+ ))), 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
3071
  Area3,
2037
3072
  {
2038
3073
  type: "monotone",