mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
feat(admin): the AI card shows the day's spend split by who paid
The provider-health route answered a spendToday payload that nothing rendered, which is the pattern the two-ended rule exists to stop. It is also the screen the operator opens when a surface refuses on budget, and the one mixed number could not answer the question that raises: a day at 1.24 million tokens is alarming until the card says that 151 200 of them were the instance's own key and the rest ran on the users' own plans. The operator ceiling is enforced against the second figure. Copy lands in all seven locales.
This commit is contained in:
1 parent
7b21840630
commit
c66bcbc295
9 files changed
+118
-9
No files matched your search
@@ -0,0 +1,72 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* v1.38.19 (Wave E) — the day's spend, split by who paid for it.
|
||||
*
|
||||
* `/api/admin/provider-health` answers `spendToday` and nothing rendered it,
|
||||
* which is the "the reader is the follow-up" shape the two-ended rule exists to
|
||||
* stop. It is also the one screen that answers the question a budget refusal
|
||||
* raises: a day at 1.24 M tokens is alarming until you can see that 151 200 of
|
||||
* them were the instance's own key and the rest ran on the users' own plans —
|
||||
* and it is the second figure the operator ceiling is enforced against.
|
||||
*/
|
||||
|
||||
const mockQueryState = vi.hoisted(() => ({
|
||||
data: null as null | object,
|
||||
isPending: false,
|
||||
isError: false,
|
||||
}));
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({
|
||||
data: mockQueryState.data,
|
||||
isPending: mockQueryState.isPending,
|
||||
isError: mockQueryState.isError,
|
||||
}),
|
||||
}));
|
||||
|
||||
import { ProviderHealthSection } from "../provider-health-section";
|
||||
|
||||
function render(data: object | null) {
|
||||
mockQueryState.data = data;
|
||||
mockQueryState.isPending = false;
|
||||
mockQueryState.isError = false;
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<ProviderHealthSection />
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("<ProviderHealthSection> — today's spend by cost owner", () => {
|
||||
it("shows both figures and the day they belong to", () => {
|
||||
const html = render({
|
||||
providers: [],
|
||||
spendToday: {
|
||||
dateKey: "2026-09-11",
|
||||
totalTokens: 1_240_000,
|
||||
operatorTokens: 151_200,
|
||||
},
|
||||
});
|
||||
expect(html).toContain("2026-09-11");
|
||||
expect(html).toContain("1,240,000");
|
||||
expect(html).toContain("151,200");
|
||||
});
|
||||
|
||||
it("renders a zero day rather than hiding the readout", () => {
|
||||
const html = render({
|
||||
providers: [],
|
||||
spendToday: { dateKey: "2026-09-11", totalTokens: 0, operatorTokens: 0 },
|
||||
});
|
||||
expect(html).toContain('data-slot="ai-spend-today"');
|
||||
});
|
||||
|
||||
it("says nothing when the payload carries no spend", () => {
|
||||
expect(render({ providers: [] })).not.toContain(
|
||||
'data-slot="ai-spend-today"',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -26,6 +26,20 @@ import { formatDateTime } from "@/lib/format";
|
||||
* failure, which never had a status to record.
|
||||
*/
|
||||
|
||||
/**
|
||||
* v1.38.19 (Wave E) — the day's spend, split by who pays for it. The operator
|
||||
* opens this card when a surface refuses on budget, and one mixed number could
|
||||
* not answer the question that raises: a day at 1.24 M tokens is alarming until
|
||||
* you see that 151 200 of them were the instance's own key and the rest ran on
|
||||
* the users' own plans. The operator ceiling is enforced against the second
|
||||
* figure, so both belong on screen.
|
||||
*/
|
||||
interface SpendToday {
|
||||
dateKey: string;
|
||||
totalTokens: number;
|
||||
operatorTokens: number;
|
||||
}
|
||||
|
||||
interface ProviderHealthRow {
|
||||
providerType: string;
|
||||
tracked: number;
|
||||
@@ -37,12 +51,18 @@ interface ProviderHealthRow {
|
||||
}
|
||||
|
||||
export function ProviderHealthSection() {
|
||||
const { t } = useTranslations();
|
||||
const { t, locale } = useTranslations();
|
||||
// Seven-digit token counts are the point of this readout; ungrouped digits
|
||||
// are what made the mixed figure unreadable in the first place.
|
||||
const groupDigits = (value: number): string =>
|
||||
new Intl.NumberFormat(locale).format(value);
|
||||
|
||||
const { data, isPending, isError } = useQuery({
|
||||
queryKey: queryKeys.adminProviderHealth(),
|
||||
queryFn: () =>
|
||||
apiGet<{ providers: ProviderHealthRow[] }>("/api/admin/provider-health"),
|
||||
apiGet<{ providers: ProviderHealthRow[]; spendToday: SpendToday }>(
|
||||
"/api/admin/provider-health",
|
||||
),
|
||||
});
|
||||
|
||||
const typeLabel = (type: string): string => {
|
||||
@@ -67,6 +87,7 @@ export function ProviderHealthSection() {
|
||||
};
|
||||
|
||||
const providers = data?.providers ?? [];
|
||||
const spendToday = data?.spendToday ?? null;
|
||||
|
||||
return (
|
||||
<SettingsCard>
|
||||
@@ -75,6 +96,15 @@ export function ProviderHealthSection() {
|
||||
title={t("admin.providerHealth.title")}
|
||||
description={t("admin.providerHealth.description")}
|
||||
/>
|
||||
{spendToday ? (
|
||||
<p className="text-muted-foreground text-sm" data-slot="ai-spend-today">
|
||||
{t("admin.providerHealth.spendToday", {
|
||||
date: spendToday.dateKey,
|
||||
total: groupDigits(spendToday.totalTokens),
|
||||
operator: groupDigits(spendToday.operatorTokens),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
{isPending ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("admin.providerHealth.loading")}
|
||||
|
||||
Reference in new issue
Block a user