codex-lb 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

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.
app/static/index.js CHANGED
@@ -12,7 +12,8 @@
12
12
  usageSummary: "/api/usage/summary",
13
13
  usageWindow: (window) =>
14
14
  `/api/usage/window?window=${encodeURIComponent(window)}`,
15
- requestLogs: (limit) => `/api/request-logs?limit=${limit}`,
15
+ requestLogs: "/api/request-logs",
16
+ requestLogOptions: "/api/request-logs/options",
16
17
  oauthStart: "/api/oauth/start",
17
18
  oauthStatus: "/api/oauth/status",
18
19
  oauthComplete: "/api/oauth/complete",
@@ -79,9 +80,20 @@
79
80
  ok: "active",
80
81
  rate_limit: "limited",
81
82
  quota: "exceeded",
82
- error: "deactivated",
83
+ error: "error",
83
84
  };
84
85
 
86
+ const MODEL_OPTION_DELIMITER = ":::";
87
+
88
+ const createDefaultRequestFilters = () => ({
89
+ search: "",
90
+ timeframe: "all",
91
+ accountIds: [],
92
+ modelOptions: [],
93
+ statuses: [],
94
+ minCost: "",
95
+ });
96
+
85
97
  const KNOWN_PLAN_TYPES = new Set([
86
98
  "free",
87
99
  "plus",
@@ -180,11 +192,16 @@
180
192
  minimumFractionDigits: 2,
181
193
  maximumFractionDigits: 2,
182
194
  });
