fix(ui): stop rendering an unreadable budget spend as a healthy $0.00

SpendBudgetCell coerces a null spend to 0 and draws an empty meter, so a
budget whose live counter could not be read rendered as untouched headroom.
The key budgets table now branches before that: an unreadable spend reads
"Unknown" against its limit with no meter, which is the difference between
"we could not tell you" and "there is nothing to tell".
This commit is contained in:
ryan-crabbe-berri 2026-08-19 17:35:14 -07:00
parent 33b519e014
commit 1cbf830083
3 changed files with 80 additions and 10 deletions

View file

@ -132,6 +132,34 @@ BudgetComparison = Literal[">=", ">"]
BudgetStatus = Literal["unlimited", "ok", "exceeded"]
BudgetNoteCode = Literal[
"alert_only",
"custom_auth_may_override_end_user_cap",
"end_user_route_only",
"model_budget_fails_open",
"per_model_counters",
"project_spend_not_tracked",
"request_tags_add_budgets",
"reservation_blocks_at_limit",
"rolling_window",
"throttled_instead_of_blocked",
"user_budget_not_applied_to_team_key",
]
BudgetNoteSeverity = Literal["info", "warning"]
BudgetSpendState = Literal["live", "no_counter", "unavailable"]
class KeyBudgetNote(BaseModel):
"""One caveat about a budget row. Branch on ``code``; ``text`` is free to be reworded."""
model_config = ConfigDict(frozen=True)
code: BudgetNoteCode
severity: BudgetNoteSeverity
text: str
class KeyBudgetEntry(BaseModel):
"""One budget that can gate requests made with a key, with its live spend."""
@ -143,6 +171,7 @@ class KeyBudgetEntry(BaseModel):
enforcement: BudgetEnforcement
max_budget: float | None = None
spend: float | None = None
spend_state: BudgetSpendState
remaining: float | None = None
comparison: BudgetComparison
budget_duration: str | None = None
@ -150,7 +179,7 @@ class KeyBudgetEntry(BaseModel):
window_start: datetime | None = None
source: str
status: BudgetStatus
note: str | None = None
notes: tuple[KeyBudgetNote, ...] = ()
class KeyBudgetsResponse(BaseModel):

View file

@ -94,6 +94,30 @@ function EnforcementCell({ entry }: { entry: KeyBudgetEntry }) {
);
}
/**
* A null spend means the live counter could not be read, which `SpendBudgetCell` coerces to 0 and
* draws as an empty meter. That renders a failed read as confident full headroom, so branch before
* reaching it: an unknown number must not look like a healthy one.
*/
function SpendCell({ entry }: { entry: KeyBudgetEntry }) {
const rule = budgetThresholdRule(entry);
return (
<div className="flex flex-col gap-0.5">
{entry.spend == null ? (
<span className="whitespace-nowrap text-xs">
<span className="font-medium text-amber-600">Unknown</span>{" "}
<span className="text-muted-foreground">
{entry.max_budget == null ? "· Unlimited" : `of $${formatNumberWithCommas(entry.max_budget, 2)}`}
</span>
</span>
) : (
<SpendBudgetCell spend={entry.spend} maxBudget={entry.max_budget} budgetDecimals={2} />
)}
{rule && <span className="whitespace-nowrap text-xs tabular-nums text-muted-foreground">{rule}</span>}
</div>
);
}
function RemainingCell({ entry }: { entry: KeyBudgetEntry }) {
const unlimited = entry.max_budget == null;
return (
@ -129,15 +153,7 @@ export const getKeyBudgetsTableColumns = (): ColumnDef<KeyBudgetEntry>[] => [
header: "Spend / Limit",
size: 200,
enableSorting: false,
cell: ({ row }) => {
const rule = budgetThresholdRule(row.original);
return (
<div className="flex flex-col gap-0.5">
<SpendBudgetCell spend={row.original.spend} maxBudget={row.original.max_budget} budgetDecimals={2} />
{rule && <span className="whitespace-nowrap text-xs tabular-nums text-muted-foreground">{rule}</span>}
</div>
);
},
cell: ({ row }) => <SpendCell entry={row.original} />,
},
{
id: "remaining",

View file

@ -281,6 +281,31 @@ describe("KeyInfoView Budgets tab", () => {
);
});
it("does not render a spend the server could not read as a healthy $0.00", async () => {
const unreadable: KeyBudgetEntry = {
...UNCONFIGURED_BUDGET,
scope: "tag",
entity_type: "tag",
entity_id: "prod",
entity_label: "prod",
max_budget: 1000,
spend: null,
remaining: null,
source: "budget_table:b-tag",
status: "ok",
note: "live spend could not be read",
};
mockBudgets([unreadable]);
const panel = await renderAndOpenBudgetsTab();
const row = rowFor(panel, "prod");
expect(row).toHaveTextContent("Unknown of $1,000.00");
expect(row).not.toHaveTextContent("$0.00");
// The meter is the part that lies loudest: drawn at 0% it reads as untouched headroom.
expect(within(row).queryByRole("meter")).not.toBeInTheDocument();
expect(row).toHaveTextContent("Blocks at ≥ $1,000.00");
});
it("renders the longest note the endpoint can emit without dropping any of it", async () => {
const endUser: KeyBudgetEntry = {
...UNCONFIGURED_BUDGET,