From 88557c9e4a72f3bc07e5572f299bc0c93bf037a7 Mon Sep 17 00:00:00 2001
From: rexdotsh <65942753+rexdotsh@users.noreply.github.com>
Date: Thu, 22 Oct 2026 12:21:55 +0000
Subject: [PATCH 1/4] feat(admin): one reset model for Codex and Claude, with
honest results
Each account's resets are usable now, usable once a limit is hit, or unavailable with a reason, shown on one line under the limits. Use only appears when a reset can be used now; Claude keeps early use with its warning, Codex has none. Every Codex and Claude result says what happened and whether anything was spent.
---
public/admin/app-accounts-render.js | 6 +--
public/admin/app-claude-resets.js | 69 +++++++++++++++----------
public/admin/app-codex-resets.js | 46 +++++++++--------
public/admin/app-reset-credits.js | 79 ++++++++++++-----------------
public/admin/styles.css | 26 ++++++++++
5 files changed, 129 insertions(+), 97 deletions(-)
diff --git a/public/admin/app-accounts-render.js b/public/admin/app-accounts-render.js
index eea8544..863ceb8 100644
--- a/public/admin/app-accounts-render.js
+++ b/public/admin/app-accounts-render.js
@@ -189,20 +189,18 @@ function resetText(window) {
function limitFacts(account) {
const quota = account.quota;
const windows = accountWindows(account);
- const reset = resetCreditsDetail(account, windows);
if (!quota)
return [
fact("Rate limits", '\u2014 ', [
"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`),
`${Math.round(window.percent)}% `,
- [escapeHtml(resetText(window)), reset?.window === window && reset.line],
+ [escapeHtml(resetText(window))],
` `
);
});
@@ -224,7 +222,6 @@ function limitFacts(account) {
);
else if (!facts.length)
facts.push(fact("Rate limits", 'None reported '));
- if (reset?.fallback) facts.push(reset.fallback);
return facts;
}
@@ -379,6 +376,7 @@ function accountDetailHtml(account) {
tokensFact(accountUsageForId(account.id)),
accountFact(account),
].join("")}
+ ${resetCreditsDetail(account)}
${hasDailyTokens(account.id) ? `
diff --git a/public/admin/app-claude-resets.js b/public/admin/app-claude-resets.js
index aaeff7b..3704796 100644
--- a/public/admin/app-claude-resets.js
+++ b/public/admin/app-claude-resets.js
@@ -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) {
@@ -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."
@@ -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,
@@ -128,39 +137,47 @@ 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",
+ ],
};
function claimToast(claim) {
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,
+ };
+ }
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 };
diff --git a/public/admin/app-codex-resets.js b/public/admin/app-codex-resets.js
index 540ab66..1fd089e 100644
--- a/public/admin/app-codex-resets.js
+++ b/public/admin/app-codex-resets.js
@@ -38,7 +38,11 @@ function codexResetOffer(account) {
const next = credits[0] || null;
const expiry = quotaTimestamp(next?.expiresAt);
const windows = accountWindows(account).filter((window) => !window.named);
- const tightest = [...windows].sort((a, b) => b.percent - a.percent)[0];
+ const state = !count
+ ? "unavailable"
+ : account.codexResets?.unavailable === "requires_limit"
+ ? "at_limit"
+ : "now";
return {
count,
next,
@@ -46,7 +50,9 @@ function codexResetOffer(account) {
noun: "reset credit",
heading: "Reset credit",
action: "Use credit",
- canUse: Boolean(count && next),
+ state,
+ canUse: state === "now" && Boolean(next),
+ atLimitOnly: state === "at_limit",
resetId: next?.id,
scope:
next?.resetType === "codex_rate_limits"
@@ -54,25 +60,22 @@ function codexResetOffer(account) {
? `${windows.map((window) => window.label).join(" + ")} windows`
: "Codex rate-limit windows"
: null,
- windowLabels:
- next?.resetType === "codex_rate_limits"
- ? windows.map((window) => window.label)
- : [],
expiresAt: expiry,
expiryText: expiry
? `${count > 1 ? "next expires" : "expires"} ${untilText(expiry)}`
: "expiry not reported",
title: next?.title,
- earlyUse:
- tightest && tightest.percent < 100
+ status:
+ state === "at_limit"
? {
- scope: `${tightest.label} window`,
- percentUsed: Math.round(tightest.percent),
+ label: "usable once a limit is hit",
+ description: "Codex only uses a credit once you hit a limit.",
}
: null,
+ earlyUse: null,
notes: [],
useful:
- count > 0 &&
+ state === "now" &&
!account.quota?.error &&
!needsReauthorization(account) &&
windows.some(
@@ -83,33 +86,36 @@ function codexResetOffer(account) {
};
}
+const NO_CREDIT_SPENT = "No credit was spent.";
+
function codexResultToast(result) {
- const windows =
- typeof result?.windowsReset === "number" && result.windowsReset > 0
- ? ` (${result.windowsReset} window${result.windowsReset === 1 ? "" : "s"} reset)`
- : "";
switch (result?.code) {
case "reset":
return {
- message: `Limits restored${windows}`,
+ message: "Limits reset \u00b7 1 credit used",
type: "success",
showBalance: true,
};
case "nothing_to_reset":
return {
- message: "Nothing to reset - limits are already clear",
+ message: `Nothing to reset yet: Codex only uses a credit once you hit a limit. ${NO_CREDIT_SPENT}`,
type: "info",
};
case "no_credit":
return {
- message: "No reset credit available on this account",
+ message: `That credit is no longer available. ${NO_CREDIT_SPENT}`,
type: "error",
};
case "already_redeemed":
- return { message: `Credit was already redeemed${windows}`, type: "info" };
+ return {
+ message:
+ "This credit already reset your limits \u00b7 nothing more was used",
+ type: "info",
+ showBalance: true,
+ };
default:
return {
- message: `Codex reset result: ${result?.code || "complete"}`,
+ message: `Codex answered \u201c${result?.code || "unknown"}\u201d \u00b7 the next limits check shows whether a credit was used`,
type: "info",
};
}
diff --git a/public/admin/app-reset-credits.js b/public/admin/app-reset-credits.js
index f26c8ee..c1a69b8 100644
--- a/public/admin/app-reset-credits.js
+++ b/public/admin/app-reset-credits.js
@@ -4,7 +4,6 @@ import { escapeHtml, mutationAttributes } from "./app-core.js";
import {
DAY_MS,
accountName,
- fact,
kbd,
needsReauthorization,
providerLabel,
@@ -39,62 +38,48 @@ function expiringResets(account) {
};
}
-function windowHours(window) {
- const span = window.label.match(/(\d+)([dh])$/);
- return span ? Number(span[1]) * (span[2] === "d" ? 24 : 1) : 0;
-}
-
-function resetCreditsDetail(account, windows) {
+// The account's resets on one line: the balance, what the owner can do with
+// it now, and what it refills.
+function resetCreditsDetail(account) {
const offer = resetOfferFor(account);
- if (!offer || (!offer.canUse && !offer.status)) return null;
- const covered = windows.filter(
- (window) =>
- (account.provider !== "codex" || !window.named) &&
- offer.windowLabels.includes(window.label)
- );
- const window = covered.sort(
- (a, b) => b.percent - a.percent || windowHours(b) - windowHours(a)
- )[0];
- const expiring = offer.expiresAt && offer.expiresAt - Date.now() < 3 * DAY_MS;
- const noun = account.provider === "codex" ? "credit" : offer.noun;
- const balance = `${offer.count ?? "\u2014"} ${noun}${offer.count === 1 ? "" : "s"}`;
- const text = offer.status
- ? offer.status.replaceCount
- ? offer.status.label
- : balance
- : !offer.scope
- ? `${balance} \u00b7 scope not reported`
- : `${balance}${account.provider === "codex" && !expiring ? " to refill" : ""}`;
+ if (!offer || (!offer.canUse && !offer.status)) return "";
+ const { status } = offer;
+ const balance = `${offer.count ?? "\u2014"} ${offer.unit}${offer.count === 1 ? "" : "s"}`;
+ const value = status?.replaceCount ? status.label : balance;
+ const left =
+ offer.expiresAt && offer.expiresAt - Date.now() < 3 * DAY_MS
+ ? `${untilText(offer.expiresAt).replace(/^in /, "")} left`
+ : null;
+ const scope = offer.scope ? `refills ${offer.scope}` : "scope not reported";
const title = [
- text,
- offer.status && !offer.status.replaceCount && offer.status.label,
- offer.scope ? `refills ${offer.scope}` : "reset scope not reported",
+ value,
+ status && !status.replaceCount && status.label,
+ scope,
offer.expiryText,
offer.title,
- offer.status?.description,
+ status?.description,
]
.filter(Boolean)
.join(" \u00b7 ");
const action = offer.canUse
? `
Use${kbd("U")} `
- : offer.status?.link
- ? `
${escapeHtml(offer.status.link)} `
+ : status?.link
+ ? `
${escapeHtml(status.link)} `
: "";
- const copy = expiring
- ? `${text} \u00b7 ${untilText(offer.expiresAt).replace(/^in /, "")} left`
- : text;
- const line = `
${escapeHtml(copy)} ${action ? `\u00b7 ${action}` : ""}`;
- return {
- window,
- line,
- fallback: window
- ? ""
- : fact(
- `${offer.heading}${offer.count === 1 ? "" : "s"}`,
- `
${escapeHtml(copy)} `,
- [action && `
${action} `]
- ),
- };
+ const details = status?.replaceCount
+ ? [status.description]
+ : [
+ status?.label,
+ offer.canUse && offer.earlyUse && "usable early",
+ left,
+ scope,
+ ];
+ return `
${escapeHtml(`${offer.heading}s`)} ${escapeHtml(value)} ${details
+ .filter(Boolean)
+ .map((detail) => ` \u00b7 ${escapeHtml(detail)}`)
+ .join(
+ ""
+ )} ${action ? `${action} ` : ""}
`;
}
function earlyUseText(earlyUse) {
diff --git a/public/admin/styles.css b/public/admin/styles.css
index 25e5387..963f32d 100644
--- a/public/admin/styles.css
+++ b/public/admin/styles.css
@@ -1796,6 +1796,32 @@ input[type="checkbox"].toggle:checked::after {
.daily-tokens {
padding-bottom: 20px;
}
+.resets {
+ display: flex;
+ gap: 4px 16px;
+ align-items: baseline;
+ padding-bottom: 20px;
+}
+.resets .fact-label {
+ flex-shrink: 0;
+}
+.resets-copy {
+ flex: 1;
+ min-width: 0;
+ font-size: 12px;
+ line-height: 18px;
+ color: var(--muted);
+}
+.resets-value {
+ color: var(--text);
+}
+.resets-action .link {
+ display: inline-flex;
+ gap: 6px;
+ align-items: center;
+ color: var(--text);
+ white-space: nowrap;
+}
.daily-head,
.daily-axis {
display: flex;
From 055740e7ad3d55c1f22091194edc1b83119b54fb Mon Sep 17 00:00:00 2001
From: rexdotsh <65942753+rexdotsh@users.noreply.github.com>
Date: Thu, 22 Oct 2026 12:30:00 +0000
Subject: [PATCH 2/4] feat(admin): re-read accounts once after a reset to show
the confirmed limits
---
public/admin/app-accounts.js | 7 +++++++
public/admin/app-claude-resets.js | 1 +
public/admin/app-codex-resets.js | 2 ++
3 files changed, 10 insertions(+)
diff --git a/public/admin/app-accounts.js b/public/admin/app-accounts.js
index 0be9429..3ccee8b 100644
--- a/public/admin/app-accounts.js
+++ b/public/admin/app-accounts.js
@@ -33,6 +33,8 @@ import {
switchToTab,
} from "./app-usage.js";
+const RESET_CONFIRM_RELOAD_MS = 6000;
+
async function loadAccounts() {
const requestSeq = ++state.accountsRequestSeq;
invalidateUsageDetails("account");
@@ -307,6 +309,11 @@ async function redeemResetCreditAction(request, accountId) {
refreshed?.find((entry) => entry.id === accountId)
);
toast(message, type);
+ // The server reads the limits once more a few seconds after a reset.
+ if (feedback.changed)
+ setTimeout(() => {
+ loadAccounts().catch(() => undefined);
+ }, RESET_CONFIRM_RELOAD_MS);
}
function startEditAccount(id) {
diff --git a/public/admin/app-claude-resets.js b/public/admin/app-claude-resets.js
index 3704796..7f48973 100644
--- a/public/admin/app-claude-resets.js
+++ b/public/admin/app-claude-resets.js
@@ -171,6 +171,7 @@ function claimToast(claim) {
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] || [
diff --git a/public/admin/app-codex-resets.js b/public/admin/app-codex-resets.js
index 1fd089e..9e621ca 100644
--- a/public/admin/app-codex-resets.js
+++ b/public/admin/app-codex-resets.js
@@ -95,6 +95,7 @@ function codexResultToast(result) {
message: "Limits reset \u00b7 1 credit used",
type: "success",
showBalance: true,
+ changed: true,
};
case "nothing_to_reset":
return {
@@ -112,6 +113,7 @@ function codexResultToast(result) {
"This credit already reset your limits \u00b7 nothing more was used",
type: "info",
showBalance: true,
+ changed: true,
};
default:
return {
From 1a009442479c21c487097c9c84f6f2445efecbfc Mon Sep 17 00:00:00 2001
From: rexdotsh <65942753+rexdotsh@users.noreply.github.com>
Date: Thu, 22 Oct 2026 13:28:50 +0000
Subject: [PATCH 3/4] feat(admin): mark limits as updating until the read after
a reset arrives
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
After a real reset the account's limits dim, with 'Updating limits…', until /admin/accounts returns a newer limits read. After about 20s the latest data shows with a quiet 'couldn't confirm yet'. Cold loads now merge codexResets from the quotas response, and an unconfirmed Claude claim says so instead of reporting a reset used.
---
public/admin/app-accounts-render.js | 35 +++++++++++++++------
public/admin/app-accounts.js | 47 +++++++++++++++++++++++++----
public/admin/app-claude-resets.js | 12 ++++++++
public/admin/app-core.js | 3 ++
public/admin/styles.css | 23 ++++++++++++++
5 files changed, 105 insertions(+), 15 deletions(-)
diff --git a/public/admin/app-accounts-render.js b/public/admin/app-accounts-render.js
index 863ceb8..75a067f 100644
--- a/public/admin/app-accounts-render.js
+++ b/public/admin/app-accounts-render.js
@@ -134,6 +134,11 @@ function limitSummaryText(account, windows) {
.join(" \u00b7 ");
}
+const UPDATING_HTML =
+ '
Updating limits\u2026 ';
+const UNCONFIRMED_TAG =
+ '
Couldn\'t confirm yet ';
+
function accountRowHtml(account, serving, grouped = false) {
const name = accountName(account);
const open = state.expandedAccountId === account.id;
@@ -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 && `
${tokenNote} `,
+ const wait = state.limitsUpdating.get(account.id);
+ const updating = wait && !wait.timedOut;
+ const limitParts = [
limitSummaryText(account, windows),
reset &&
`
${escapeHtml(reset.label)} resets ${escapeHtml(countdown)} `,
].filter(Boolean);
+ const phoneParts = [
+ tokenNote && `
${tokenNote} `,
+ updating && UPDATING_HTML,
+ updating
+ ? `
${limitParts.join(" \u00b7 ")} `
+ : limitParts.join(" \u00b7 "),
+ ].filter(Boolean);
const cursor = state.cursorAccountId === account.id ? " cursor" : "";
const credits = resetOfferFor(account);
const expiring = serving ? expiringResets(account) : null;
@@ -168,9 +181,9 @@ function accountRowHtml(account, serving, grouped = false) {
: "";
return `
-
${escapeHtml(name)} ${account.isPrimary ? 'Primary ' : ""}${isFreeTierAccount(account) ? `${grouped ? escapeHtml(providerName(account.provider)) : "Free"} ` : ""}${account.enabled === false ? 'Disabled ' : ""}${creditHint}${tokenNote ? `${tokenNote} ` : ""}
-
${limitMetersHtml(account, windows)}
-
${reset ? `${escapeHtml(reset.label)} ` : ""}${escapeHtml(countdown)}
+
${escapeHtml(name)} ${account.isPrimary ? 'Primary ' : ""}${isFreeTierAccount(account) ? `${grouped ? escapeHtml(providerName(account.provider)) : "Free"} ` : ""}${account.enabled === false ? 'Disabled ' : ""}${creditHint}${wait?.timedOut ? UNCONFIRMED_TAG : ""}${tokenNote ? `${tokenNote} ` : ""}
+
${limitMetersHtml(account, windows)}
+ ${updating ? `
${UPDATING_HTML} ` : `
${reset ? `${escapeHtml(reset.label)} ` : ""}${escapeHtml(countdown)} `}
${requestsCellHtml(usage)}
${icon("more")}
${phoneParts.join(" \u00b7 ")}
@@ -189,6 +202,8 @@ function resetText(window) {
function limitFacts(account) {
const quota = account.quota;
const windows = accountWindows(account);
+ const wait = state.limitsUpdating.get(account.id);
+ const stale = wait && !wait.timedOut ? " stale" : "";
if (!quota)
return [
fact("Rate limits", '
\u2014 ', [
@@ -199,9 +214,9 @@ function limitFacts(account) {
const severity = limitSeverity(window.percent);
return fact(
escapeHtml(window.named ? window.label : `${window.label} window`),
- `
${Math.round(window.percent)}% `,
+ `
${Math.round(window.percent)}% `,
[escapeHtml(resetText(window))],
- `
`
+ `
`
);
});
if (isRateLimitedWithData(quota))
@@ -363,9 +378,11 @@ function accountDetailHtml(account) {
account.provider === "codex" || account.provider === "claude";
const button = (label, action, shortcut, cls = "btn btn-ghost btn-sm") =>
`
${label}${kbd(shortcut)} `;
- const synced = account.quota?.fetchedAt
- ? `
${limitsSourceText(account.quota)} `
+ const wait = state.limitsUpdating.get(account.id);
+ let synced = account.quota?.fetchedAt
+ ? `
${limitsSourceText(account.quota)}${wait?.timedOut ? " \u00b7 couldn't confirm the new limits yet" : ""} `
: "";
+ if (wait && !wait.timedOut) synced = UPDATING_HTML;
const entering =
state.enteringDetail === `account:${account.id}` ? " entering" : "";
return `
diff --git a/public/admin/app-accounts.js b/public/admin/app-accounts.js
index 3ccee8b..e4ed14c 100644
--- a/public/admin/app-accounts.js
+++ b/public/admin/app-accounts.js
@@ -34,6 +34,24 @@ import {
} 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);
+ }
+}
+
+function reloadAccountsLater(delay, before = () => undefined) {
+ setTimeout(() => {
+ before();
+ loadAccounts().catch(() => undefined);
+ }, delay);
+}
async function loadAccounts() {
const requestSeq = ++state.accountsRequestSeq;
@@ -63,8 +81,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 || [];
@@ -120,7 +142,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;
@@ -140,7 +162,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();
@@ -309,11 +336,19 @@ async function redeemResetCreditAction(request, accountId) {
refreshed?.find((entry) => entry.id === accountId)
);
toast(message, type);
- // The server reads the limits once more a few seconds after a reset.
- if (feedback.changed)
- setTimeout(() => {
- loadAccounts().catch(() => undefined);
- }, RESET_CONFIRM_RELOAD_MS);
+ if (!feedback.changed) return;
+ // The server reads the limits once more a few seconds after a reset; until
+ // that read shows up the account's limits are marked as updating.
+ const wait = {
+ after: accountById(accountId)?.quota?.fetchedAt ?? 0,
+ timedOut: false,
+ };
+ state.limitsUpdating.set(accountId, wait);
+ renderAccounts();
+ reloadAccountsLater(RESET_CONFIRM_RELOAD_MS);
+ reloadAccountsLater(RESET_CONFIRM_TIMEOUT_MS, () => {
+ wait.timedOut = true;
+ });
}
function startEditAccount(id) {
diff --git a/public/admin/app-claude-resets.js b/public/admin/app-claude-resets.js
index 7f48973..12c607f 100644
--- a/public/admin/app-claude-resets.js
+++ b/public/admin/app-claude-resets.js
@@ -158,7 +158,19 @@ const CLAIM_RESULT_TOASTS = {
],
};
+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:
diff --git a/public/admin/app-core.js b/public/admin/app-core.js
index 87c485f..2486368 100644
--- a/public/admin/app-core.js
+++ b/public/admin/app-core.js
@@ -34,6 +34,9 @@ const sessionState = () => ({
keyFilter: "",
accountFilter: "",
expandedAccountId: null,
+ // Accounts waiting for the limits read after a reset: the fetchedAt shown
+ // when it was used, and whether the wait timed out.
+ limitsUpdating: new Map(),
pendingExpandedAccountId: null,
showAllReauthorize: false,
expandedKeyId: null,
diff --git a/public/admin/styles.css b/public/admin/styles.css
index 963f32d..fc13d8b 100644
--- a/public/admin/styles.css
+++ b/public/admin/styles.css
@@ -188,6 +188,29 @@ code,
.tag-primary {
color: var(--accent);
}
+.stale {
+ opacity: 0.45;
+}
+.updating {
+ display: inline-flex;
+ gap: 6px;
+ align-items: center;
+ color: var(--muted);
+ white-space: nowrap;
+}
+.updating::before {
+ width: 6px;
+ height: 6px;
+ content: "";
+ background: currentColor;
+ border-radius: 50%;
+ animation: updating-pulse 1.2s ease-in-out infinite;
+}
+@keyframes updating-pulse {
+ 50% {
+ opacity: 0.25;
+ }
+}
/* Login */
From bffe7fffd604116a99c9c5835798b87788f8bece Mon Sep 17 00:00:00 2001
From: rexdotsh <65942753+rexdotsh@users.noreply.github.com>
Date: Thu, 22 Oct 2026 14:06:29 +0000
Subject: [PATCH 4/4] fix(admin): start the updating state from the reset's
answer, and end it on timeout
The consume route returns respondedAt, and the wait starts right after the spend's answer, with both re-reads on timers that no reload can block. Limits read after respondedAt end it. The 20s timeout re-renders, so the spinner gives way to 'Couldn't confirm yet' even when every reload fails.
---
public/admin/app-accounts.js | 40 ++++++++++++++++---------------
public/admin/app-core.js | 4 ++--
src/http/routes/admin-accounts.ts | 3 ++-
3 files changed, 25 insertions(+), 22 deletions(-)
diff --git a/public/admin/app-accounts.js b/public/admin/app-accounts.js
index e4ed14c..7e75e07 100644
--- a/public/admin/app-accounts.js
+++ b/public/admin/app-accounts.js
@@ -46,11 +46,23 @@ function settleLimitsUpdating() {
}
}
-function reloadAccountsLater(delay, before = () => undefined) {
- setTimeout(() => {
- before();
+// 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);
- }, delay);
+ };
+ 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() {
@@ -318,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(
@@ -336,19 +351,6 @@ async function redeemResetCreditAction(request, accountId) {
refreshed?.find((entry) => entry.id === accountId)
);
toast(message, type);
- if (!feedback.changed) return;
- // The server reads the limits once more a few seconds after a reset; until
- // that read shows up the account's limits are marked as updating.
- const wait = {
- after: accountById(accountId)?.quota?.fetchedAt ?? 0,
- timedOut: false,
- };
- state.limitsUpdating.set(accountId, wait);
- renderAccounts();
- reloadAccountsLater(RESET_CONFIRM_RELOAD_MS);
- reloadAccountsLater(RESET_CONFIRM_TIMEOUT_MS, () => {
- wait.timedOut = true;
- });
}
function startEditAccount(id) {
diff --git a/public/admin/app-core.js b/public/admin/app-core.js
index 2486368..28eb7f6 100644
--- a/public/admin/app-core.js
+++ b/public/admin/app-core.js
@@ -34,8 +34,8 @@ const sessionState = () => ({
keyFilter: "",
accountFilter: "",
expandedAccountId: null,
- // Accounts waiting for the limits read after a reset: the fetchedAt shown
- // when it was used, and whether the wait timed out.
+ // Accounts waiting for the limits read after a reset: the server time the
+ // reset was answered, and whether the wait timed out.
limitsUpdating: new Map(),
pendingExpandedAccountId: null,
showAllReauthorize: false,
diff --git a/src/http/routes/admin-accounts.ts b/src/http/routes/admin-accounts.ts
index 0617051..7205d31 100644
--- a/src/http/routes/admin-accounts.ts
+++ b/src/http/routes/admin-accounts.ts
@@ -351,8 +351,9 @@ export const adminAccountsRoutes = new Hono()
provider === "codex"
? await redeemCodexResetCredit(db, id, resetId)
: await claimClaudeReset(db, id, resetId);
+ // Limits read after this time show the spend's effect.
return result
- ? context.json({ result })
+ ? context.json({ result, respondedAt: Date.now() })
: context.json(accountNotFoundBody, 404);
} catch (error) {
if (error instanceof ResetSpendConflict) {