183
- const timeLongFormatter = new Intl.DateTimeFormat("en-US", {
195
+ const timeFormatter = new Intl.DateTimeFormat("en-US", {
184
196
  hour: "2-digit",
185
197
  minute: "2-digit",
186
198
  second: "2-digit",
187
199
  });
200
+ const dateFormatter = new Intl.DateTimeFormat("en-US", {
201
+ year: "numeric",
202
+ month: "2-digit",
203
+ day: "2-digit",
204
+ });
188
205
 
189
206
  const createEmptyDashboardData = () => ({
190
207
  lastSyncAt: "",
@@ -373,9 +390,12 @@
373
390
  const formatTimeLong = (iso) => {
374
391
  const date = parseDate(iso);
375
392
  if (!date) {
376
- return "--";
393
+ return { time: "--", date: "--" };
377
394
  }
378
- return timeLongFormatter.format(date);
395
+ return {
396
+ time: timeFormatter.format(date),
397
+ date: dateFormatter.format(date),
398
+ };
379
399
  };
380
400
 
381
401
  const formatRelative = (ms) => {
@@ -506,6 +526,33 @@
506
526
  };
507
527
  const routingLabel = (strategy) => ROUTING_LABELS[strategy] || "unknown";
508
528
  const errorLabel = (code) => ERROR_LABELS[code] || "--";
529
+ const calculateProgressClass = (status, remainingPercent) => {
530
+ if (status === "exceeded") return "error";
531
+ if (status === "paused" || status === "deactivated") return "";
532
+ const percent = toNumber(remainingPercent) || 0;
533
+ if (percent <= 20) return "error";
534
+ if (percent <= 50) return "limited";
535
+ return "success";
536
+ };
537
+ const calculateProgressTextClass = (status, remainingPercent) => {
538
+ const cls = calculateProgressClass(status, remainingPercent);
539
+ return cls ? `text-${cls}` : "";
540
+ };
541
+
542
+ const calculateTextUsageClass = (status, remainingPercent) => {
543
+ if (status === "exceeded") return "error";
544
+ // For text, we show usage color even if paused. Only deactivated is plain.
545
+ if (status === "deactivated") return "";
546
+ const percent = toNumber(remainingPercent) || 0;
547
+ if (percent <= 20) return "error";
548
+ if (percent <= 50) return "limited";
549
+ return "success";
550
+ };
551
+
552
+ const calculateTextUsageTextClass = (status, remainingPercent) => {
553
+ const cls = calculateTextUsageClass(status, remainingPercent);
554
+ return cls ? `text-${cls}` : "";
555
+ };
509
556
  const progressClass = (status) => PROGRESS_CLASS_BY_STATUS[status] || "";
510
557
 
511
558
  const normalizeSearchInput = (value) =>
@@ -741,7 +788,7 @@
741
788
  typeof windowMinutes === "number" && windowMinutes > 0
742
789
  ? windowMinutes / 60
743
790
  : 24 * 7;
744
- return Math.round(numeric / hours);
791
+ return numeric / hours;
745
792
  };
746
793
 
747
794
  const countByStatus = (accounts) =>
@@ -937,9 +984,9 @@
937
984
  const entries =
938
985
  window.key === "primary"
939
986
  ? applySecondaryExhaustedToPrimary(
940
- rawEntries,
941
- secondaryExhaustedAccounts,
942
- )
987
+ rawEntries,
988
+ secondaryExhaustedAccounts,
989
+ )
943
990
  : rawEntries;
944
991
  const remaining =
945
992
  hasPrimaryAdjustments
@@ -954,21 +1001,40 @@
954
1001
  window.key,
955
1002
  );
956
1003
  const gradient = buildDonutGradient(items, capacity);
957
- const legendItems = items.map((item) => ({
958
- label: item.label,
959
- detailLabel: "Remaining",
960
- detailValue: formatPercent(item.remainingPercent),
961
- color: item.color,
962
- }));
963
- if (capacity > 0 && consumed > 0) {
1004
+ const legendItems = items.map((item) => {
1005
+ const percent = item.remainingPercent;
1006
+ let valueClass = "success";
1007
+ if (percent <= 20) {
1008
+ valueClass = "error";
1009
+ } else if (percent <= 50) {
1010
+ valueClass = "limited";
1011
+ }
1012
+ return {
1013
+ label: truncateText(item.label, 28),
1014
+ fullLabel: item.label,
1015
+ detailLabel: "Remaining",
1016
+ detailValue: formatPercent(item.remainingPercent),
1017
+ valueClass,
1018
+ color: item.color,
1019
+ };
1020
+ });
1021
+ if (capacity > 0) {
964
1022
  const consumedPercent = Math.min(
965
1023
  100,
966
1024
  Math.max(0, (consumed / capacity) * 100),
967
1025
  );
1026
+ let consumedClass = "success";
1027
+ if (consumedPercent >= 80) {
1028
+ consumedClass = "error";
1029
+ } else if (consumedPercent >= 50) {
1030
+ consumedClass = "limited";
1031
+ }
968
1032
  legendItems.push({
969
1033
  label: "Consumed",
1034
+ fullLabel: "Consumed",
970
1035
  detailLabel: "",
971
1036
  detailValue: formatPercent(consumedPercent),
1037
+ valueClass: consumedClass,
972
1038
  color: CONSUMED_COLOR,
973
1039
  });
974
1040
  }
@@ -995,7 +1061,7 @@
995
1061
  },
996
1062
  remaining: remainingRounded,
997
1063
  remainingText: formatPercent(secondaryRemaining),
998
- progressClass: progressClass(account.status),
1064
+ progressClass: calculateProgressClass(account.status, secondaryRemaining),
999
1065
  marquee: account.status === "deactivated",
1000
1066
  meta: formatQuotaResetMeta(
1001
1067
  account.resetAtSecondary,
@@ -1009,6 +1075,9 @@
1009
1075
  const rawError = request.errorMessage || request.errorCode || "";
1010
1076
  const accountLabel = formatAccountLabel(request.accountId, accounts);
1011
1077
  const modelLabel = formatModelLabel(request.model, request.reasoningEffort);
1078
+ const totalTokens = formatCompactNumber(request.tokens);
1079
+ const cachedInputTokens = toNumber(request.cachedInputTokens);
1080
+ const cachedTokens = cachedInputTokens > 0 ? formatCompactNumber(cachedInputTokens) : null;
1012
1081
  return {
1013
1082
  key: `${request.requestId}-${request.timestamp}`,
1014
1083
  requestId: request.requestId,
@@ -1019,10 +1088,16 @@
1019
1088
  class: requestStatusClass(request.status),
1020
1089
  label: requestStatusLabel(request.status),
1021
1090
  },
1022
- tokens: formatTokensWithCached(request.tokens, request.cachedInputTokens),
1091
+ tokens: {
1092
+ total: totalTokens,
1093
+ cached: cachedTokens,
1094
+ },
1095
+ tokensTooltip: formatTokensWithCached(request.tokens, request.cachedInputTokens),
1023
1096
  cost: formatCurrency(request.cost),
1024
1097
  error: rawError ? truncateText(rawError, 80) : "--",
1025
1098
  errorTitle: rawError,
1099
+ isTruncated: rawError.length > 20,
1100
+ isErrorPlaceholder: !rawError,
1026
1101
  };
1027
1102
  });
