@ychris12138/dsh-usage-stats 0.2.10 → 0.3.1

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/lib/client.js CHANGED
@@ -59,12 +59,16 @@ window.__ModuleLoader__.load({
59
59
  ".usg_providerPickerLabel{color:var(--dsw-alias-label-tertiary);flex:none}",
60
60
  ".usg_providerSelect{box-sizing:border-box;min-width:0;flex:1;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base));border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px 6px;font:inherit;font-size:12px;line-height:18px}",
61
61
  ".usg_accountGrid{flex-direction:column;gap:8px;display:flex}",
62
+ ".usg_exportLinks{display:flex;flex-wrap:wrap;gap:6px}",
63
+ ".usg_exportLink{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l1,transparent);border:1px solid var(--dsw-alias-border-l2);border-radius:7px;padding:4px 7px;font-size:11px;line-height:16px;text-decoration:none}",
64
+ ".usg_exportLink:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
62
65
  ".usg_accountCard{--usg-providerAccent:#1f6feb;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:linear-gradient(135deg,color-mix(in srgb,var(--usg-providerAccent) 8%,transparent),transparent 42%);border-radius:12px;padding:10px 11px;display:flex;flex-direction:column;gap:9px}",
63
66
  ".usg_accountCard[data-provider=deepseek],.usg_accountCard[data-provider=deepseek-official]{--usg-providerAccent:#1f6feb}",
64
67
  ".usg_accountCard[data-provider=opencode-go]{--usg-providerAccent:#00a67d}",
65
68
  ".usg_accountCard[data-provider=ollama],.usg_accountCard[data-adapter=ollama]{--usg-providerAccent:#d97706}",
66
69
  ".usg_accountCard[data-provider=zai],.usg_accountCard[data-provider=zai-coding-cn]{--usg-providerAccent:#7656e8}",
67
70
  ".usg_accountCard[data-provider=openrouter]{--usg-providerAccent:#6366f1}",
71
+ ".usg_accountCard[data-provider=orcarouter]{--usg-providerAccent:#0891b2}",
68
72
  ".usg_accountCard[data-provider=moonshotai],.usg_accountCard[data-provider=moonshotai-cn],.usg_accountCard[data-provider=kimi],.usg_accountCard[data-provider=kimi-coding]{--usg-providerAccent:#e07a1f}",
69
73
  ".usg_accountHead{align-items:center;gap:8px;display:flex}",
70
74
  ".usg_accountMark{width:24px;height:24px;color:#fff;background:var(--usg-providerAccent);border-radius:7px;justify-content:center;align-items:center;font-size:10px;font-weight:700;display:flex;box-shadow:0 4px 12px color-mix(in srgb,var(--usg-providerAccent) 25%,transparent)}",
@@ -73,6 +77,9 @@ window.__ModuleLoader__.load({
73
77
  ".usg_accountPlan{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:10px;line-height:14px;overflow:hidden}",
74
78
  ".usg_accountStatus{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-fill-l2);border-radius:999px;padding:2px 7px;font-size:10px;line-height:16px;white-space:nowrap}",
75
79
  ".usg_accountStatus[data-status=ok]{color:var(--usg-providerAccent);background:color-mix(in srgb,var(--usg-providerAccent) 12%,transparent)}",
80
+ ".usg_accountHealth{border-top:1px solid var(--dsw-alias-border-l1);grid-template-columns:auto minmax(0,1fr);gap:3px 10px;margin:2px 0 0;padding-top:7px;display:grid}",
81
+ ".usg_accountHealth dt{color:var(--dsw-alias-label-caption);font-size:10px;line-height:15px}",
82
+ ".usg_accountHealth dd{color:var(--dsw-alias-label-secondary);min-width:0;margin:0;font-size:10px;line-height:15px;text-align:right;overflow-wrap:anywhere}",
76
83
  ".usg_quotaList{flex-direction:column;gap:8px;display:flex}",
77
84
  ".usg_quotaRow{display:flex;flex-direction:column;gap:4px}",
78
85
  ".usg_quotaMeta{align-items:baseline;gap:8px;display:flex}",
@@ -116,8 +123,8 @@ window.__ModuleLoader__.load({
116
123
  ".usg_day{width:100%;min-height:30px;align-items:center;gap:8px;border:0;background:0 0;border-bottom:1px solid var(--dsw-alias-border-l1);padding:5px 0;font:inherit;text-align:left;cursor:pointer;display:flex}",
117
124
  ".usg_day:last-child{border-bottom:0}",
118
125
  ".usg_day:hover{background:var(--dsw-alias-interactive-bg-hover)}",
119
- ".usg_dayDate{color:var(--dsw-alias-label-secondary);flex:none;width:104px;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
120
- ".usg_dayTokens{color:var(--dsw-alias-label-primary);flex:none;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
126
+ ".usg_dayDate{color:var(--dsw-alias-label-secondary);flex:0 1 104px;min-width:0;text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums;overflow:hidden}",
127
+ ".usg_dayTokens{color:var(--dsw-alias-label-primary);flex:none;min-width:84px;text-align:right;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
121
128
  ".usg_dayHit{color:var(--dsw-alias-label-tertiary);flex:none;width:52px;font-size:11px;line-height:18px;font-variant-numeric:tabular-nums;text-align:right}",
122
129
  ".usg_dayBar{background:var(--usg-blue);border-radius:2px;height:6px;flex:1;min-width:4px;opacity:.65}",
123
130
  ".usg_detailHeader{align-items:center;gap:8px;display:flex}",
@@ -171,6 +178,8 @@ window.__ModuleLoader__.load({
171
178
  providerPickerLabel: "usg_providerPickerLabel",
172
179
  providerSelect: "usg_providerSelect",
173
180
  accountGrid: "usg_accountGrid",
181
+ exportLinks: "usg_exportLinks",
182
+ exportLink: "usg_exportLink",
174
183
  accountCard: "usg_accountCard",
175
184
  accountHead: "usg_accountHead",
176
185
  accountMark: "usg_accountMark",
@@ -178,6 +187,7 @@ window.__ModuleLoader__.load({
178
187
  accountName: "usg_accountName",
179
188
  accountPlan: "usg_accountPlan",
180
189
  accountStatus: "usg_accountStatus",
190
+ accountHealth: "usg_accountHealth",
181
191
  quotaList: "usg_quotaList",
182
192
  quotaRow: "usg_quotaRow",
183
193
  quotaMeta: "usg_quotaMeta",
@@ -301,6 +311,21 @@ window.__ModuleLoader__.load({
301
311
  }
302
312
  }
303
313
 
314
+ function budgetWindowText(period, window, translate) {
315
+ if (window === null || typeof window !== "object" || window.limit === null || window.limit === void 0) return null;
316
+ const spent = window.costComplete === true ? fmtCurrency(window.estimatedSpend, window.currency) : "—";
317
+ const limit = fmtCurrency(window.limit, window.currency);
318
+ const level = ["normal", "warning", "critical"].includes(window.level)
319
+ ? translate(`budget.level.${window.level}`)
320
+ : translate("budget.level.unknown");
321
+ return translate("budget.summary", {
322
+ period: translate(`budget.period.${period}`),
323
+ spent,
324
+ limit,
325
+ level
326
+ });
327
+ }
328
+
304
329
  /**
305
330
  * Collapsed-badge account value, derived from the unified v0.2.0 account
306
331
  * snapshot. Balance providers show a real-currency amount; subscription /
@@ -367,13 +392,25 @@ window.__ModuleLoader__.load({
367
392
  return !inside(layer) && !inside(panel);
368
393
  }
369
394
 
395
+ const SAFE_DIAGNOSTIC_REASONS = new Set([
396
+ "dns-resolution-failed",
397
+ "timeout",
398
+ "rate-limited",
399
+ "unauthorized",
400
+ "upstream-invalid-json",
401
+ "upstream-not-json",
402
+ "upstream-too-large",
403
+ "upstream-invalid-response",
404
+ "blocked-network",
405
+ "all-addresses-unreachable",
406
+ "no-validated-address",
407
+ "sub2api-balance-shape-unrecognized",
408
+ "unknown"
409
+ ]);
410
+
370
411
  /** Defense-in-depth filter for server-provided, secret-free diagnostic reasons. */
371
412
  function safeDiagnosticReason(reason) {
372
- if (typeof reason !== "string") return null;
373
- const value = reason.trim();
374
- if (value === "" || value.length > 160) return null;
375
- if (/(authorization|bearer\s|api[_-]?key|cookie|token\s*[=:]|sk-[a-z0-9])/i.test(value)) return null;
376
- return value;
413
+ return typeof reason === "string" && SAFE_DIAGNOSTIC_REASONS.has(reason) ? reason : null;
377
414
  }
378
415
 
379
416
  function diagnosticReasonText(reason, translate) {
@@ -383,8 +420,15 @@ window.__ModuleLoader__.load({
383
420
  if (value === "all-addresses-unreachable") return translate("account.reason.allAddressesUnreachable");
384
421
  if (value === "upstream-not-json") return translate("account.reason.upstreamNotJson");
385
422
  if (value === "upstream-invalid-json") return translate("account.reason.upstreamInvalidJson");
423
+ if (value === "upstream-too-large") return translate("account.reason.upstreamTooLarge");
424
+ if (value === "upstream-invalid-response") return translate("account.reason.upstreamInvalidResponse");
425
+ if (value === "timeout") return translate("account.reason.timeout");
426
+ if (value === "rate-limited") return translate("account.reason.rateLimited");
427
+ if (value === "unauthorized") return translate("account.reason.unauthorized");
428
+ if (value === "blocked-network") return translate("account.reason.blockedNetwork");
429
+ if (value === "no-validated-address") return translate("account.reason.noValidatedAddress");
386
430
  if (value === "sub2api-balance-shape-unrecognized") return translate("account.reason.sub2apiBalanceShapeUnrecognized");
387
- return value;
431
+ return translate("account.reason.unknown");
388
432
  }
389
433
 
390
434
  /**
@@ -398,20 +442,80 @@ window.__ModuleLoader__.load({
398
442
  })) : [];
399
443
  }
400
444
 
445
+ const ORCAROUTER_ADD_SENTINEL = "__add_orcarouter__";
446
+ const ORCAROUTER_INTEGRATION_PATH = "/api/usage-stats/integrations/orcarouter";
447
+
448
+ /** Append the explicit setup action without changing the real provider list. */
449
+ function buildProviderPickerChoices(providers, integration) {
450
+ if (!Array.isArray(providers)) return [];
451
+ if (providers.some((provider) => provider?.id === "orcarouter")) return providers;
452
+ if (integration?.available !== true || integration?.installed !== false) return providers;
453
+ return [...providers, {
454
+ id: ORCAROUTER_ADD_SENTINEL,
455
+ displayName: "OrcaRouter",
456
+ accountMode: "balance",
457
+ synthetic: true
458
+ }];
459
+ }
460
+
461
+ /** Localized selector label; sponsorship is a small disclosure, not a card. */
462
+ function providerChoiceLabel(provider, translate) {
463
+ const label = typeof provider?.displayName === "string" && provider.displayName.trim() !== ""
464
+ ? provider.displayName
465
+ : String(provider?.id ?? "");
466
+ if (provider?.id !== "orcarouter" && provider?.id !== ORCAROUTER_ADD_SENTINEL) return label;
467
+ const suffix = typeof translate === "function" ? translate("orcarouter.choiceSuffix") : "";
468
+ return typeof suffix === "string" && suffix !== "" ? `${label}${suffix}` : label;
469
+ }
470
+
401
471
  /** Locale-safe template interpolation: `t("key", {a})` replaces `{a}`. */
402
472
  function interpolate(template, params) {
403
473
  if (params === void 0) return template;
404
474
  return template.replace(/\{(\w+)\}/g, (match, key) => (Object.hasOwn(params, key) ? String(params[key]) : match));
405
475
  }
406
476
 
407
- async function fetchJson(path) {
408
- const response = await fetch(path, { headers: { accept: "application/json" } });
477
+ async function fetchJson(path, options = {}) {
478
+ const response = await fetch(path, {
479
+ ...options,
480
+ headers: { accept: "application/json", ...options.headers }
481
+ });
409
482
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
410
483
  const payload = await response.json();
411
484
  if (payload === null || typeof payload !== "object") throw new Error("unexpected response");
412
485
  return payload;
413
486
  }
414
487
 
488
+ async function loadOrcaRouterIntegration(request = fetchJson) {
489
+ const payload = await request(ORCAROUTER_INTEGRATION_PATH);
490
+ if (payload?.ok !== true || payload.integration === null || typeof payload.integration !== "object") throw new Error("integration status unavailable");
491
+ return payload.integration;
492
+ }
493
+
494
+ async function addOrcaRouterIntegration(request = fetchJson) {
495
+ const payload = await request(ORCAROUTER_INTEGRATION_PATH, {
496
+ method: "POST",
497
+ headers: {
498
+ "content-type": "application/json",
499
+ "x-dsh-usage-stats-action": "add-orcarouter"
500
+ },
501
+ body: "{}"
502
+ });
503
+ if (payload?.ok !== true || payload.integration?.installed !== true) throw new Error("provider preset was not added");
504
+ return payload.integration;
505
+ }
506
+
507
+ function enableFooterActionWrapping(host) {
508
+ const hostStyle = window.getComputedStyle(host);
509
+ if (!hostStyle.display.includes("flex")
510
+ || hostStyle.flexDirection.startsWith("column")
511
+ || hostStyle.flexWrap !== "nowrap") return void 0;
512
+ const previous = host.style.flexWrap;
513
+ host.style.flexWrap = "wrap";
514
+ return () => {
515
+ host.style.flexWrap = previous;
516
+ };
517
+ }
518
+
415
519
  /**
416
520
  * Build one month's calendar heatmap: weeks as rows (Mon-first), only
417
521
  * the month's own days, padded with null placeholders. Cell tokens come
@@ -473,6 +577,68 @@ window.__ModuleLoader__.load({
473
577
  //#endregion
474
578
 
475
579
  //#region UsageStatsPanel
580
+ const SELECTED_PROVIDER_STORAGE_KEY = "@ychris12138/dsh-usage-stats:selected-provider:v1";
581
+
582
+ function browserStorage() {
583
+ try {
584
+ return typeof window === "object" ? window.localStorage ?? null : null;
585
+ } catch {
586
+ return null;
587
+ }
588
+ }
589
+
590
+ function validStoredProvider(value) {
591
+ return typeof value === "string"
592
+ && value !== ORCAROUTER_ADD_SENTINEL
593
+ && value.length > 0
594
+ && value.length <= 256
595
+ && !value.includes("\0") ? value : null;
596
+ }
597
+
598
+ function readSelectedProvider(storage = browserStorage()) {
599
+ try {
600
+ const stored = storage?.getItem?.(SELECTED_PROVIDER_STORAGE_KEY) ?? null;
601
+ const normalized = validStoredProvider(stored);
602
+ if (stored !== null && normalized === null) storage?.removeItem?.(SELECTED_PROVIDER_STORAGE_KEY);
603
+ return normalized;
604
+ } catch {
605
+ return null;
606
+ }
607
+ }
608
+
609
+ function writeSelectedProvider(providerId, storage = browserStorage()) {
610
+ try {
611
+ // The synthetic dropdown entry is an action, never provider state. A
612
+ // defensive no-op also preserves the previous real selection if this
613
+ // helper is called directly by a future UI refactor.
614
+ if (providerId === ORCAROUTER_ADD_SENTINEL) return;
615
+ const normalized = validStoredProvider(providerId);
616
+ if (normalized === null) storage?.removeItem?.(SELECTED_PROVIDER_STORAGE_KEY);
617
+ else storage?.setItem?.(SELECTED_PROVIDER_STORAGE_KEY, normalized);
618
+ } catch {
619
+ // Storage may be disabled or quota-limited. Selection remains usable
620
+ // for the current mount and silently falls back on the next one.
621
+ }
622
+ }
623
+
624
+ function reconcileSelectedProvider(current, providers, storage = browserStorage()) {
625
+ // An empty response can be a transient provider-service refresh. Keep the
626
+ // current choice until a non-empty authoritative list can prove removal.
627
+ if (!Array.isArray(providers) || providers.length === 0) return current;
628
+ if (Array.isArray(providers) && providers.some((provider) => provider?.id === current)) return current;
629
+ if (current !== null) writeSelectedProvider(null, storage);
630
+ if (!Array.isArray(providers) || providers.length === 0) return null;
631
+ return providers.find((provider) => provider.id === "deepseek-official" && provider.configured)?.id
632
+ ?? providers.find((provider) => provider.id === "deepseek")?.id
633
+ ?? providers.find((provider) => provider.configured)?.id
634
+ ?? providers[0].id;
635
+ }
636
+
637
+ function authoritativeProviderList(payload) {
638
+ if (payload?.ok !== true) return null;
639
+ return Array.isArray(payload.providers) ? payload.providers : [];
640
+ }
641
+
476
642
  /**
477
643
  * Sidebar footer action: badge + floating panel with balance and usage.
478
644
  * @param props - `wide` from the sidebar shell, `t` bound by the slot runtime.
@@ -485,13 +651,18 @@ window.__ModuleLoader__.load({
485
651
  const [selectedDay, setSelectedDay] = react.useState(null);
486
652
  const [viewMonth, setViewMonth] = react.useState(() => currentMonthKey());
487
653
  const [providers, setProviders] = react.useState([]);
488
- const [providersLoaded, setProvidersLoaded] = react.useState(false);
489
- const [selectedProvider, setSelectedProvider] = react.useState(null);
654
+ const [providersAuthoritative, setProvidersAuthoritative] = react.useState(false);
655
+ const storageRef = react.useRef(browserStorage());
656
+ const [selectedProvider, setSelectedProvider] = react.useState(() => readSelectedProvider(storageRef.current));
490
657
  const [account, setAccount] = react.useState(null);
491
658
  const [accountLoading, setAccountLoading] = react.useState(false);
492
659
  const [accountError, setAccountError] = react.useState(null);
660
+ const [orcaRouterIntegration, setOrcaRouterIntegration] = react.useState(null);
661
+ const [orcaRouterPending, setOrcaRouterPending] = react.useState(false);
662
+ const [orcaRouterError, setOrcaRouterError] = react.useState(false);
493
663
  const [refreshedAt, setRefreshedAt] = react.useState(null);
494
664
  const mountedRef = react.useRef(true);
665
+ const orcaRouterMutationRef = react.useRef(false);
495
666
  const usageLoaderRef = react.useRef(null);
496
667
  const accountLoaderRef = react.useRef(null);
497
668
  const layerRef = react.useRef(null);
@@ -499,8 +670,24 @@ window.__ModuleLoader__.load({
499
670
  if (usageLoaderRef.current === null) usageLoaderRef.current = createLoader();
500
671
  if (accountLoaderRef.current === null) accountLoaderRef.current = createLoader();
501
672
  const providerChoices = react.useMemo(() => buildProviderChoices(providers), [providers]);
673
+ const providerPickerChoices = react.useMemo(
674
+ () => buildProviderPickerChoices(providerChoices, orcaRouterIntegration),
675
+ [providerChoices, orcaRouterIntegration]
676
+ );
502
677
  const selectedProviderInfo = providerChoices.find((provider) => provider.id === selectedProvider) ?? null;
503
-
678
+ const selectProvider = react.useCallback((providerId) => {
679
+ if (providerId === ORCAROUTER_ADD_SENTINEL) return;
680
+ const normalized = validStoredProvider(providerId);
681
+ setSelectedProvider(normalized);
682
+ writeSelectedProvider(normalized, storageRef.current);
683
+ }, []);
684
+ const closePanel = react.useCallback(() => {
685
+ // Re-assert the last visible choice at the explicit close boundary. This
686
+ // keeps the X action stable even when storage was temporarily unavailable
687
+ // during an earlier provider-selection event.
688
+ if (selectedProvider !== null) writeSelectedProvider(selectedProvider, storageRef.current);
689
+ setOpen(false);
690
+ }, [selectedProvider]);
504
691
  const loadUsage = react.useCallback(() => {
505
692
  const seq = usageLoaderRef.current.start();
506
693
  setUsageError(null);
@@ -518,20 +705,60 @@ window.__ModuleLoader__.load({
518
705
  });
519
706
  }, []);
520
707
 
521
- const loadProviders = react.useCallback(() => {
522
- fetchJson("/api/usage-stats/providers").then((payload) => {
523
- if (!mountedRef.current) return;
524
- if (payload.ok !== true) {
525
- setProvidersLoaded(true);
526
- return;
527
- }
528
- const list = Array.isArray(payload.providers) ? payload.providers : [];
708
+ const loadProviders = react.useCallback(async () => {
709
+ try {
710
+ const payload = await fetchJson("/api/usage-stats/providers");
711
+ if (!mountedRef.current) return null;
712
+ const list = authoritativeProviderList(payload);
713
+ if (list === null) return null;
529
714
  setProviders(list);
530
- setProvidersLoaded(true);
531
- }).catch(() => { setProvidersLoaded(true); });
715
+ setProvidersAuthoritative(true);
716
+ return list;
717
+ } catch {
718
+ return null;
719
+ }
720
+ }, []);
721
+
722
+ const loadOrcaRouterStatus = react.useCallback(async () => {
723
+ if (mountedRef.current) setOrcaRouterError(false);
724
+ try {
725
+ const integration = await loadOrcaRouterIntegration();
726
+ if (mountedRef.current) setOrcaRouterIntegration(integration);
727
+ return integration;
728
+ } catch {
729
+ if (mountedRef.current) setOrcaRouterIntegration(null);
730
+ return null;
731
+ }
532
732
  }, []);
533
733
 
534
- const loadAccount = react.useCallback((providerId, force = false) => {
734
+ const selectProviderChoice = react.useCallback(async (providerId) => {
735
+ if (providerId !== ORCAROUTER_ADD_SENTINEL) {
736
+ setOrcaRouterError(false);
737
+ selectProvider(providerId);
738
+ return;
739
+ }
740
+ if (orcaRouterMutationRef.current) return;
741
+ orcaRouterMutationRef.current = true;
742
+ setOrcaRouterPending(true);
743
+ setOrcaRouterError(false);
744
+ try {
745
+ const integration = await addOrcaRouterIntegration();
746
+ const refreshedProviders = await loadProviders();
747
+ if (!mountedRef.current) return;
748
+ if (!Array.isArray(refreshedProviders) || !refreshedProviders.some((provider) => provider?.id === "orcarouter")) {
749
+ throw new Error("installed provider was not returned");
750
+ }
751
+ setOrcaRouterIntegration(integration);
752
+ selectProvider("orcarouter");
753
+ } catch {
754
+ if (mountedRef.current) setOrcaRouterError(true);
755
+ } finally {
756
+ orcaRouterMutationRef.current = false;
757
+ if (mountedRef.current) setOrcaRouterPending(false);
758
+ }
759
+ }, [loadProviders, selectProvider]);
760
+
761
+ const loadAccount = react.useCallback((providerId, force = false, activity = null) => {
535
762
  const seq = accountLoaderRef.current.start();
536
763
  setAccountLoading(true);
537
764
  setAccountError(null);
@@ -541,7 +768,8 @@ window.__ModuleLoader__.load({
541
768
  setAccountError("no providers");
542
769
  return;
543
770
  }
544
- const query = `?provider=${encodeURIComponent(target)}${force ? "&refresh=1" : ""}`;
771
+ const activityQuery = activity === "detail" ? "&activity=detail" : "";
772
+ const query = `?provider=${encodeURIComponent(target)}${force ? "&refresh=1" : ""}${activityQuery}`;
545
773
  fetchJson(`/api/usage-stats/account${query}`).then((payload) => {
546
774
  if (!mountedRef.current || !accountLoaderRef.current.isCurrent(seq)) return;
547
775
  if (payload.ok !== true) {
@@ -565,13 +793,12 @@ window.__ModuleLoader__.load({
565
793
  };
566
794
  }, []);
567
795
 
568
- // The host footer-actions container lays sidebar.footer.action list
569
- // registrations out in a ROW, so two full-width entries squeeze onto
570
- // one line and the later one (us) is pushed out of view (#21). Switch
571
- // the container to column stacking while mounted and restore its
572
- // previous inline value on unmount. Walking up (instead of targeting
573
- // the host's hashed class) keeps this working across dsh versions,
574
- // and a container that is already columnar is left untouched.
796
+ // The shared footer-actions host lays full-width registrations out in a
797
+ // row. Let those actions wrap onto separate lines for #21 without
798
+ // changing the row main axis used by sibling plugins (#82). Walking up
799
+ // avoids depending on the host's hashed CSS-module class. Existing
800
+ // column/wrap layouts are left untouched, and our inline wrap override
801
+ // is restored on unmount.
575
802
  react.useEffect(() => {
576
803
  let host = layerRef.current?.parentElement ?? null;
577
804
  for (let depth = 0; host !== null && depth < 3; depth += 1) {
@@ -579,13 +806,7 @@ window.__ModuleLoader__.load({
579
806
  host = host.parentElement;
580
807
  }
581
808
  if (host === null) return void 0;
582
- const hostStyle = window.getComputedStyle(host);
583
- if (!hostStyle.display.includes("flex") || hostStyle.flexDirection === "column") return void 0;
584
- const previous = host.style.flexDirection;
585
- host.style.flexDirection = "column";
586
- return () => {
587
- host.style.flexDirection = previous;
588
- };
809
+ return enableFooterActionWrapping(host);
589
810
  }, []);
590
811
 
591
812
 
@@ -611,41 +832,36 @@ window.__ModuleLoader__.load({
611
832
  // Keep exactly one valid provider selected across independent provider
612
833
  // and subscription responses. DeepSeek remains the initial preference.
613
834
  react.useEffect(() => {
614
- if (!providersLoaded || providerChoices.length === 0) return;
615
- setSelectedProvider((current) => {
616
- if (current !== null && providerChoices.some((provider) => provider.id === current)) return current;
617
- return providerChoices.find((provider) => provider.id === "deepseek-official" && provider.configured)?.id
618
- ?? providerChoices.find((provider) => provider.id === "deepseek")?.id
619
- ?? providerChoices.find((provider) => provider.configured)?.id
620
- ?? providerChoices[0].id;
621
- });
622
- }, [providerChoices, providersLoaded]);
835
+ if (!providersAuthoritative) return;
836
+ setSelectedProvider((current) => reconcileSelectedProvider(current, providerChoices, storageRef.current));
837
+ }, [providerChoices, providersAuthoritative]);
623
838
 
624
839
  react.useEffect(() => {
625
840
  if (!open) return;
626
841
  loadUsage();
627
842
  loadProviders();
843
+ loadOrcaRouterStatus();
628
844
  const usageTimer = window.setInterval(loadUsage, 60000);
629
845
  const providerTimer = window.setInterval(loadProviders, 300000);
630
846
  return () => {
631
847
  window.clearInterval(usageTimer);
632
848
  window.clearInterval(providerTimer);
633
849
  };
634
- }, [open, loadUsage, loadProviders]);
850
+ }, [open, loadUsage, loadProviders, loadOrcaRouterStatus]);
635
851
 
636
852
  // Fetch the selected account for BOTH the panel and the collapsed
637
- // sidebar badge. The server refreshes all providers in the background
638
- // (five-minute cache); the client re-reads that cache every five
639
- // minutes (no refresh=1 the upstream fetch stays server-owned) so
640
- // the badge/panel never stays on a stale balance or quota value.
853
+ // sidebar badge. The server owns the adaptive upstream schedule; this
854
+ // pre-existing shared panel path only re-reads that cache every five
855
+ // minutes (no refresh=1), and marks an open detail view as active.
641
856
  react.useEffect(() => {
642
857
  if (selectedProvider === null) return;
643
- loadAccount(selectedProvider);
644
- const cacheTimer = window.setInterval(() => loadAccount(selectedProvider), 300000);
858
+ const activity = open ? "detail" : null;
859
+ loadAccount(selectedProvider, false, activity);
860
+ const cacheTimer = window.setInterval(() => loadAccount(selectedProvider, false, activity), 300000);
645
861
  return () => {
646
862
  window.clearInterval(cacheTimer);
647
863
  };
648
- }, [selectedProvider, loadAccount]);
864
+ }, [selectedProvider, loadAccount, open]);
649
865
 
650
866
  const dayMap = react.useMemo(() => {
651
867
  const map = new Map();
@@ -702,6 +918,10 @@ window.__ModuleLoader__.load({
702
918
 
703
919
  const selectedEntry = selectedDay !== null ? dayMap.get(selectedDay) ?? null : null;
704
920
  const badgeCount = stats !== null ? fmtCompact(stats.dayTokens) : null;
921
+ const budgetRows = usage?.budgets === null || usage?.budgets === void 0 ? [] : [
922
+ ["daily", usage.budgets.daily],
923
+ ["monthly", usage.budgets.monthly]
924
+ ].map(([period, window]) => ({ period, window, text: budgetWindowText(period, window, translate) })).filter((entry) => entry.text !== null);
705
925
 
706
926
  // Collapsed-badge account value, derived from the unified v0.2.0
707
927
  // account snapshot. Balance providers show a real-currency amount;
@@ -723,7 +943,7 @@ window.__ModuleLoader__.load({
723
943
  const retry = () => {
724
944
  loadUsage();
725
945
  loadProviders();
726
- if (selectedProvider !== null) loadAccount(selectedProvider, true);
946
+ if (selectedProvider !== null) loadAccount(selectedProvider, true, open ? "detail" : null);
727
947
  };
728
948
 
729
949
  const updatedLabel = refreshedAt === null ? "" : translate("panel.updatedAt", {
@@ -777,7 +997,7 @@ window.__ModuleLoader__.load({
777
997
  type: "button",
778
998
  className: S.iconButton,
779
999
  "aria-label": translate("action.close"),
780
- onClick: () => setOpen(false),
1000
+ onClick: closePanel,
781
1001
  children: react_jsx_runtime.jsx(primitives.IconCloseOutline16, { size: 14 })
782
1002
  })
783
1003
  })
@@ -799,20 +1019,27 @@ window.__ModuleLoader__.load({
799
1019
  children: react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("account.title") })
800
1020
  }),
801
1021
  react_jsx_runtime.jsx(ProviderPicker, {
802
- providers: providerChoices,
1022
+ providers: providerPickerChoices,
803
1023
  selectedProvider,
804
- onSelect: (id) => setSelectedProvider(id),
1024
+ onSelect: selectProviderChoice,
1025
+ disabled: orcaRouterPending,
805
1026
  translate
806
1027
  }),
1028
+ orcaRouterError ? react_jsx_runtime.jsx("p", {
1029
+ className: S.note,
1030
+ role: "status",
1031
+ "data-orcarouter-action-error": true,
1032
+ children: translate("orcarouter.addError")
1033
+ }) : null,
807
1034
  selectedProviderInfo === null ? react_jsx_runtime.jsx("p", { className: S.note, children: translate("account.loading") }) : react_jsx_runtime.jsx("div", {
808
1035
  className: S.accountGrid,
809
- children: react_jsx_runtime.jsx(ProviderAccountCard, {
810
- provider: selectedProviderInfo,
811
- account: account?.id === selectedProvider ? account : null,
812
- accountLoading,
813
- accountError,
814
- translate,
815
- onRetry: () => loadAccount(selectedProvider, true)
1036
+ children: react_jsx_runtime.jsx(ProviderAccountCard, {
1037
+ provider: selectedProviderInfo,
1038
+ account: account?.id === selectedProvider ? account : null,
1039
+ accountLoading,
1040
+ accountError,
1041
+ translate,
1042
+ onRetry: () => loadAccount(selectedProvider, true, "detail")
816
1043
  }, selectedProviderInfo.id)
817
1044
  }),
818
1045
  react_jsx_runtime.jsx("section", {
@@ -837,9 +1064,9 @@ window.__ModuleLoader__.load({
837
1064
  react_jsx_runtime.jsxs("div", {
838
1065
  className: S.statsRow,
839
1066
  children: [
840
- react_jsx_runtime.jsx("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.dayTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.today") })] }),
841
- react_jsx_runtime.jsx("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.monthTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.month") })] }),
842
- react_jsx_runtime.jsx("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.total) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.total") })] })
1067
+ react_jsx_runtime.jsxs("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.dayTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.today") })] }),
1068
+ react_jsx_runtime.jsxs("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.monthTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.month") })] }),
1069
+ react_jsx_runtime.jsxs("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.total) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.total") })] })
843
1070
  ]
844
1071
  }),
845
1072
  react_jsx_runtime.jsx("p", {
@@ -849,10 +1076,16 @@ window.__ModuleLoader__.load({
849
1076
  translate("usage.hit.today"),
850
1077
  ": ",
851
1078
  react_jsx_runtime.jsx("b", { children: fmtHit(stats.todayHit) })
852
- ]
853
- })
1079
+ ]
854
1080
  })
855
- ]
1081
+ }),
1082
+ ...budgetRows.map((entry) => react_jsx_runtime.jsx("p", {
1083
+ className: S.hitCaption,
1084
+ "data-budget-period": entry.period,
1085
+ "data-budget-level": entry.window.level,
1086
+ children: entry.text
1087
+ }, entry.period))
1088
+ ]
856
1089
  }),
857
1090
  usage !== null && usageError === null && react_jsx_runtime.jsxs("section", {
858
1091
  className: S.section,
@@ -898,7 +1131,7 @@ window.__ModuleLoader__.load({
898
1131
  })
899
1132
  ]
900
1133
  }),
901
- recent.length > 0 && react_jsx_runtime.jsxs("section", {
1134
+ recent.length > 0 && react_jsx_runtime.jsxs("section", {
902
1135
  className: S.section,
903
1136
  children: [
904
1137
  react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("usage.recent") }),
@@ -923,8 +1156,22 @@ window.__ModuleLoader__.load({
923
1156
  })
924
1157
  })
925
1158
  ]
926
- }),
927
- updatedLabel !== "" && react_jsx_runtime.jsx("p", { className: S.footerNote, children: updatedLabel })
1159
+ }),
1160
+ react_jsx_runtime.jsxs("section", {
1161
+ className: S.section,
1162
+ children: [
1163
+ react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("export.title") }),
1164
+ react_jsx_runtime.jsxs("div", {
1165
+ className: S.exportLinks,
1166
+ children: [
1167
+ react_jsx_runtime.jsx("a", { className: S.exportLink, href: "/api/usage-stats/export/daily.csv", download: "dsh-usage-daily.csv", "data-export-format": "daily-csv", children: translate("export.dailyCsv") }),
1168
+ react_jsx_runtime.jsx("a", { className: S.exportLink, href: "/api/usage-stats/export/sessions.csv", download: "dsh-usage-sessions.csv", "data-export-format": "sessions-csv", children: translate("export.sessionsCsv") }),
1169
+ react_jsx_runtime.jsx("a", { className: S.exportLink, href: "/api/usage-stats/export.json", download: "dsh-usage-stats.json", "data-export-format": "json", children: translate("export.json") })
1170
+ ]
1171
+ })
1172
+ ]
1173
+ }),
1174
+ updatedLabel !== "" && react_jsx_runtime.jsx("p", { className: S.footerNote, children: updatedLabel })
928
1175
  ]
929
1176
  })
930
1177
  ]
@@ -963,6 +1210,7 @@ window.__ModuleLoader__.load({
963
1210
  "opencode-go": "GO",
964
1211
  ollama: "OL",
965
1212
  openrouter: "OR",
1213
+ orcarouter: "OC",
966
1214
  moonshotai: "K",
967
1215
  "moonshotai-cn": "K",
968
1216
  kimi: "K",
@@ -1015,7 +1263,7 @@ window.__ModuleLoader__.load({
1015
1263
  }
1016
1264
 
1017
1265
  /** Provider selector shared by monetary and subscription account modes. */
1018
- function ProviderPicker({ providers, selectedProvider, onSelect, translate }) {
1266
+ function ProviderPicker({ providers, selectedProvider, onSelect, disabled = false, translate }) {
1019
1267
  if (providers.length === 0) return null;
1020
1268
  return react_jsx_runtime.jsxs("label", {
1021
1269
  className: S.providerPicker,
@@ -1024,11 +1272,12 @@ window.__ModuleLoader__.load({
1024
1272
  react_jsx_runtime.jsx("select", {
1025
1273
  className: S.providerSelect,
1026
1274
  value: selectedProvider ?? "",
1275
+ disabled,
1027
1276
  "aria-label": translate("account.provider"),
1028
1277
  onChange: (event) => onSelect(event.target.value),
1029
1278
  children: providers.map((provider) => react_jsx_runtime.jsx("option", {
1030
1279
  value: provider.id,
1031
- children: provider.displayName
1280
+ children: providerChoiceLabel(provider, translate)
1032
1281
  }, provider.id))
1033
1282
  })
1034
1283
  ]
@@ -1043,6 +1292,7 @@ window.__ModuleLoader__.load({
1043
1292
  if (status === "invalid-response") return translate("account.status.invalidResponse");
1044
1293
  if (status === "blocked") return translate("account.status.blocked");
1045
1294
  if (status === "unsupported") return translate("account.status.unsupported");
1295
+ if (status === "unknown") return translate("account.status.unknown");
1046
1296
  return translate("subscription.status.unavailable");
1047
1297
  }
1048
1298
 
@@ -1056,12 +1306,69 @@ window.__ModuleLoader__.load({
1056
1306
  return kind;
1057
1307
  }
1058
1308
 
1059
- function resetLabel(resetsAt, translate) {
1309
+ function durationLabel(totalMinutes, translate) {
1310
+ if (totalMinutes >= 1440) {
1311
+ const days = Math.floor(totalMinutes / 1440);
1312
+ const hours = Math.floor(totalMinutes % 1440 / 60);
1313
+ return translate("duration.daysHours", { days, hours });
1314
+ }
1315
+ if (totalMinutes >= 60) {
1316
+ const hours = Math.floor(totalMinutes / 60);
1317
+ const minutes = totalMinutes % 60;
1318
+ return translate("duration.hoursMinutes", { hours, minutes });
1319
+ }
1320
+ return translate("duration.minutes", { minutes: totalMinutes });
1321
+ }
1322
+
1323
+ function formatResetCountdown(resetsAt, now, translate) {
1060
1324
  if (typeof resetsAt !== "string") return "";
1061
1325
  const date = new Date(resetsAt);
1062
1326
  if (Number.isNaN(date.getTime())) return "";
1063
- return translate("subscription.resets", {
1064
- time: date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })
1327
+ const diffMs = date.getTime() - now;
1328
+ if (diffMs <= 0) return translate("subscription.resetDue");
1329
+ const totalMinutes = Math.max(1, Math.ceil(diffMs / 60000));
1330
+ return translate("subscription.resets", { time: durationLabel(totalMinutes, translate) });
1331
+ }
1332
+
1333
+ function resetLabel(resetsAt, translate, now = Date.now()) {
1334
+ return formatResetCountdown(resetsAt, now, translate);
1335
+ }
1336
+
1337
+ function provenanceLabel(provenance, translate) {
1338
+ if (provenance === "official") return translate("account.provenance.official");
1339
+ if (provenance === "provider") return translate("account.provenance.provider");
1340
+ if (provenance === "configured") return translate("account.provenance.configured");
1341
+ if (provenance === "experimental") return translate("account.provenance.experimental");
1342
+ return translate("account.provenance.unknown");
1343
+ }
1344
+
1345
+ function healthTimeLabel(value) {
1346
+ if (typeof value !== "number" || !Number.isFinite(value)) return "";
1347
+ const date = new Date(value);
1348
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
1349
+ }
1350
+
1351
+ function healthAgeLabel(value, translate) {
1352
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return "";
1353
+ return durationLabel(Math.floor(value / 60000), translate);
1354
+ }
1355
+
1356
+ function AccountHealth({ account, translate }) {
1357
+ if (account === null || account === void 0 || typeof account !== "object") return null;
1358
+ const rows = [];
1359
+ const attempted = healthTimeLabel(account.lastAttemptAt);
1360
+ const succeeded = healthTimeLabel(account.lastSuccessAt);
1361
+ const age = healthAgeLabel(account.ageMs, translate);
1362
+ if (attempted !== "") rows.push(["account.health.lastAttempt", attempted]);
1363
+ if (succeeded !== "") rows.push(["account.health.lastSuccess", succeeded]);
1364
+ if (age !== "") rows.push(["account.health.age", age]);
1365
+ rows.push(["account.health.provenance", provenanceLabel(account.provenance, translate)]);
1366
+ return react_jsx_runtime.jsx("dl", {
1367
+ className: S.accountHealth,
1368
+ children: rows.flatMap(([label, value]) => [
1369
+ react_jsx_runtime.jsx("dt", { children: translate(label) }, `${label}-term`),
1370
+ react_jsx_runtime.jsx("dd", { children: value }, `${label}-value`)
1371
+ ])
1065
1372
  });
1066
1373
  }
1067
1374
 
@@ -1115,7 +1422,8 @@ window.__ModuleLoader__.load({
1115
1422
  const mode = account?.mode ?? provider.accountMode ?? "balance";
1116
1423
  const subscriptionMode = mode === "subscription";
1117
1424
  const status = accountLoading && account === null ? "loading" : account?.status ?? "unavailable";
1118
- const statusText = status === "loading" ? translate("account.status.loading")
1425
+ const statusText = account?.stale === true ? translate("account.status.stale")
1426
+ : status === "loading" ? translate("account.status.loading")
1119
1427
  : status === "blocked" ? translate("account.status.blocked")
1120
1428
  : status === "unsupported" ? translate("account.status.unsupported")
1121
1429
  : subscriptionStatusLabel(status, translate);
@@ -1133,6 +1441,7 @@ window.__ModuleLoader__.load({
1133
1441
  "data-provider": provider.id,
1134
1442
  "data-adapter": provider.adapter ?? provider.accountMode ?? "",
1135
1443
  "data-account-mode": mode,
1444
+ "data-account-stale": account?.stale === true,
1136
1445
  children: [
1137
1446
  react_jsx_runtime.jsxs("div", {
1138
1447
  className: S.accountHead,
@@ -1159,7 +1468,8 @@ window.__ModuleLoader__.load({
1159
1468
  ? react_jsx_runtime.jsx("p", { className: S.quotaEmpty, children: translate("subscription.loading") })
1160
1469
  : react_jsx_runtime.jsx(SubscriptionContent, { provider: account ?? { status: "unavailable", windows: [] }, translate })
1161
1470
  : react_jsx_runtime.jsx(BalanceContent, { balance: account?.balance ?? null, state: balanceState, message: balanceMessage, translate, onRetry }),
1162
- reasonText !== null && status !== "ok" ? react_jsx_runtime.jsx("p", { className: S.note, children: reasonText }) : null
1471
+ react_jsx_runtime.jsx(AccountHealth, { account, translate }),
1472
+ reasonText !== null && status !== "ok" ? react_jsx_runtime.jsx("p", { className: S.note, children: reasonText }) : null
1163
1473
  ]
1164
1474
  });
1165
1475
  }
@@ -1318,20 +1628,41 @@ window.__ModuleLoader__.load({
1318
1628
  const zh = {
1319
1629
  "panel.title": "用量与余额",
1320
1630
  "panel.badge": "用量/余额",
1631
+ "orcarouter.choiceSuffix": "(赞助集成)",
1632
+ "orcarouter.addError": "未能添加 OrcaRouter,请稍后重试。",
1321
1633
  "account.title": "供应商账户",
1322
1634
  "account.provider": "当前供应商",
1323
1635
  "account.balanceMode": "API 余额",
1324
1636
  "account.loading": "正在加载供应商…",
1325
1637
  "account.status.loading": "查询中",
1638
+ "account.status.stale": "数据已过期",
1326
1639
  "account.status.blocked": "已阻止",
1327
1640
  "account.status.unsupported": "不支持余额",
1328
1641
  "account.status.invalidResponse": "响应异常",
1642
+ "account.status.unknown": "未知",
1329
1643
  "account.invalidResponse": "供应商返回了无法识别的额度数据。",
1330
1644
  "account.reason.dnsResolutionFailed": "无法解析供应商域名。",
1331
1645
  "account.reason.allAddressesUnreachable": "已验证的网络地址均无法连接。",
1332
1646
  "account.reason.upstreamNotJson": "上游返回的不是 JSON",
1333
1647
  "account.reason.upstreamInvalidJson": "上游返回了无法解析的 JSON",
1334
1648
  "account.reason.sub2apiBalanceShapeUnrecognized": "Sub2API 余额接口返回了未识别的数据结构。",
1649
+ "account.reason.timeout": "账户请求超时。",
1650
+ "account.reason.rateLimited": "供应商限制了账户查询频率。",
1651
+ "account.reason.unauthorized": "账户凭据未通过验证。",
1652
+ "account.reason.upstreamTooLarge": "上游响应超过安全大小限制。",
1653
+ "account.reason.upstreamInvalidResponse": "上游返回了无法识别的响应。",
1654
+ "account.reason.blockedNetwork": "账户请求被本地网络安全策略阻止。",
1655
+ "account.reason.noValidatedAddress": "没有可安全连接的供应商地址。",
1656
+ "account.reason.unknown": "账户查询失败,未提供可安全展示的诊断信息。",
1657
+ "account.health.lastAttempt": "上次尝试",
1658
+ "account.health.lastSuccess": "上次成功",
1659
+ "account.health.age": "数据年龄",
1660
+ "account.health.provenance": "数据来源",
1661
+ "account.provenance.official": "官方接口",
1662
+ "account.provenance.provider": "供应商接口",
1663
+ "account.provenance.configured": "自定义配置",
1664
+ "account.provenance.experimental": "实验性探测",
1665
+ "account.provenance.unknown": "未知",
1335
1666
  "account.blocked": "账户查询被本地安全策略阻止,请检查 HTTPS、同源和私网访问设置。",
1336
1667
  "balance.title": "账户余额",
1337
1668
  "balance.provider": "供应商",
@@ -1362,7 +1693,11 @@ window.__ModuleLoader__.load({
1362
1693
  "subscription.window.quota": "总额度",
1363
1694
  "subscription.window.mcp": "MCP 月度额度",
1364
1695
  "subscription.used": "已用 {value}%",
1365
- "subscription.resets": "{time} 重置",
1696
+ "subscription.resets": "{time}后重置",
1697
+ "subscription.resetDue": "已到重置时间",
1698
+ "duration.minutes": "{minutes} 分钟",
1699
+ "duration.hoursMinutes": "{hours} 小时 {minutes} 分钟",
1700
+ "duration.daysHours": "{days} 天 {hours} 小时",
1366
1701
  "subscription.notConfigured": "配置 {refs} 后显示真实订阅比例。",
1367
1702
  "subscription.unauthorized": "凭据已失效,请更新后重试。",
1368
1703
  "subscription.rateLimited": "供应商暂时限制查询,请稍后重试。",
@@ -1386,6 +1721,17 @@ window.__ModuleLoader__.load({
1386
1721
  "usage.cacheRead": "缓存读",
1387
1722
  "usage.unknownModel": "未知模型",
1388
1723
  "usage.noModels": "这一天没有分模型数据。",
1724
+ "export.title": "安全导出",
1725
+ "export.dailyCsv": "每日 CSV",
1726
+ "export.sessionsCsv": "会话 CSV",
1727
+ "export.json": "完整 JSON",
1728
+ "budget.period.daily": "今日估算",
1729
+ "budget.period.monthly": "本月估算",
1730
+ "budget.level.normal": "正常",
1731
+ "budget.level.warning": "接近预算",
1732
+ "budget.level.critical": "已超预算",
1733
+ "budget.level.unknown": "费用不完整",
1734
+ "budget.summary": "{period}:{spent} / {limit} · {level}",
1389
1735
  "month.year": "{year}年{month}",
1390
1736
  "action.refresh": "刷新",
1391
1737
  "action.retry": "重试",
@@ -1406,20 +1752,41 @@ window.__ModuleLoader__.load({
1406
1752
  const en = {
1407
1753
  "panel.title": "Usage & Balance",
1408
1754
  "panel.badge": "Usage/Balance",
1755
+ "orcarouter.choiceSuffix": " (Sponsored integration)",
1756
+ "orcarouter.addError": "Could not add OrcaRouter. Please try again.",
1409
1757
  "account.title": "Provider account",
1410
1758
  "account.provider": "Current provider",
1411
1759
  "account.balanceMode": "API balance",
1412
1760
  "account.loading": "Loading providers…",
1413
1761
  "account.status.loading": "Loading",
1762
+ "account.status.stale": "Stale data",
1414
1763
  "account.status.blocked": "Blocked",
1415
1764
  "account.status.unsupported": "Balance unsupported",
1416
1765
  "account.status.invalidResponse": "Invalid response",
1766
+ "account.status.unknown": "Unknown",
1417
1767
  "account.invalidResponse": "The provider returned unrecognized quota data.",
1418
1768
  "account.reason.dnsResolutionFailed": "The provider hostname could not be resolved.",
1419
1769
  "account.reason.allAddressesUnreachable": "None of the validated network addresses could be reached.",
1420
1770
  "account.reason.upstreamNotJson": "The upstream response was not JSON",
1421
1771
  "account.reason.upstreamInvalidJson": "The upstream returned unparsable JSON",
1422
1772
  "account.reason.sub2apiBalanceShapeUnrecognized": "The Sub2API balance endpoint returned an unrecognized data shape.",
1773
+ "account.reason.timeout": "The account request timed out.",
1774
+ "account.reason.rateLimited": "The provider rate limited account checks.",
1775
+ "account.reason.unauthorized": "The account credential was rejected.",
1776
+ "account.reason.upstreamTooLarge": "The upstream response exceeded the safety size limit.",
1777
+ "account.reason.upstreamInvalidResponse": "The upstream returned an unrecognized response.",
1778
+ "account.reason.blockedNetwork": "The account request was blocked by the local network policy.",
1779
+ "account.reason.noValidatedAddress": "No validated provider address was available.",
1780
+ "account.reason.unknown": "The account check failed without a safe diagnostic detail.",
1781
+ "account.health.lastAttempt": "Last attempt",
1782
+ "account.health.lastSuccess": "Last success",
1783
+ "account.health.age": "Data age",
1784
+ "account.health.provenance": "Source",
1785
+ "account.provenance.official": "Official API",
1786
+ "account.provenance.provider": "Provider API",
1787
+ "account.provenance.configured": "Configured",
1788
+ "account.provenance.experimental": "Experimental",
1789
+ "account.provenance.unknown": "Unknown",
1423
1790
  "account.blocked": "The account query was blocked by the local security policy. Check HTTPS, same-origin, and private-network settings.",
1424
1791
  "balance.title": "Account balance",
1425
1792
  "balance.provider": "Provider",
@@ -1450,7 +1817,11 @@ window.__ModuleLoader__.load({
1450
1817
  "subscription.window.quota": "Total quota",
1451
1818
  "subscription.window.mcp": "Monthly MCP quota",
1452
1819
  "subscription.used": "{value}% used",
1453
- "subscription.resets": "Resets {time}",
1820
+ "subscription.resets": "Resets in {time}",
1821
+ "subscription.resetDue": "Reset due",
1822
+ "duration.minutes": "{minutes}m",
1823
+ "duration.hoursMinutes": "{hours}h {minutes}m",
1824
+ "duration.daysHours": "{days}d {hours}h",
1454
1825
  "subscription.notConfigured": "Configure {refs} to show live subscription usage.",
1455
1826
  "subscription.unauthorized": "The credential has expired; update it and retry.",
1456
1827
  "subscription.rateLimited": "The provider is rate limiting checks; retry later.",
@@ -1474,6 +1845,17 @@ window.__ModuleLoader__.load({
1474
1845
  "usage.cacheRead": "Cache read",
1475
1846
  "usage.unknownModel": "Unknown model",
1476
1847
  "usage.noModels": "No per-model data for this day.",
1848
+ "export.title": "Secret-free export",
1849
+ "export.dailyCsv": "Daily CSV",
1850
+ "export.sessionsCsv": "Sessions CSV",
1851
+ "export.json": "Full JSON",
1852
+ "budget.period.daily": "Today estimated",
1853
+ "budget.period.monthly": "This month estimated",
1854
+ "budget.level.normal": "Normal",
1855
+ "budget.level.warning": "Near budget",
1856
+ "budget.level.critical": "Over budget",
1857
+ "budget.level.unknown": "Incomplete cost",
1858
+ "budget.summary": "{period}: {spent} / {limit} · {level}",
1477
1859
  "month.year": "{month} {year}",
1478
1860
  "action.refresh": "Refresh",
1479
1861
  "action.retry": "Retry",
@@ -1498,7 +1880,7 @@ window.__ModuleLoader__.load({
1498
1880
  const inject = ["slots", "locale"];
1499
1881
 
1500
1882
  /**
1501
- * Client plugin body: register the dictionaries and the sidebar footer action.
1883
+ * Client plugin body: register dictionaries and the sidebar account panel.
1502
1884
  * @param ctx - client root context.
1503
1885
  */
1504
1886
  function apply(ctx) {
@@ -1522,13 +1904,26 @@ window.__ModuleLoader__.load({
1522
1904
  exports.cellColor = cellColor;
1523
1905
  exports.createLoader = createLoader;
1524
1906
  exports.buildProviderChoices = buildProviderChoices;
1907
+ exports.buildProviderPickerChoices = buildProviderPickerChoices;
1908
+ exports.loadOrcaRouterIntegration = loadOrcaRouterIntegration;
1909
+ exports.addOrcaRouterIntegration = addOrcaRouterIntegration;
1525
1910
  exports.modelLabelOf = modelLabelOf;
1526
1911
  exports.fmt = fmt;
1527
1912
  exports.fmtCurrency = fmtCurrency;
1913
+ exports.budgetWindowText = budgetWindowText;
1528
1914
  exports.badgeAccountValue = badgeAccountValue;
1529
1915
  exports.badgeWarnOf = badgeWarnOf;
1530
1916
  exports.shouldDismissPanel = shouldDismissPanel;
1531
1917
  exports.safeDiagnosticReason = safeDiagnosticReason;
1918
+ exports.formatResetCountdown = formatResetCountdown;
1919
+ exports.enableFooterActionWrapping = enableFooterActionWrapping;
1920
+ exports.providerChoiceLabel = providerChoiceLabel;
1921
+ exports.ORCAROUTER_ADD_SENTINEL = ORCAROUTER_ADD_SENTINEL;
1922
+ exports.SELECTED_PROVIDER_STORAGE_KEY = SELECTED_PROVIDER_STORAGE_KEY;
1923
+ exports.authoritativeProviderList = authoritativeProviderList;
1924
+ exports.readSelectedProvider = readSelectedProvider;
1925
+ exports.reconcileSelectedProvider = reconcileSelectedProvider;
1926
+ exports.writeSelectedProvider = writeSelectedProvider;
1532
1927
  return module.exports;
1533
1928
  }
1534
1929
  });