diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f9f4d08..59b506d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features - **User limits**: add per-user total-token budgets for Orbit Provider and Codex with rolling 5-hour and weekly windows +- **User quota**: show remaining Orbit and Codex headroom in the users table with session and weekly usage details - **Orbit Provider**: add Anthropic-compatible API-key routing for Claude Opus 4.6–4.8 models - **xAI**: Grok Imagine video generation (`/v1/videos`) + CLI - **CLI tools**: Grok Build setup — writes `[model.9router]` to `~/.grok/config.toml` diff --git a/src/app/(dashboard)/dashboard/users/components/QuotaCell.js b/src/app/(dashboard)/dashboard/users/components/QuotaCell.js new file mode 100644 index 00000000..ac16cec9 --- /dev/null +++ b/src/app/(dashboard)/dashboard/users/components/QuotaCell.js @@ -0,0 +1,103 @@ +"use client"; + +import { useEffect, useState } from "react"; +import PropTypes from "prop-types"; +import { + TOKEN_LIMIT_PROVIDER_OPTIONS, + TOKEN_LIMIT_WINDOW_OPTIONS, + formatTokenCount, + getProviderRemainingPercentage, + getQuotaTone, +} from "./tokenLimitDisplay.js"; + +function buildQuotaSummary(providerUsage) { + return TOKEN_LIMIT_WINDOW_OPTIONS + .map(({ id, name }) => { + const usage = providerUsage?.[id]; + if (!usage?.limit) return null; + return `${name}: ${formatTokenCount(usage.remaining)} of ${formatTokenCount(usage.limit)} tokens left`; + }) + .filter(Boolean) + .join(" · "); +} + +export default function QuotaCell({ userId, refreshKey = 0 }) { + const [state, setState] = useState({ status: "loading", data: null }); + + useEffect(() => { + const controller = new AbortController(); + + async function loadUsage() { + setState((current) => ({ ...current, status: "loading" })); + try { + const response = await fetch(`/api/users/${userId}/token-usage`, { + cache: "no-store", + signal: controller.signal, + }); + const payload = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(payload.error || "Failed to load token usage"); + setState({ status: "ready", data: payload }); + } catch (error) { + if (error?.name !== "AbortError") setState({ status: "error", data: null }); + } + } + + void loadUsage(); + return () => controller.abort(); + }, [refreshKey, userId]); + + if (state.status === "loading") { + return ( +
{option.name}
+{option.description}
+{option.name}
+{option.description}
+Usage data is unavailable
+{state.error}
+{provider.description}
+{limitsError}
: null} + {limitEditor ? ( +Current headroom
+The lowest active window determines the quota shown in the users table.
+Budget settings
+Set 0 to leave a provider window unlimited.
+