1028
1103
 
@@ -1191,8 +1266,107 @@
1191
1266
  const fetchUsageWindow = async (window) =>
1192
1267
  fetchJson(API_ENDPOINTS.usageWindow(window), `usage window (${window})`);
1193
1268
 
1194
- const fetchRequestLogs = async (limit) =>
1195
- fetchJson(API_ENDPOINTS.requestLogs(limit), "request logs");
1269
+ const parseMinCostUsd = (value) => {
1270
+ if (value === null || value === undefined) {
1271
+ return null;
1272
+ }
1273
+ const asString = String(value).trim();
1274
+ if (!asString) {
1275
+ return null;
1276
+ }
1277
+ const numeric = Number(asString);
1278
+ return Number.isFinite(numeric) ? numeric : null;
1279
+ };
1280
+
1281
+ const buildSinceIsoFromTimeframe = (timeframe) => {
1282
+ const now = Date.now();
1283
+ if (timeframe === "1h") {
1284
+ return new Date(now - 60 * 60 * 1000).toISOString();
1285
+ }
1286
+ if (timeframe === "24h") {
1287
+ return new Date(now - 24 * 60 * 60 * 1000).toISOString();
1288
+ }
1289
+ if (timeframe === "7d") {
1290
+ return new Date(now - 7 * 24 * 60 * 60 * 1000).toISOString();
1291
+ }
1292
+ return null;
1293
+ };
1294
+
1295
+ const buildRequestLogsQueryParams = ({ filters, pagination }) => {
1296
+ const params = {
1297
+ limit: pagination?.limit,
1298
+ offset: pagination?.offset,
1299
+ };
1300
+ if (filters?.search) {
1301
+ params.search = filters.search;
1302
+ }
1303
+ if (Array.isArray(filters?.accountIds) && filters.accountIds.length) {
1304
+ params.accountId = filters.accountIds.filter(Boolean);
1305
+ }
1306
+ if (Array.isArray(filters?.modelOptions) && filters.modelOptions.length) {
1307
+ params.modelOption = filters.modelOptions.filter(Boolean);
1308
+ }
1309
+ if (Array.isArray(filters?.statuses) && filters.statuses.length) {
1310
+ params.status = filters.statuses.filter(Boolean);
1311
+ }
1312
+ const since = buildSinceIsoFromTimeframe(filters?.timeframe);
1313
+ if (since) {
1314
+ params.since = since;
1315
+ }
1316
+ return params;
1317
+ };
1318
+
1319
+ const applyClientSideRequestFilters = (requests, filters) => {
1320
+ const minCostUsd = parseMinCostUsd(filters?.minCost);
1321
+ if (minCostUsd === null) {
1322
+ return requests;
1323
+ }
1324
+ return (requests || []).filter((entry) => (entry.cost || 0) >= minCostUsd);
1325
+ };
1326
+
1327
+ const buildQueryString = (params) => {
1328
+ const searchParams = new URLSearchParams();
1329
+ if (!params || typeof params !== "object") {
1330
+ return "";
1331
+ }
1332
+ for (const [key, value] of Object.entries(params)) {
1333
+ if (value === null || value === undefined) {
1334
+ continue;
1335
+ }
1336
+ if (Array.isArray(value)) {
1337
+ const filtered = value
1338
+ .map((item) => String(item ?? "").trim())
1339
+ .filter(Boolean);
1340
+ for (const item of filtered) {
1341
+ searchParams.append(key, item);
1342
+ }
1343
+ continue;
1344
+ }
1345
+ if (typeof value === "string" && value.trim() === "") {
1346
+ continue;
1347
+ }
1348
+ searchParams.append(key, String(value));
1349
+ }
1350
+ return searchParams.toString();
1351
+ };
1352
+
1353
+ const fetchRequestLogs = async (params) => {
1354
+ const query = buildQueryString(params);
1355
+ return fetchJson(
1356
+ query ? `${API_ENDPOINTS.requestLogs}?${query}` : API_ENDPOINTS.requestLogs,
1357
+ "request logs",
1358
+ );
1359
+ };
1360
+
1361
+ const fetchRequestLogOptions = async (params) => {
1362
+ const query = buildQueryString(params);
1363
+ return fetchJson(
1364
+ query
1365
+ ? `${API_ENDPOINTS.requestLogOptions}?${query}`
1366
+ : API_ENDPOINTS.requestLogOptions,
1367
+ "request log options",
1368
+ );
1369
+ };
1196
1370
 
