fix: improve the UI for check quota of the user

This commit is contained in:
2026-07-17 22:24:11 +07:00
parent b700a070c0
commit c77127aaf3
7 changed files with 611 additions and 24 deletions
+1
View File
@@ -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.64.8 models
- **xAI**: Grok Imagine video generation (`/v1/videos`) + CLI
- **CLI tools**: Grok Build setup — writes `[model.9router]` to `~/.grok/config.toml`
@@ -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 (
<div className="flex w-24 flex-col gap-1.5" aria-label="Loading quota">
<span className="h-2.5 w-20 animate-pulse rounded-full bg-surface-2" />
<span className="h-2.5 w-14 animate-pulse rounded-full bg-surface-2" />
</div>
);
}
if (state.status === "error") {
return (
<span className="inline-flex items-center gap-2 text-xs text-text-muted" title="Quota data is unavailable">
<span className="size-1.5 rounded-full bg-text-muted/50" aria-hidden="true" />
Unavailable
</span>
);
}
const configuredProviders = TOKEN_LIMIT_PROVIDER_OPTIONS.flatMap((provider) => {
const usage = state.data?.providers?.[provider.id];
const remainingPercentage = getProviderRemainingPercentage(usage);
return remainingPercentage === null ? [] : [{ ...provider, usage, remainingPercentage }];
});
if (configuredProviders.length === 0) {
return <span className="text-xs text-text-muted">Unlimited</span>;
}
return (
<div className="flex flex-col items-start gap-1.5">
{configuredProviders.map((provider) => {
const tone = getQuotaTone(provider.remainingPercentage);
const title = `${provider.name} · ${buildQuotaSummary(provider.usage)}`;
return (
<span
key={provider.id}
className="inline-flex items-center gap-2 whitespace-nowrap text-xs"
title={title}
>
<span className={`size-1.5 shrink-0 rounded-full ${tone.dot}`} aria-hidden="true" />
<span className="w-10 text-text-muted">{provider.shortName}</span>
<strong className={`min-w-8 text-right font-semibold tabular-nums ${tone.text}`}>
{provider.remainingPercentage}%
</strong>
</span>
);
})}
</div>
);
}
QuotaCell.propTypes = {
userId: PropTypes.string.isRequired,
refreshKey: PropTypes.number,
};
@@ -0,0 +1,178 @@
"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 UsageWindow({ option, usage }) {
const hasLimit = usage?.limit > 0;
if (!hasLimit) {
return (
<div className="grid gap-2 py-3 first:pt-0 last:pb-0 sm:grid-cols-[9rem_minmax(0,1fr)] sm:items-center sm:gap-4">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.12em] text-text-muted">{option.name}</p>
<p className="mt-0.5 text-[11px] text-text-muted">{option.description}</p>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-sm font-medium tabular-nums text-text-main">
{formatTokenCount(usage?.used)} used
</span>
<span className="rounded-full border border-border-subtle bg-surface px-2 py-1 text-[11px] font-medium text-text-muted">
Unlimited
</span>
</div>
</div>
);
}
const tone = getQuotaTone(usage.remainingPercentage);
return (
<div className="grid gap-2 py-3 first:pt-0 last:pb-0 sm:grid-cols-[9rem_minmax(0,1fr)] sm:items-center sm:gap-4">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.12em] text-text-muted">{option.name}</p>
<p className="mt-0.5 text-[11px] text-text-muted">{option.description}</p>
</div>
<div className="min-w-0">
<div className="mb-1.5 flex items-center justify-between gap-3 text-xs">
<span className={`font-semibold tabular-nums ${tone.text}`}>
{usage.remainingPercentage}% remaining
</span>
<span className="truncate text-right tabular-nums text-text-muted">
{formatTokenCount(usage.used)} / {formatTokenCount(usage.limit)} used
</span>
</div>
<div
className="h-2 overflow-hidden rounded-full bg-surface"
role="progressbar"
aria-label={`${option.name} quota remaining`}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={usage.remainingPercentage}
>
<div
className={`h-full rounded-full transition-[width] duration-300 ${tone.bar}`}
style={{ width: `${usage.remainingPercentage}%` }}
/>
</div>
</div>
</div>
);
}
function UsageSkeleton() {
return (
<div className="space-y-3" aria-label="Loading token usage">
{[1, 2].map((key) => (
<div key={key} className="animate-pulse rounded-xl border border-border-subtle p-4">
<div className="h-4 w-24 rounded bg-surface-2" />
<div className="mt-4 h-2 w-full rounded-full bg-surface-2" />
<div className="mt-4 h-2 w-4/5 rounded-full bg-surface-2" />
</div>
))}
</div>
);
}
export default function TokenLimitsUsage({ userId, refreshKey = 0 }) {
const [state, setState] = useState({ status: "loading", data: null, error: "" });
useEffect(() => {
const controller = new AbortController();
async function loadUsage() {
setState((current) => ({ ...current, status: "loading", error: "" }));
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, error: "" });
} catch (error) {
if (error?.name !== "AbortError") {
setState({ status: "error", data: null, error: error?.message || "Failed to load token usage" });
}
}
}
void loadUsage();
return () => controller.abort();
}, [refreshKey, userId]);
if (state.status === "loading") return <UsageSkeleton />;
if (state.status === "error") {
return (
<div className="flex items-start gap-3 rounded-xl border border-red-500/25 bg-red-500/5 px-4 py-3">
<span className="material-symbols-outlined mt-0.5 text-[19px] text-red-500">error</span>
<div>
<p className="text-sm font-medium text-text-main">Usage data is unavailable</p>
<p className="mt-0.5 text-xs text-text-muted">{state.error}</p>
</div>
</div>
);
}
return (
<div className="space-y-3">
{TOKEN_LIMIT_PROVIDER_OPTIONS.map((provider) => {
const providerUsage = state.data?.providers?.[provider.id];
const remainingPercentage = getProviderRemainingPercentage(providerUsage);
const tone = remainingPercentage === null ? null : getQuotaTone(remainingPercentage);
return (
<section key={provider.id} className="overflow-hidden rounded-xl border border-border-subtle bg-surface-2/25">
<header className="flex items-center justify-between gap-3 border-b border-border-subtle px-4 py-3">
<div className="flex min-w-0 items-center gap-3">
<span className="material-symbols-outlined text-[20px] text-brand-500">{provider.icon}</span>
<div className="min-w-0">
<h3 className="text-sm font-semibold text-text-main">{provider.name}</h3>
<p className="truncate text-[11px] text-text-muted">{provider.description}</p>
</div>
</div>
{remainingPercentage === null ? (
<span className="shrink-0 text-xs font-medium text-text-muted">No limits</span>
) : (
<span className={`inline-flex shrink-0 items-center gap-1.5 text-xs font-semibold tabular-nums ${tone.text}`}>
<span className={`size-1.5 rounded-full ${tone.dot}`} aria-hidden="true" />
{remainingPercentage}% min.
</span>
)}
</header>
<div className="divide-y divide-border-subtle px-4 py-3">
{TOKEN_LIMIT_WINDOW_OPTIONS.map((option) => (
<UsageWindow key={option.id} option={option} usage={providerUsage?.[option.id]} />
))}
</div>
</section>
);
})}
</div>
);
}
UsageWindow.propTypes = {
option: PropTypes.shape({
name: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
}).isRequired,
usage: PropTypes.shape({
limit: PropTypes.number,
used: PropTypes.number,
remainingPercentage: PropTypes.number,
}),
};
TokenLimitsUsage.propTypes = {
userId: PropTypes.string.isRequired,
refreshKey: PropTypes.number,
};
@@ -0,0 +1,76 @@
import {
USER_TOKEN_LIMIT_PROVIDERS,
USER_TOKEN_LIMIT_WINDOWS,
} from "open-sse/config/userTokenLimits.js";
const TOKEN_FORMATTER = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
const COMPACT_TOKEN_FORMATTER = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
});
export const TOKEN_LIMIT_PROVIDER_OPTIONS = Object.freeze([
{
id: USER_TOKEN_LIMIT_PROVIDERS.ORBIT,
name: "Orbit Provider",
shortName: "Orbit",
description: "Anthropic-compatible traffic routed through Orbit.",
icon: "orbit",
},
{
id: USER_TOKEN_LIMIT_PROVIDERS.CODEX,
name: "Codex",
shortName: "Codex",
description: "OpenAI Codex responses and coding sessions.",
icon: "terminal",
},
]);
export const TOKEN_LIMIT_WINDOW_OPTIONS = Object.freeze([
{
id: USER_TOKEN_LIMIT_WINDOWS.SESSION,
name: "Session",
description: "Rolling 5 hours",
},
{
id: USER_TOKEN_LIMIT_WINDOWS.WEEKLY,
name: "Weekly",
description: "Resets Monday, Vietnam time",
},
]);
export function getQuotaTone(remainingPercentage) {
if (remainingPercentage > 70) {
return {
bar: "bg-emerald-500",
dot: "bg-emerald-500",
text: "text-emerald-600 dark:text-emerald-400",
};
}
if (remainingPercentage >= 30) {
return {
bar: "bg-amber-500",
dot: "bg-amber-500",
text: "text-amber-600 dark:text-amber-400",
};
}
return {
bar: "bg-red-500",
dot: "bg-red-500",
text: "text-red-600 dark:text-red-400",
};
}
export function getProviderRemainingPercentage(providerUsage) {
const activeWindows = TOKEN_LIMIT_WINDOW_OPTIONS
.map(({ id }) => providerUsage?.[id])
.filter((windowUsage) => windowUsage?.limit > 0);
if (activeWindows.length === 0) return null;
return Math.min(...activeWindows.map((windowUsage) => windowUsage.remainingPercentage));
}
export function formatTokenCount(value, compact = false) {
const amount = Math.max(0, Number(value) || 0);
return (compact ? COMPACT_TOKEN_FORMATTER : TOKEN_FORMATTER).format(amount);
}
+31 -24
View File
@@ -6,26 +6,12 @@ import { Button, Card, Input } from "@/shared/components";
import Modal, { ConfirmModal } from "@/shared/components/Modal";
import useUserStore from "@/store/userStore";
import { formatVietnamDateTime } from "@/shared/utils/dateTime";
import {
USER_TOKEN_LIMIT_PROVIDERS,
USER_TOKEN_LIMIT_WINDOWS,
} from "open-sse/config/userTokenLimits.js";
import { USER_TOKEN_LIMIT_WINDOWS } from "open-sse/config/userTokenLimits.js";
import QuotaCell from "./components/QuotaCell";
import TokenLimitsUsage from "./components/TokenLimitsUsage";
import { TOKEN_LIMIT_PROVIDER_OPTIONS } from "./components/tokenLimitDisplay.js";
const EMPTY_FORM = { username: "", password: "", role: "user", isActive: true };
const TOKEN_LIMIT_PROVIDER_OPTIONS = [
{
id: USER_TOKEN_LIMIT_PROVIDERS.ORBIT,
name: "Orbit Provider",
description: "Anthropic-compatible traffic routed through Orbit.",
icon: "orbit",
},
{
id: USER_TOKEN_LIMIT_PROVIDERS.CODEX,
name: "Codex",
description: "OpenAI Codex responses and coding sessions.",
icon: "terminal",
},
];
function createEmptyTokenLimits() {
return Object.fromEntries(TOKEN_LIMIT_PROVIDER_OPTIONS.map(({ id }) => [
@@ -58,6 +44,7 @@ export default function UsersPage() {
const [limitsLoading, setLimitsLoading] = useState(false);
const [limitsSaving, setLimitsSaving] = useState(false);
const [limitsError, setLimitsError] = useState("");
const [quotaRefreshKey, setQuotaRefreshKey] = useState(0);
const loadUsers = useCallback(async () => {
setLoading(true);
@@ -192,6 +179,7 @@ export default function UsersPage() {
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Failed to save token limits");
setQuotaRefreshKey((current) => current + 1);
setLimitEditor(null);
} catch (requestError) {
setLimitsError(requestError.message || "Failed to save token limits");
@@ -228,20 +216,24 @@ export default function UsersPage() {
<th className="px-5 py-3 font-medium">Username</th>
<th className="px-5 py-3 font-medium">Role</th>
<th className="px-5 py-3 font-medium">Status</th>
<th className="px-5 py-3 font-medium">Quota remaining</th>
<th className="px-5 py-3 font-medium">Created</th>
<th className="px-5 py-3 text-right font-medium">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-border-subtle">
{loading ? (
<tr><td colSpan="5" className="px-5 py-12 text-center text-text-muted">Loading users</td></tr>
<tr><td colSpan="6" className="px-5 py-12 text-center text-text-muted">Loading users</td></tr>
) : users.length === 0 ? (
<tr><td colSpan="5" className="px-5 py-12 text-center text-text-muted">No users found.</td></tr>
<tr><td colSpan="6" className="px-5 py-12 text-center text-text-muted">No users found.</td></tr>
) : users.map((entry) => (
<tr key={entry.id} className="transition-colors hover:bg-surface-2/40">
<td className="px-5 py-4 font-medium text-text-main">{entry.username}{entry.id === user.id ? <span className="ml-2 text-xs font-normal text-text-muted">(you)</span> : null}</td>
<td className="px-5 py-4"><span className={`rounded-full px-2 py-1 text-xs font-medium ${entry.role === "admin" ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"}`}>{entry.role}</span></td>
<td className="px-5 py-4"><span className={entry.isActive ? "text-emerald-600 dark:text-emerald-400" : "text-text-muted"}>{entry.isActive ? "Active" : "Disabled"}</span></td>
<td className="px-5 py-4">
{entry.role === "user" ? <QuotaCell userId={entry.id} refreshKey={quotaRefreshKey} /> : <span className="text-xs text-text-muted"></span>}
</td>
<td className="px-5 py-4 text-text-muted">{formatDate(entry.createdAt)}</td>
<td className="px-5 py-4 text-right">
<div className="flex justify-end gap-2">
@@ -274,10 +266,11 @@ export default function UsersPage() {
<Modal
isOpen={!!limitEditor}
onClose={() => !limitsSaving && setLimitEditor(null)}
title={`Token limits · ${limitEditor?.username || "user"}`}
title={`Usage & limits · ${limitEditor?.username || "user"}`}
size="xl"
footer={<><Button variant="ghost" onClick={() => setLimitEditor(null)} disabled={limitsSaving}>Cancel</Button><Button variant="primary" onClick={saveTokenLimits} loading={limitsSaving} disabled={limitsLoading}>Save limits</Button></>}
>
<div className="space-y-5">
<div className="space-y-6">
<div className="rounded-xl border border-brand-500/20 bg-brand-500/5 px-4 py-3">
<div className="flex items-start gap-3">
<span className="material-symbols-outlined mt-0.5 text-[20px] text-brand-500">hourglass_top</span>
@@ -290,10 +283,24 @@ export default function UsersPage() {
{limitsError ? <p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400">{limitsError}</p> : null}
{limitEditor ? (
<section className="space-y-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-brand-500">Current headroom</p>
<p className="mt-1 text-sm text-text-muted">The lowest active window determines the quota shown in the users table.</p>
</div>
<TokenLimitsUsage userId={limitEditor.id} refreshKey={quotaRefreshKey} />
</section>
) : null}
{limitsLoading ? (
<div className="py-10 text-center text-sm text-text-muted">Loading token limits</div>
) : (
<div className="space-y-3">
<section className="space-y-3 border-t border-border-subtle pt-6">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-brand-500">Budget settings</p>
<p className="mt-1 text-sm text-text-muted">Set 0 to leave a provider window unlimited.</p>
</div>
{TOKEN_LIMIT_PROVIDER_OPTIONS.map((provider) => (
<section key={provider.id} className="rounded-xl border border-border-subtle bg-surface-2/35 p-4">
<div className="mb-4 flex items-center gap-3">
@@ -333,7 +340,7 @@ export default function UsersPage() {
</div>
</section>
))}
</div>
</section>
)}
</div>
</Modal>
@@ -0,0 +1,97 @@
import { NextResponse } from "next/server";
import {
getUserById,
getUserProviderTokenUsageSince,
getUserTokenLimits,
} from "@/lib/db/index.js";
import { requireAdminUser } from "@/lib/auth/currentUser.js";
import { getUserTokenLimitWindowStart } from "@/lib/tokenLimitEnforcer.js";
import {
USER_TOKEN_LIMIT_PROVIDER_IDS,
USER_TOKEN_LIMIT_WINDOW_IDS,
} from "open-sse/config/userTokenLimits.js";
export const dynamic = "force-dynamic";
const NO_STORE_HEADERS = { "Cache-Control": "no-store" };
function errorResponse(error) {
const message = error?.message || "Request failed";
const status = message === "Unauthorized"
? 401
: message === "Forbidden"
? 403
: message === "User not found"
? 404
: 400;
return NextResponse.json({ error: message }, { status, headers: NO_STORE_HEADERS });
}
function buildWindowUsage(limit, used, windowStart) {
const normalizedLimit = Math.max(0, Number(limit) || 0);
const normalizedUsed = Math.max(0, Number(used) || 0);
const remaining = normalizedLimit > 0
? Math.max(0, normalizedLimit - normalizedUsed)
: null;
const remainingPercentage = normalizedLimit > 0
? Math.round((remaining / normalizedLimit) * 100)
: null;
return {
limit: normalizedLimit,
used: normalizedUsed,
remaining,
remainingPercentage,
windowStart: windowStart.toISOString(),
};
}
export async function GET(_request, { params }) {
try {
await requireAdminUser();
const { userId } = await params;
const user = await getUserById(userId);
if (!user) throw new Error("User not found");
if (user.role !== "user") throw new Error("Token usage only applies to user accounts");
const now = new Date();
const limits = await getUserTokenLimits(user.id);
const windows = Object.fromEntries(USER_TOKEN_LIMIT_WINDOW_IDS.map((windowType) => [
windowType,
getUserTokenLimitWindowStart(windowType, now),
]));
const usageEntries = await Promise.all(
USER_TOKEN_LIMIT_PROVIDER_IDS.flatMap((provider) => (
USER_TOKEN_LIMIT_WINDOW_IDS.map(async (windowType) => {
const used = await getUserProviderTokenUsageSince(
user.id,
provider,
windows[windowType],
);
return [provider, windowType, used];
})
)),
);
const usageByProvider = Object.fromEntries(
USER_TOKEN_LIMIT_PROVIDER_IDS.map((provider) => [provider, {}]),
);
for (const [provider, windowType, used] of usageEntries) {
usageByProvider[provider][windowType] = buildWindowUsage(
limits[provider]?.[windowType],
used,
windows[windowType],
);
}
return NextResponse.json({
userId: user.id,
providers: usageByProvider,
updatedAt: now.toISOString(),
}, { headers: NO_STORE_HEADERS });
} catch (error) {
return errorResponse(error);
}
}
+125
View File
@@ -0,0 +1,125 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const requireAdminUser = vi.fn();
const getUserById = vi.fn();
const getUserTokenLimits = vi.fn();
const getUserProviderTokenUsageSince = vi.fn();
const getUserTokenLimitWindowStart = vi.fn();
vi.mock("next/server", () => ({
NextResponse: {
json(body, init = {}) {
return new Response(JSON.stringify(body), {
status: init.status || 200,
headers: { "Content-Type": "application/json", ...(init.headers || {}) },
});
},
},
}));
vi.mock("@/lib/auth/currentUser.js", () => ({ requireAdminUser }));
vi.mock("@/lib/db/index.js", () => ({
getUserById,
getUserTokenLimits,
getUserProviderTokenUsageSince,
}));
vi.mock("@/lib/tokenLimitEnforcer.js", () => ({ getUserTokenLimitWindowStart }));
const { GET } = await import("@/app/api/users/[userId]/token-usage/route.js");
const context = (userId = "user-1") => ({ params: Promise.resolve({ userId }) });
const request = new Request("https://9router.local/api/users/user-1/token-usage");
const sessionStart = new Date("2026-07-17T05:00:00.000Z");
const weeklyStart = new Date("2026-07-13T17:00:00.000Z");
const limits = {
"orbit-provider": { session: 100, weekly: 1000 },
codex: { session: 0, weekly: 500 },
};
function usageKey(provider, since) {
return `${provider}|${since.toISOString()}`;
}
describe("/api/users/[userId]/token-usage", () => {
beforeEach(() => {
requireAdminUser.mockReset();
getUserById.mockReset();
getUserTokenLimits.mockReset();
getUserProviderTokenUsageSince.mockReset();
getUserTokenLimitWindowStart.mockReset();
requireAdminUser.mockResolvedValue({ id: "admin-1", role: "admin" });
getUserById.mockResolvedValue({ id: "user-1", role: "user", isActive: true });
getUserTokenLimits.mockResolvedValue(limits);
getUserTokenLimitWindowStart.mockImplementation((windowType) => (
windowType === "session" ? sessionStart : weeklyStart
));
const usage = new Map([
[usageKey("orbit-provider", sessionStart), 25],
[usageKey("orbit-provider", weeklyStart), 1200],
[usageKey("codex", sessionStart), 12],
[usageKey("codex", weeklyStart), 400],
]);
getUserProviderTokenUsageSince.mockImplementation(async (_userId, provider, since) => (
usage.get(usageKey(provider, since)) || 0
));
});
it("returns usage and remaining headroom for every provider window", async () => {
const response = await GET(request, context());
const payload = await response.json();
expect(response.status).toBe(200);
expect(response.headers.get("Cache-Control")).toBe("no-store");
expect(payload).toMatchObject({
userId: "user-1",
providers: {
"orbit-provider": {
session: {
limit: 100,
used: 25,
remaining: 75,
remainingPercentage: 75,
windowStart: sessionStart.toISOString(),
},
weekly: {
limit: 1000,
used: 1200,
remaining: 0,
remainingPercentage: 0,
windowStart: weeklyStart.toISOString(),
},
},
codex: {
session: {
limit: 0,
used: 12,
remaining: null,
remainingPercentage: null,
windowStart: sessionStart.toISOString(),
},
weekly: {
limit: 500,
used: 400,
remaining: 100,
remainingPercentage: 20,
windowStart: weeklyStart.toISOString(),
},
},
},
});
expect(payload.updatedAt).toEqual(expect.any(String));
expect(getUserProviderTokenUsageSince).toHaveBeenCalledTimes(4);
});
it("requires an administrator and an existing regular user", async () => {
requireAdminUser.mockRejectedValueOnce(new Error("Forbidden"));
expect((await GET(request, context())).status).toBe(403);
getUserById.mockResolvedValueOnce(null);
expect((await GET(request, context("missing-user"))).status).toBe(404);
getUserById.mockResolvedValueOnce({ id: "admin-2", role: "admin", isActive: true });
expect((await GET(request, context("admin-2"))).status).toBe(400);
});
});