Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions public/admin/app-accounts-render.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ function limitSummaryText(account, windows) {
.join(" \u00b7 ");
}

const UPDATING_HTML =
'<span class="updating" role="status">Updating limits\u2026</span>';
const UNCONFIRMED_TAG =
'<span class="tag" title="The limits read after the reset hasn\'t arrived yet">Couldn\'t confirm yet</span>';

function accountRowHtml(account, serving, grouped = false) {
const name = accountName(account);
const open = state.expandedAccountId === account.id;
Expand All @@ -152,12 +157,20 @@ function accountRowHtml(account, serving, grouped = false) {
const resetSeverity = reset ? limitSeverity(reset.percent) : "";
const countdown = resetCountdown(reset?.resetsAt);
const usage = accountUsageForId(account.id);
const phoneParts = [
tokenNote && `<span class="err">${tokenNote}</span>`,
const wait = state.limitsUpdating.get(account.id);
const updating = wait && !wait.timedOut;
const limitParts = [
limitSummaryText(account, windows),
reset &&
`<span class="${resetSeverity}">${escapeHtml(reset.label)} resets ${escapeHtml(countdown)}</span>`,
].filter(Boolean);
const phoneParts = [
tokenNote && `<span class="err">${tokenNote}</span>`,
updating && UPDATING_HTML,
updating
? `<span class="stale">${limitParts.join(" \u00b7 ")}</span>`
: limitParts.join(" \u00b7 "),
].filter(Boolean);
const cursor = state.cursorAccountId === account.id ? " cursor" : "";
const credits = resetOfferFor(account);
const expiring = serving ? expiringResets(account) : null;
Expand All @@ -168,9 +181,9 @@ function accountRowHtml(account, serving, grouped = false) {
: "";

return `<div class="row t-acc${open ? " open" : ""}${serving ? "" : " dim"}${cursor}" data-account-id="${account.id}" data-flip="account:${account.id}" tabindex="0" role="button" aria-expanded="${open}">
<span class="name"><span class="dot ${dot}"></span><span class="ellipsis">${escapeHtml(name)}</span>${account.isPrimary ? '<span class="tag tag-primary">Primary</span>' : ""}${isFreeTierAccount(account) ? `<span class="tag">${grouped ? escapeHtml(providerName(account.provider)) : "Free"}</span>` : ""}${account.enabled === false ? '<span class="tag">Disabled</span>' : ""}${creditHint}${tokenNote ? `<span class="token-note err small d">${tokenNote}</span>` : ""}</span>
<span class="meters d">${limitMetersHtml(account, windows)}</span>
<span class="num d reset ${resetSeverity || "muted"}">${reset ? `<small>${escapeHtml(reset.label)}</small> ` : ""}${escapeHtml(countdown)}</span>
<span class="name"><span class="dot ${dot}"></span><span class="ellipsis">${escapeHtml(name)}</span>${account.isPrimary ? '<span class="tag tag-primary">Primary</span>' : ""}${isFreeTierAccount(account) ? `<span class="tag">${grouped ? escapeHtml(providerName(account.provider)) : "Free"}</span>` : ""}${account.enabled === false ? '<span class="tag">Disabled</span>' : ""}${creditHint}${wait?.timedOut ? UNCONFIRMED_TAG : ""}${tokenNote ? `<span class="token-note err small d">${tokenNote}</span>` : ""}</span>
<span class="meters d${updating ? " stale" : ""}">${limitMetersHtml(account, windows)}</span>
${updating ? `<span class="num d reset muted">${UPDATING_HTML}</span>` : `<span class="num d reset ${resetSeverity || "muted"}">${reset ? `<small>${escapeHtml(reset.label)}</small> ` : ""}${escapeHtml(countdown)}</span>`}
<span class="num">${requestsCellHtml(usage)}</span>
<button class="more" type="button" data-menu="account" data-account-id="${account.id}" aria-label="Actions for ${escapeHtml(name)}" aria-haspopup="menu" aria-expanded="false">${icon("more")}</button>
<span class="l2">${phoneParts.join(" \u00b7 ")}</span>
Expand All @@ -189,21 +202,21 @@ function resetText(window) {
function limitFacts(account) {
const quota = account.quota;
const windows = accountWindows(account);
const reset = resetCreditsDetail(account, windows);
const wait = state.limitsUpdating.get(account.id);
const stale = wait && !wait.timedOut ? " stale" : "";
if (!quota)
return [
fact("Rate limits", '<span class="muted">\u2014</span>', [
"waiting for the first sync",
]),
reset?.fallback,
];
const facts = windows.map((window) => {
const severity = limitSeverity(window.percent);
return fact(
escapeHtml(window.named ? window.label : `${window.label} window`),
`<span class="${severity}">${Math.round(window.percent)}%</span>`,
[escapeHtml(resetText(window)), reset?.window === window && reset.line],
`<span class="bar${severity ? ` ${severity}` : ""}"><i style="width:${window.percent}%"></i></span>`
`<span class="${severity}${stale}">${Math.round(window.percent)}%</span>`,
[escapeHtml(resetText(window))],
`<span class="bar${severity ? ` ${severity}` : ""}${stale}"><i style="width:${window.percent}%"></i></span>`
);
});
if (isRateLimitedWithData(quota))
Expand All @@ -224,7 +237,6 @@ function limitFacts(account) {
);
else if (!facts.length)
facts.push(fact("Rate limits", '<span class="muted">None reported</span>'));
if (reset?.fallback) facts.push(reset.fallback);
return facts;
}

Expand Down Expand Up @@ -366,9 +378,11 @@ function accountDetailHtml(account) {
account.provider === "codex" || account.provider === "claude";
const button = (label, action, shortcut, cls = "btn btn-ghost btn-sm") =>
`<button class="${cls}" data-action="${action}" data-account-id="${account.id}" ${actionMutationAttributes(action, account.id)} type="button">${label}${kbd(shortcut)}</button>`;
const synced = account.quota?.fetchedAt
? `<span>${limitsSourceText(account.quota)}</span>`
const wait = state.limitsUpdating.get(account.id);
let synced = account.quota?.fetchedAt
? `<span>${limitsSourceText(account.quota)}${wait?.timedOut ? " \u00b7 couldn't confirm the new limits yet" : ""}</span>`
: "";
if (wait && !wait.timedOut) synced = UPDATING_HTML;
const entering =
state.enteringDetail === `account:${account.id}` ? " entering" : "";
return `<div class="detail${entering}" data-detail-for="${account.id}" data-flip="detail:${account.id}">
Expand All @@ -379,6 +393,7 @@ function accountDetailHtml(account) {
tokensFact(accountUsageForId(account.id)),
accountFact(account),
].join("")}</div>
${resetCreditsDetail(account)}
${hasDailyTokens(account.id) ? `<div class="daily-tokens" data-daily-for="${account.id}">${dailyTokensBodyHtml(account.id)}</div>` : ""}
${hasTraffic(accountUsageForId(account.id)) ? `<div class="usage-slot usage-grid" data-usage-for="account:${account.id}">${usageSlotHtml("account", account.id)}</div>` : ""}
<div class="actions">
Expand Down
50 changes: 47 additions & 3 deletions public/admin/app-accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,38 @@ import {
switchToTab,
} from "./app-usage.js";

const RESET_CONFIRM_RELOAD_MS = 6000;
const RESET_CONFIRM_TIMEOUT_MS = 20_000;

// Ends the wait once a limits read newer than the one shown at the reset
// arrives.
function settleLimitsUpdating() {
for (const [id, wait] of state.limitsUpdating) {
const fetchedAt = state.accountsById.get(id)?.quota?.fetchedAt;
if (!state.accountsById.has(id) || (fetchedAt ?? 0) > wait.after)
state.limitsUpdating.delete(id);
}
}

// Marks the account's limits as updating until a read taken after
// `respondedAt` (server time) arrives, re-reading the accounts twice and
// giving up quietly at the second read.
function awaitLimitsAfterReset(accountId, respondedAt) {
const wait = { after: respondedAt, timedOut: false };
state.limitsUpdating.set(accountId, wait);
renderAccounts();
const reload = () => {
loadAccounts().catch(() => undefined);
};
setTimeout(reload, RESET_CONFIRM_RELOAD_MS);
setTimeout(() => {
if (state.limitsUpdating.get(accountId) !== wait) return;
wait.timedOut = true;
renderAccounts();
reload();
}, RESET_CONFIRM_TIMEOUT_MS);
}

async function loadAccounts() {
const requestSeq = ++state.accountsRequestSeq;
invalidateUsageDetails("account");
Expand Down Expand Up @@ -61,8 +93,12 @@ async function loadAccounts() {
claudeResets: account.quota
? account.claudeResets
: state.accountsById.get(account.id)?.claudeResets,
codexResets: account.quota
? account.codexResets
: state.accountsById.get(account.id)?.codexResets,
}));
state.accountsById = usageMapFromList(state.accounts, "id");
settleLimitsUpdating();
pruneUsageDetails("account", state.accountsById);
pruneUsageDetails("daily", state.accountsById);
state.providerStatuses = accountsResult.value.providers || [];
Expand Down Expand Up @@ -118,7 +154,7 @@ async function loadMissingQuotas(requestSeq, missingQuotas) {
);
if (!missing) return;
try {
const { quotas, profiles, claudeResets } = await api(
const { quotas, profiles, claudeResets, codexResets } = await api(
"/admin/accounts/quotas"
);
if (requestSeq !== state.accountsRequestSeq) return;
Expand All @@ -138,7 +174,12 @@ async function loadMissingQuotas(requestSeq, missingQuotas) {
account.claudeResets = claudeResets[account.id];
changed = true;
}
if (codexResets && account.id in codexResets) {
account.codexResets = codexResets[account.id];
changed = true;
}
}
settleLimitsUpdating();
if (changed) {
renderAccounts();
if (state.keysLoaded) renderKeys();
Expand Down Expand Up @@ -289,16 +330,19 @@ async function redeemResetCreditAction(request, accountId) {
if (!confirmed) return;

let feedback;
let respondedAt = Date.now();
try {
const { result } = await request(
const response = await request(
`/admin/accounts/${accountId}/tracking/${account.provider}/reset-credits/consume`,
{ method: "POST", body: JSON.stringify({ resetId: offer.resetId }) }
);
feedback = resetProviders[account.provider].resultToast(result);
feedback = resetProviders[account.provider].resultToast(response.result);
respondedAt = response.respondedAt ?? respondedAt;
} catch (error) {
feedback = { message: error.message, type: "error" };
}
if (!request.isCurrent()) return;
if (feedback.changed) awaitLimitsAfterReset(accountId, respondedAt);
const refreshed = await loadAccounts();
if (!request.isCurrent()) return;
const { message, type } = resetFeedback(
Expand Down
82 changes: 56 additions & 26 deletions public/admin/app-claude-resets.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@ function expiryText(endsAt) {
}

const UNAVAILABLE_TEXT = {
requires_limit: "at limit only",
paused: "paused",
cooldown: "try later",
not_usable: "unavailable",
checking: "checking",
unsupported_limits: "scope not reported",
cooldown: "cooling down after a recent reset",
not_usable: "unavailable right now",
unsupported_limits: "can't confirm what it refills",
};

function claudeResetStatus(resets) {
Expand All @@ -63,10 +61,16 @@ function claudeResetStatus(resets) {
};
}
if (!resets.saved || resets.offer) return null;
const label = UNAVAILABLE_TEXT[resets.unavailable] || "waiting";
if (resets.unavailable === "requires_limit") {
return {
label: "usable once a limit is hit",
description:
"Claude only uses this reset once you hit a limit it refills.",
};
}
return {
label,
link: label,
label: UNAVAILABLE_TEXT[resets.unavailable] || "unavailable right now",
link: "Settings",
description:
resets.unavailable === "cooldown"
? "Another reset was just used. Try later."
Expand All @@ -91,17 +95,22 @@ function claudeResetOffer(account) {
];
const endsAt = offer?.endsAt ?? resets.nextEndsAt;
const weekly = quotaTimestamp(offer?.weeklyResetsAt);
const state = offer
? "now"
: resets.unavailable === "requires_limit"
? "at_limit"
: "unavailable";
return {
count: resets.saved,
unit: "reset",
noun: "saved reset",
heading: "Limit reset",
action: "Use reset",
canUse: Boolean(offer),
atLimitOnly: !offer && resets.unavailable === "requires_limit",
state,
canUse: state === "now",
atLimitOnly: state === "at_limit",
resetId: offer?.grantId,
scope: windowLabels.length ? limitsText(clears) : null,
windowLabels,
expiresAt: quotaTimestamp(endsAt),
expiryText: expiryText(endsAt),
title: offer?.label,
Expand All @@ -128,39 +137,60 @@ function claudeResetOffer(account) {
};
}

const NO_RESET_SPENT = "No reset was spent.";

const CLAIM_RESULT_TOASTS = {
reset: ["Limits reset", "success"],
already_used: [
"That reset was already used \u00b7 nothing changed now",
"That reset was already used \u00b7 nothing more was spent",
"info",
],
not_limited: ["Nothing was used \u00b7 no limit needed a reset", "info"],
cooldown: ["Another reset was just used \u00b7 nothing was used now", "info"],
ineligible: [
"This reset isn't available any more \u00b7 nothing was used",
not_limited: [
`Nothing to reset yet: no limit this reset refills is hit. ${NO_RESET_SPENT}`,
"info",
],
cooldown: [
`Another reset was used recently \u00b7 try again later. ${NO_RESET_SPENT}`,
"info",
],
ineligible: [
`This reset can't be used on this account any more. ${NO_RESET_SPENT}`,
"error",
],
};

const UNCONFIRMED_REASONS = new Set([
"stamp_indeterminate",
"reset_unconfirmed",
]);

function claimToast(claim) {
if (UNCONFIRMED_REASONS.has(claim.reason)) {
return {
message:
"Reset not confirmed yet \u00b7 the next limits check shows whether it was used",
type: "info",
};
}
if (claim.result === "unavailable") {
return {
message:
"Couldn't confirm the reset \u00b7 the next limits check will show whether it was used",
"Couldn't confirm the reset \u00b7 the next limits check shows whether it was used",
type: "error",
};
}
if (claim.result === "reset") {
return {
message: `Refilled the ${limitsText(claim.cleared)} \u00b7 1 reset used`,
type: "success",
remaining: claim.resetsLeft,
changed: true,
};
}
const [message, type] = CLAIM_RESULT_TOASTS[claim.result] || [
"Reset finished",
`Claude answered \u201c${claim.result}\u201d \u00b7 the next limits check shows whether a reset was used`,
"info",
];
return claim.result === "reset"
? {
message: `Refilled the ${limitsText(claim.cleared)}`,
type,
remaining: claim.resetsLeft,
}
: { message, type };
return { message, type };
}

const claudeResets = { offerFor: claudeResetOffer, resultToast: claimToast };
Expand Down
Loading
Loading