1197
1371
  const normalizeSettingsPayload = (payload) => ({
1198
1372
  stickyThreadsEnabled: Boolean(payload?.stickyThreadsEnabled),
@@ -1212,6 +1386,24 @@
1212
1386
  ui: createUiConfig(),
1213
1387
  dashboardData: createEmptyDashboardData(),
1214
1388
  dashboard: createEmptyDashboardView(),
1389
+
1390
+ filtersDraft: createDefaultRequestFilters(),
1391
+ filtersApplied: createDefaultRequestFilters(),
1392
+ pagination: {
1393
+ limit: 25,
1394
+ offset: 0,
1395
+ },
1396
+ recentRequestsState: {
1397
+ isLoading: false,
1398
+ error: "",
1399
+ },
1400
+ requestLogOptions: {
1401
+ accountIds: [],
1402
+ modelOptions: [],
1403
+ isLoading: false,
1404
+ error: "",
1405
+ },
1406
+
1215
1407
  settings: {
1216
1408
  stickyThreadsEnabled: false,
1217
1409
  preferEarlierResetAccounts: false,
@@ -1259,12 +1451,19 @@
1259
1451
  isLoading: true,
1260
1452
  hasInitialized: false,
1261
1453
  refreshPromise: null,
1454
+
1262
1455
  async init() {
1263
1456
  if (this.hasInitialized) {
1264
1457
  return;
1265
1458
  }
1266
1459
  this.hasInitialized = true;
1267
1460
  this.view = getViewFromPath(window.location.pathname);
1461
+
1462
+ this.$watch("pagination.limit", () => {
1463
+ this.pagination.offset = 0;
1464
+ this.refreshRequests();
1465
+ });
1466
+
1268
1467
  await this.loadData();
1269
1468
  this.syncTitle();
1270
1469
  this.syncUrl(true);
@@ -1283,6 +1482,115 @@
1283
1482
  }
1284
1483
  });
1285
1484
  },
