diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx
index e99c8f6f3a..bc653f92f5 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx
@@ -192,7 +192,22 @@ function aggregateWorst(statuses: StatusKey[]): "critical" | "alert" | "ok" | "e
return worst;
}
-export default function ProviderLimits() {
+function formatAutoRefreshCountdown(ms: number): string {
+ const totalSeconds = Math.max(0, Math.ceil(ms / 1000));
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
+}
+
+interface ProviderLimitsProps {
+ showFilters?: boolean;
+ autoRefreshInterval?: number;
+}
+
+export default function ProviderLimits({
+ showFilters = true,
+ autoRefreshInterval = 0,
+}: ProviderLimitsProps) {
const t = useTranslations("usage");
const tr = useCallback(
(key: string, fallback: string, values?: UsageTranslationValues) =>
@@ -228,6 +243,9 @@ export default function ProviderLimits() {
const lastFetchTimeRef = useRef
>({});
const staleProbeRef = useRef>({});
+ const lastRefreshAllAtRef = useRef(Date.now());
+ const autoRefreshIntervalMs = autoRefreshInterval > 0 ? autoRefreshInterval * 1000 : 0;
+ const [autoRefreshClock, setAutoRefreshClock] = useState(() => Date.now());
const [cutoffModalConn, setCutoffModalConn] = useState(null);
const [cutoffModalWindows, setCutoffModalWindows] = useState([]);
const [providerWindowDefaults, setProviderWindowDefaults] = useState<
@@ -401,6 +419,9 @@ export default function ProviderLimits() {
const refreshAll = useCallback(async () => {
if (refreshingAllRef.current) return;
refreshingAllRef.current = true;
+ const now = Date.now();
+ lastRefreshAllAtRef.current = now;
+ setAutoRefreshClock(now);
setRefreshingAll(true);
try {
const response = await fetch("/api/usage/provider-limits", { method: "POST" });
@@ -422,6 +443,33 @@ export default function ProviderLimits() {
}
}, [applyCachedQuotaState, fetchConnections]);
+ useEffect(() => {
+ if (autoRefreshIntervalMs <= 0) return;
+
+ const tick = () => setAutoRefreshClock(Date.now());
+ tick();
+
+ const timer = window.setInterval(tick, 1000);
+ const handleVisibilityChange = () => {
+ if (document.visibilityState === "visible") tick();
+ };
+
+ document.addEventListener("visibilitychange", handleVisibilityChange);
+ return () => {
+ window.clearInterval(timer);
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
+ };
+ }, [autoRefreshIntervalMs]);
+
+ useEffect(() => {
+ if (autoRefreshIntervalMs <= 0) return;
+ if (document.visibilityState !== "visible") return;
+ if (refreshingAllRef.current) return;
+ if (autoRefreshClock - lastRefreshAllAtRef.current >= autoRefreshIntervalMs) {
+ void refreshAll();
+ }
+ }, [autoRefreshClock, autoRefreshIntervalMs, refreshAll]);
+
useEffect(() => {
const init = async () => {
setInitialLoading(true);
@@ -702,148 +750,159 @@ export default function ProviderLimits() {
onClick={refreshAll}
disabled={refreshingAll}
className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg bg-bg-subtle border border-border text-text-main text-[13px] disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
+ title={autoRefreshIntervalMs > 0 ? tr("autoRefreshing", "Auto-refreshing") : t("refreshAll")}
>
- refresh
+ {autoRefreshIntervalMs > 0 ? "schedule" : "refresh"}
- {t("refreshAll")}
+ {refreshingAll
+ ? tr("refreshing", "Refreshing")
+ : autoRefreshIntervalMs > 0
+ ? `${tr("autoRefreshing", "Auto-refreshing")} ${formatAutoRefreshCountdown(
+ Math.max(0, autoRefreshIntervalMs - (autoRefreshClock - lastRefreshAllAtRef.current))
+ )}`
+ : t("refreshAll")}
- {/* Summary stats — clickable status filter */}
-
- {(["all", "critical", "alert", "ok"] as StatusKey[]).map((key) => {
- const tone = STATUS_TONE[key];
- const labelMap: Record
= {
- all: tr("statTotal", "Total"),
- critical: tr("statCritical", "Critical"),
- alert: tr("statAlert", "Alert"),
- ok: tr("statHealthy", "Healthy"),
- };
- const active = statusFilter === key;
- const count = statusCounts[key] || 0;
- return (
- handleSetStatusFilter(key)}
- className="text-left rounded-lg px-3 py-2.5 border transition-colors cursor-pointer"
- style={{
- background: active ? tone.bg : "var(--color-surface)",
- borderColor: active ? tone.ring : "var(--color-border)",
- }}
- >
-
-
- {labelMap[key]}
-
- {key !== "all" && (
-
- )}
-
-
- {count}
-
-
- );
- })}
-
+ {showFilters && (
+ <>
+ {/* Summary stats — clickable status filter */}
+
+ {(["all", "critical", "alert", "ok"] as StatusKey[]).map((key) => {
+ const tone = STATUS_TONE[key];
+ const labelMap: Record
= {
+ all: tr("statTotal", "Total"),
+ critical: tr("statCritical", "Critical"),
+ alert: tr("statAlert", "Alert"),
+ ok: tr("statHealthy", "Healthy"),
+ };
+ const active = statusFilter === key;
+ const count = statusCounts[key] || 0;
+ return (
+ handleSetStatusFilter(key)}
+ className="text-left rounded-lg px-3 py-2.5 border transition-colors cursor-pointer"
+ style={{
+ background: active ? tone.bg : "var(--color-surface)",
+ borderColor: active ? tone.ring : "var(--color-border)",
+ }}
+ >
+
+
+ {labelMap[key]}
+
+ {key !== "all" && (
+
+ )}
+
+
+ {count}
+
+
+ );
+ })}
+
- {/* Purchase Type filter */}
-
-
- {tr("filterPurchaseTypeLabel", "Type")}
-
- {PURCHASE_TYPES.map((type) => {
- const count = purchaseTypeCounts[type.key] || 0;
- if (type.key !== "all" && count === 0) return null;
- const active = purchaseTypeFilter === type.key;
- return (
- handleSetPurchaseFilter(type.key)}
- className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold cursor-pointer"
- style={{
- border: active
- ? "1px solid var(--color-primary, #E54D5E)"
- : "1px solid var(--color-border)",
- background: active ? "rgba(229,77,94,0.1)" : "transparent",
- color: active ? "var(--color-primary, #E54D5E)" : "var(--color-text-muted)",
- }}
- >
- {tr(type.labelKey, type.fallback)}
- {count}
-
- );
- })}
-
+ {/* Purchase Type filter */}
+
+
+ {tr("filterPurchaseTypeLabel", "Type")}
+
+ {PURCHASE_TYPES.map((type) => {
+ const count = purchaseTypeCounts[type.key] || 0;
+ if (type.key !== "all" && count === 0) return null;
+ const active = purchaseTypeFilter === type.key;
+ return (
+ handleSetPurchaseFilter(type.key)}
+ className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold cursor-pointer"
+ style={{
+ border: active
+ ? "1px solid var(--color-primary, #E54D5E)"
+ : "1px solid var(--color-border)",
+ background: active ? "rgba(229,77,94,0.1)" : "transparent",
+ color: active ? "var(--color-primary, #E54D5E)" : "var(--color-text-muted)",
+ }}
+ >
+ {tr(type.labelKey, type.fallback)}
+ {count}
+
+ );
+ })}
+
- {/* Tier filter */}
-
-
- {tr("filterTierLabel", "Tier")}
-
- {TIER_FILTERS.map((tier) => {
- if (tier.key !== "all" && !tierCounts[tier.key]) return null;
- const active = tierFilter === tier.key;
- return (
- setTierFilter(tier.key)}
- className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold cursor-pointer"
- style={{
- border: active
- ? "1px solid var(--color-primary, #E54D5E)"
- : "1px solid var(--color-border)",
- background: active ? "rgba(229,77,94,0.1)" : "transparent",
- color: active ? "var(--color-primary, #E54D5E)" : "var(--color-text-muted)",
- }}
- >
- {tier.label || t(tier.labelKey!)}
- {tierCounts[tier.key] || 0}
-
- );
- })}
-
+ {/* Tier filter */}
+
+
+ {tr("filterTierLabel", "Tier")}
+
+ {TIER_FILTERS.map((tier) => {
+ if (tier.key !== "all" && !tierCounts[tier.key]) return null;
+ const active = tierFilter === tier.key;
+ return (
+ setTierFilter(tier.key)}
+ className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold cursor-pointer"
+ style={{
+ border: active
+ ? "1px solid var(--color-primary, #E54D5E)"
+ : "1px solid var(--color-border)",
+ background: active ? "rgba(229,77,94,0.1)" : "transparent",
+ color: active ? "var(--color-primary, #E54D5E)" : "var(--color-text-muted)",
+ }}
+ >
+ {tier.label || t(tier.labelKey!)}
+ {tierCounts[tier.key] || 0}
+
+ );
+ })}
+
- {/* Env filter — only renders when at least one connection has a tag */}
- {envTags.length > 0 && (
-
-
- {tr("filterEnvLabel", "Env")}
-
- {(["all", ...envTags] as string[]).map((tag) => {
- const count = envCounts[tag] || 0;
- const active = envFilter === tag;
- const label = tag === "all" ? tr("filterEnvAll", "All") : tag;
- return (
- handleSetEnvFilter(tag)}
- className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold cursor-pointer"
- style={{
- border: active
- ? "1px solid var(--color-primary, #E54D5E)"
- : "1px solid var(--color-border)",
- background: active ? "rgba(229,77,94,0.1)" : "transparent",
- color: active ? "var(--color-primary, #E54D5E)" : "var(--color-text-muted)",
- }}
- >
- {label}
- {count}
-
- );
- })}
-
+ {/* Env filter — only renders when at least one connection has a tag */}
+ {envTags.length > 0 && (
+
+
+ {tr("filterEnvLabel", "Env")}
+
+ {(["all", ...envTags] as string[]).map((tag) => {
+ const count = envCounts[tag] || 0;
+ const active = envFilter === tag;
+ const label = tag === "all" ? tr("filterEnvAll", "All") : tag;
+ return (
+ handleSetEnvFilter(tag)}
+ className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold cursor-pointer"
+ style={{
+ border: active
+ ? "1px solid var(--color-primary, #E54D5E)"
+ : "1px solid var(--color-border)",
+ background: active ? "rgba(229,77,94,0.1)" : "transparent",
+ color: active ? "var(--color-primary, #E54D5E)" : "var(--color-text-muted)",
+ }}
+ >
+ {label}
+ {count}
+
+ );
+ })}
+
+ )}
+ >
)}
{/* Provider groups */}
diff --git a/src/app/(dashboard)/home/ProviderQuotaWidget.tsx b/src/app/(dashboard)/home/ProviderQuotaWidget.tsx
index 221c54dd5c..eb267fafdb 100644
--- a/src/app/(dashboard)/home/ProviderQuotaWidget.tsx
+++ b/src/app/(dashboard)/home/ProviderQuotaWidget.tsx
@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
import Card from "@/shared/components/Card";
import ProviderIcon from "@/shared/components/ProviderIcon";
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
+import { translateUsageOrFallback } from "../dashboard/usage/components/ProviderLimits/i18nFallback";
type Connection = {
id: string;
@@ -16,9 +17,23 @@ type Connection = {
type QuotaData = Record
;
-export default function ProviderQuotaWidget() {
+interface ProviderQuotaWidgetProps {
+ autoRefreshInterval?: number;
+}
+
+function formatAutoRefreshCountdown(ms: number): string {
+ const totalSeconds = Math.max(0, Math.ceil(ms / 1000));
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
+}
+
+export default function ProviderQuotaWidget({ autoRefreshInterval = 0 }: ProviderQuotaWidgetProps) {
const t = useTranslations("usage");
- const tc = useTranslations("common");
+ const tr = useCallback(
+ (key: string, fallback: string) => translateUsageOrFallback(t, key, fallback),
+ [t]
+ );
const [connections, setConnections] = useState([]);
const [quotaData, setQuotaData] = useState({});
@@ -26,6 +41,9 @@ export default function ProviderQuotaWidget() {
const [refreshingAll, setRefreshingAll] = useState(false);
const refreshingAllRef = useRef(false);
+ const lastRefreshAllAtRef = useRef(Date.now());
+ const autoRefreshIntervalMs = autoRefreshInterval > 0 ? autoRefreshInterval * 1000 : 0;
+ const [autoRefreshClock, setAutoRefreshClock] = useState(() => Date.now());
const fetchConnections = useCallback(async () => {
try {
@@ -69,9 +87,30 @@ export default function ProviderQuotaWidget() {
loadData();
}, [loadData]);
+ useEffect(() => {
+ if (autoRefreshIntervalMs <= 0) return;
+
+ const tick = () => setAutoRefreshClock(Date.now());
+ tick();
+
+ const timer = window.setInterval(tick, 1000);
+ const handleVisibilityChange = () => {
+ if (document.visibilityState === "visible") tick();
+ };
+
+ document.addEventListener("visibilitychange", handleVisibilityChange);
+ return () => {
+ window.clearInterval(timer);
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
+ };
+ }, [autoRefreshIntervalMs]);
+
const refreshAll = useCallback(async () => {
if (refreshingAllRef.current) return;
refreshingAllRef.current = true;
+ const now = Date.now();
+ lastRefreshAllAtRef.current = now;
+ setAutoRefreshClock(now);
setRefreshingAll(true);
try {
@@ -98,6 +137,15 @@ export default function ProviderQuotaWidget() {
}
}, [fetchConnections, fetchCached]);
+ useEffect(() => {
+ if (autoRefreshIntervalMs <= 0) return;
+ if (document.visibilityState !== "visible") return;
+ if (refreshingAllRef.current) return;
+ if (autoRefreshClock - lastRefreshAllAtRef.current >= autoRefreshIntervalMs) {
+ void refreshAll();
+ }
+ }, [autoRefreshClock, autoRefreshIntervalMs, refreshAll]);
+
// Simple summary: group by provider for display
const providerGroups = connections.reduce>((acc, conn) => {
if (!acc[conn.provider]) acc[conn.provider] = [];
@@ -116,9 +164,9 @@ export default function ProviderQuotaWidget() {
account_balance
-
{t("providerQuota") || "Provider Quota"}
+
{tr("providerQuota", "Provider Quota")}
- {t("providerQuotaHomeHint") || "Live status across connected accounts"}
+ {tr("providerQuotaHomeHint", "Live status across connected accounts")}
@@ -127,14 +175,22 @@ export default function ProviderQuotaWidget() {
onClick={refreshAll}
disabled={refreshingAll || loading}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border bg-bg-subtle text-xs font-medium text-text-main disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface transition-colors"
- title={t("refreshAll") || "Refresh All"}
+ title={autoRefreshIntervalMs > 0 ? tr("autoRefreshing", "Auto-refreshing") : tr("refreshAll", "Refresh All")}
>