codex-proxy / web /src /hooks /use-accounts.ts
icebear0828
feat: display secondary rate limit window (weekly cap) in dashboard
2996fd8
raw
history blame
5.61 kB
import { useState, useEffect, useCallback } from "preact/hooks";
export interface AccountQuotaWindow {
used_percent?: number | null;
limit_reached?: boolean;
reset_at?: number | null;
limit_window_seconds?: number | null;
}
export interface AccountQuota {
rate_limit?: AccountQuotaWindow;
/** Secondary rate limit window (e.g. weekly cap). */
secondary_rate_limit?: AccountQuotaWindow | null;
}
export interface Account {
id: string;
email: string;
status: string;
planType?: string;
usage?: {
request_count?: number;
input_tokens?: number;
output_tokens?: number;
window_request_count?: number;
window_input_tokens?: number;
window_output_tokens?: number;
};
quota?: AccountQuota;
}
export function useAccounts() {
const [list, setList] = useState<Account[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [addVisible, setAddVisible] = useState(false);
const [addInfo, setAddInfo] = useState("");
const [addError, setAddError] = useState("");
const loadAccounts = useCallback(async () => {
setRefreshing(true);
try {
const resp = await fetch("/auth/accounts?quota=true");
const data = await resp.json();
setList(data.accounts || []);
setLastUpdated(new Date());
} catch {
setList([]);
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
useEffect(() => {
loadAccounts();
}, [loadAccounts]);
// Listen for OAuth callback success
useEffect(() => {
const handler = async (event: MessageEvent) => {
if (event.data?.type === "oauth-callback-success") {
setAddVisible(false);
setAddInfo("accountAdded");
await loadAccounts();
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [loadAccounts]);
const startAdd = useCallback(async () => {
setAddInfo("");
setAddError("");
try {
const resp = await fetch("/auth/login-start", { method: "POST" });
const data = await resp.json();
if (!resp.ok || !data.authUrl) {
throw new Error(data.error || "failedStartLogin");
}
window.open(data.authUrl, "oauth_add", "width=600,height=700,scrollbars=yes");
setAddVisible(true);
// Poll for new account + focus/visibility detection
const prevResp = await fetch("/auth/accounts");
const prevData = await prevResp.json();
const prevCount = prevData.accounts?.length || 0;
let checking = false;
const checkForNewAccount = async () => {
if (checking) return;
checking = true;
try {
const r = await fetch("/auth/accounts");
const d = await r.json();
if ((d.accounts?.length || 0) > prevCount) {
cleanup();
setAddVisible(false);
setAddInfo("accountAdded");
await loadAccounts();
}
} catch {} finally {
checking = false;
}
};
// Focus event — check immediately when window regains focus
const onFocus = () => { checkForNewAccount(); };
window.addEventListener("focus", onFocus);
// Visibility change — check when tab becomes visible
const onVisible = () => {
if (document.visibilityState === "visible") checkForNewAccount();
};
document.addEventListener("visibilitychange", onVisible);
// Interval polling as fallback
const pollTimer = setInterval(checkForNewAccount, 2000);
const cleanup = () => {
clearInterval(pollTimer);
window.removeEventListener("focus", onFocus);
document.removeEventListener("visibilitychange", onVisible);
};
setTimeout(cleanup, 5 * 60 * 1000);
} catch (err) {
setAddError(err instanceof Error ? err.message : "failedStartLogin");
}
}, [loadAccounts]);
const submitRelay = useCallback(
async (callbackUrl: string) => {
setAddInfo("");
setAddError("");
if (!callbackUrl.trim()) {
setAddError("pleasePassCallback");
return;
}
try {
const resp = await fetch("/auth/code-relay", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ callbackUrl }),
});
const data = await resp.json();
if (resp.ok && data.success) {
setAddVisible(false);
setAddInfo("accountAdded");
await loadAccounts();
} else {
setAddError(data.error || "failedExchangeCode");
}
} catch (err) {
setAddError(
"networkError" + (err instanceof Error ? err.message : String(err))
);
}
},
[loadAccounts]
);
const deleteAccount = useCallback(
async (id: string) => {
try {
const resp = await fetch("/auth/accounts/" + encodeURIComponent(id), {
method: "DELETE",
});
if (!resp.ok) {
const data = await resp.json();
return data.error || "failedDeleteAccount";
}
await loadAccounts();
return null;
} catch (err) {
return "networkError" + (err instanceof Error ? err.message : "");
}
},
[loadAccounts]
);
return {
list,
loading,
refreshing,
lastUpdated,
addVisible,
addInfo,
addError,
refresh: loadAccounts,
startAdd,
submitRelay,
deleteAccount,
};
}