1485
+
1486
+ async refreshRequests() {
1487
+ try {
1488
+ this.recentRequestsState.isLoading = true;
1489
+ this.recentRequestsState.error = "";
1490
+ const params = buildRequestLogsQueryParams({
1491
+ filters: this.filtersApplied,
1492
+ pagination: this.pagination,
1493
+ });
1494
+ const requestsData = await fetchRequestLogs(params);
1495
+ const normalized = normalizeRequestLogsPayload(requestsData);
1496
+ const requests = applyClientSideRequestFilters(
1497
+ normalized,
1498
+ this.filtersApplied,
1499
+ );
1500
+
1501
+ this.dashboardData.recentRequests = requests;
1502
+
1503
+ this.dashboard = buildDashboardView(this);
1504
+ } catch (err) {
1505
+ this.recentRequestsState.error =
1506
+ err?.message || "Failed to refresh requests.";
1507
+ console.error("Failed to refresh requests:", err);
1508
+ } finally {
1509
+ this.recentRequestsState.isLoading = false;
1510
+ }
1511
+ },
1512
+
1513
+ applyFilters() {
1514
+ this.pagination.offset = 0;
1515
+ const accountIds = Array.isArray(this.filtersDraft.accountIds)
1516
+ ? [...new Set(this.filtersDraft.accountIds.map(String).filter(Boolean))]
1517
+ : [];
1518
+ const modelOptions = Array.isArray(this.filtersDraft.modelOptions)
1519
+ ? [...new Set(this.filtersDraft.modelOptions.map(String).filter(Boolean))]
1520
+ : [];
1521
+ const statuses = Array.isArray(this.filtersDraft.statuses)
1522
+ ? [...new Set(this.filtersDraft.statuses.map(String).filter(Boolean))]
1523
+ : [];
1524
+ this.filtersApplied = {
1525
+ ...createDefaultRequestFilters(),
1526
+ ...this.filtersDraft,
1527
+ accountIds,
1528
+ modelOptions,
1529
+ statuses,
1530
+ };
1531
+ this.refreshRequests();
1532
+ },
1533
+
1534
+ resetFilters() {
1535
+ this.filtersDraft = createDefaultRequestFilters();
1536
+ this.applyFilters();
1537
+ },
1538
+
1539
+ accountFilterLabel(accountId) {
1540
+ return formatAccountLabel(accountId, this.accounts.rows);
1541
+ },
1542
+
1543
+ modelOptionValue(option) {
1544
+ const model = String(option?.model || "").trim();
1545
+ const effort = String(option?.reasoningEffort || "").trim();
1546
+ return `${model}${MODEL_OPTION_DELIMITER}${effort}`;
1547
+ },
1548
+
1549
+ modelOptionLabel(option) {
1550
+ return formatModelLabel(option?.model, option?.reasoningEffort);
1551
+ },
1552
+
1553
+ toggleMultiSelectValue(listKey, rawValue) {
1554
+ const value = String(rawValue ?? "").trim();
1555
+ if (!value) {
1556
+ return;
1557
+ }
1558
+ const current = Array.isArray(this.filtersDraft[listKey])
1559
+ ? [...this.filtersDraft[listKey]]
1560
+ : [];
1561
+ const index = current.indexOf(value);
1562
+ if (index >= 0) {
1563
+ current.splice(index, 1);
1564
+ } else {
1565
+ current.push(value);
1566
+ }
1567
+ this.filtersDraft = { ...this.filtersDraft, [listKey]: current };
1568
+ },
1569
+
1570
+ multiSelectSummary(values, emptyLabel, singularLabel, pluralLabel) {
1571
+ const list = Array.isArray(values)
1572
+ ? values.map((value) => String(value ?? "").trim()).filter(Boolean)
1573
+ : [];
1574
+ if (list.length === 0) {
1575
+ return emptyLabel;
1576
+ }
1577
+ if (list.length === 1) {
1578
+ return `1 ${singularLabel}`;
1579
+ }
1580
+ const plural = pluralLabel ? String(pluralLabel) : `${singularLabel}s`;
1581
+ return `${list.length} ${plural}`;
1582
+ },
1583
+
1584
+ timeframeLabel(value) {
1585
+ const labels = {
1586
+ all: "All time",
1587
+ "1h": "Last 1h",
1588
+ "24h": "Last 24h",
1589
+ "7d": "Last 7d",
1590
+ };
1591
+ return labels[value] || "All time";
1592
+ },
1593
+
1286
1594
  async loadData() {
1287
1595
  try {
1288
1596
  await this.refreshAll({ silent: true });
@@ -1305,22 +1613,32 @@
1305
1613
  return this.refreshPromise;
1306
1614
  }
1307
1615
  const { preferredId, silent = false } = options;
1616
+ this.requestLogOptions.isLoading = true;
1617
+ this.requestLogOptions.error = "";
1308
1618
  this.refreshPromise = (async () => {
1619
+ const params = buildRequestLogsQueryParams({
1620
+ filters: this.filtersApplied,
1621
+ pagination: this.pagination,
1622
+ });
1623
+
1309
1624
  const [
1310
1625
  accountsResult,
1311
1626
  summaryResult,
1312
1627
  primaryResult,
1313
1628
  secondaryResult,
1314
1629
  requestLogsResult,
1630
+ requestLogOptionsResult,
1315
1631
  settingsResult,
1316
1632
  ] = await Promise.allSettled([
1317
1633
  fetchAccounts(),
1318
1634
  fetchUsageSummary(),
1319
1635
  fetchUsageWindow("primary"),
1320
1636
  fetchUsageWindow("secondary"),
1321
- fetchRequestLogs(50),
1637
+ fetchRequestLogs(params),
1638
+ fetchRequestLogOptions({}),
1322
1639
  fetchSettings(),
1323
1640
  ]);
1641
+
1324
1642
  const errors = [];
1325
1643
  if (accountsResult.status !== "fulfilled") {
1326
1644
  throw accountsResult.reason;
@@ -1352,6 +1670,21 @@
1352
1670
  errors.push(requestLogsResult.reason);
1353
1671
  }
1354
1672
 
1673
+ if (requestLogOptionsResult.status === "fulfilled") {
1674
+ const payload = requestLogOptionsResult.value;
1675
+ this.requestLogOptions.accountIds = Array.isArray(payload?.accountIds)
1676
+ ? payload.accountIds
1677
+ : [];
1678
+ this.requestLogOptions.modelOptions = Array.isArray(payload?.modelOptions)
1679
+ ? payload.modelOptions
1680
+ : [];
1681
+ } else {
1682
+ this.requestLogOptions.error =
1683
+ requestLogOptionsResult.reason?.message ||
1684
+ "Failed to load request log options.";
1685
+ errors.push(requestLogOptionsResult.reason);
1686
+ }
1687
+
1355
1688
  const settings =
1356
1689
  settingsResult.status === "fulfilled" ? settingsResult.value : null;
1357
1690
  if (settingsResult.status === "rejected") {
@@ -1369,7 +1702,10 @@
1369
1702
  summary,
1370
1703
  primaryUsage,
1371
1704
  secondaryUsage,
1372
- requestLogs,
1705
+ requestLogs: applyClientSideRequestFilters(
1706
+ requestLogs,
1707
+ this.filtersApplied,
1708
+ ),
1373
1709
  settings,
1374
1710
  },
1375
1711
  preferredId,
@@ -1391,6 +1727,7 @@
1391
1727
  await this.refreshPromise;
1392
1728
  } finally {
1393
1729
  this.refreshPromise = null;
1730
+ this.requestLogOptions.isLoading = false;
1394
1731
  }
1395
1732
  },
1396
1733
  applyData(data, preferredId) {
@@ -1700,12 +2037,33 @@
1700
2037
  window.open(this.authDialog.verificationUrl, "_blank", "noopener");
1701
2038
  }
1702
2039
  },
1703
- async copyToClipboard(value, label) {
1704
- if (!value) {
1705
- return;
2040
+ calculateProgressClass(status, remainingPercent) {
2041
+ return calculateProgressClass(status, remainingPercent);
2042
+ },
2043
+ calculateProgressTextClass(status, remainingPercent) {
2044
+ return calculateProgressTextClass(status, remainingPercent);
2045
+ },
2046
+ async copyToClipboard(value, label, e) {
2047
+ if (!value) return;
2048
+
2049
+ // Localized feedback in the button
2050
+ let btn = null;
2051
+ let originalText = "";
2052
+ if (e && e.target) {
2053
+ btn = e.target.tagName === "BUTTON" ? e.target : e.target.closest("button");
2054
+ if (btn) {
2055
+ originalText = btn.textContent;
2056
+ btn.textContent = "Copied!";
2057
+ btn.classList.add("copy-success");
2058
+ window.setTimeout(() => {
2059
+ btn.textContent = originalText;
2060
+ btn.classList.remove("copy-success");
2061
+ }, 4000);
2062
+ }
1706
2063
  }
2064
+
1707
2065
  try {
1708
- if (navigator.clipboard?.writeText) {
2066
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1709
2067
  await navigator.clipboard.writeText(value);
1710
2068
  } else {
1711
2069
  const textarea = document.createElement("textarea");
@@ -1718,29 +2076,26 @@
1718
2076
  document.execCommand("copy");
1719
2077
  document.body.removeChild(textarea);
1720
2078
  }
1721
- if (this.authDialog.open) {
1722
- const previousLabel = this.authDialog.statusLabel;
1723
- this.authDialog.statusLabel = `${label} copied.`;
1724
- window.setTimeout(() => {
1725
- if (this.authDialog.statusLabel === `${label} copied.`) {
1726
- this.authDialog.statusLabel = previousLabel;
1727
- }
1728
- }, 2000);
1729
- } else {
2079
+
2080
+ // Only show message box if auth dialog is not open and button feedback wasn't possible
2081
+ if (!this.authDialog.open && !btn) {
1730
2082
  this.openMessageBox({
1731
2083
  tone: "success",
1732
2084
  title: "Copied",
1733
2085
  message: `${label} copied to clipboard.`,
1734
2086
  });
1735
2087
  }
1736
- } catch (error) {
1737
- if (this.authDialog.open) {
1738
- this.authDialog.statusLabel = "Copy failed.";
1739
- } else {
2088
+ } catch (err) {
2089
+ console.error("Clipboard error:", err);
2090
+ if (btn) {
2091
+ btn.textContent = "Failed";
2092
+ btn.classList.remove("copy-success");
2093
+ window.setTimeout(() => { btn.textContent = originalText; }, 2000);
2094
+ } else if (!this.authDialog.open) {
1740
2095
  this.openMessageBox({
1741
2096
  tone: "error",
1742
2097
  title: "Copy failed",
1743
- message: "Unable to copy to clipboard.",
2098
+ message: `Could not copy ${label}.`,
1744
2099
  });
1745
2100
  }
1746
2101
  }
@@ -1896,15 +2251,19 @@
1896
2251
  },
1897
2252
  get statusItems() {
1898
2253
  const lastSync = formatTimeLong(this.dashboardData.lastSyncAt);
2254
+ const lastSyncLabel =
2255
+ lastSync && lastSync.time && lastSync.time !== "--"
2256
+ ? `${lastSync.time} · ${lastSync.date}`
2257
+ : "--";
1899
2258
  const items =
1900
2259
  this.view === "accounts"
1901
2260
  ? [
1902
2261
  `Selection: ${this.accounts.selectedId || "--"}`,
1903
2262
  `Rotation: ${this.dashboardData.routing?.rotationEnabled ? "enabled" : "disabled"}`,
1904
- `Last sync: ${lastSync}`,
2263
+ `Last sync: ${lastSyncLabel}`,
1905
2264
  ]
1906
2265
  : [
1907
- `Last sync: ${lastSync}`,
2266
+ `Last sync: ${lastSyncLabel}`,
1908
2267
  `Routing: ${routingLabel(this.dashboardData.routing?.strategy)}`,
1909
2268
  `Backend: ${this.backendPath}`,
1910
2269
  ];
@@ -2084,6 +2443,7 @@
2084
2443
  statusLabel,
2085
2444
  requestStatusLabel,
2086
2445
  requestStatusClass,
2446
+ calculateTextUsageTextClass,
2087
2447
  progressClass,
2088
2448
  planLabel,
2089
2449
  routingLabel,
@@ -2100,7 +2460,14 @@
2100
2460
  formatQuotaResetLabel,
2101
2461
  formatAccessTokenLabel,
2102
2462
  formatRefreshTokenLabel,
2463
+ formatAccessTokenLabel,
2464
+ formatRefreshTokenLabel,
2103
2465
  formatIdTokenLabel,
2466
+ theme: localStorage.getItem('theme') || 'dark',
2467
+ toggleTheme() {
2468
+ this.theme = this.theme === 'dark' ? 'light' : 'dark';
2469
+ localStorage.setItem('theme', this.theme);
2470
+ },
2104
2471
  }));
2105
2472
  };
2106
2473