mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
feat(admin): name the status a failing provider came back with
The provider-health ledger has recorded the HTTP status of every failure since v1.11.0 and nothing has ever read it. The operator card could say three of five chains were failing without saying whose problem that was, which is the one thing the operator needs: a 401 is a credential only they can replace, a 429 or a 503 is the provider's afternoon and waiting is the right move. The fold carries the status of the failure whose instant the row already shows, not an aggregate over the column. A MAX of two HTTP statuses is arithmetic on a label, and a status describing a different failure than the one on screen would send the operator to the wrong place. The column joins the grouping key instead, and the fold picks the group belonging to the newest failure. Blank where there was nothing to record: a connection that never completed has no status, and a chain that has since recovered had its status cleared along with the rest of its failure state.
This commit is contained in:
1 parent
2ea08c6966
commit
7dbec3bc4d
12 files changed
+186
-2
No files matched your search
@@ -0,0 +1,73 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* The provider-health card and the HTTP status of the last failure.
|
||||
*
|
||||
* The ledger has recorded a status on every failure since v1.11.0 and no
|
||||
* surface read it, so the card could say a provider was failing without
|
||||
* saying whose problem it was. A 401 is a credential the operator has to
|
||||
* replace; a 503 is the provider's afternoon. These pin that the status
|
||||
* reaches the row, that it is left out when there was none to record,
|
||||
* and that a healthy provider does not carry a stale one.
|
||||
*/
|
||||
|
||||
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 row(over: Record<string, unknown> = {}) {
|
||||
return {
|
||||
providerType: "admin-openai",
|
||||
tracked: 5,
|
||||
failing: 2,
|
||||
maxConsecutiveFailures: 4,
|
||||
lastOkAt: null,
|
||||
lastFailureAt: "2026-08-27T06:00:00.000Z",
|
||||
lastFailureStatus: 401,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
function render(providers: object[]) {
|
||||
mockQueryState.data = { providers };
|
||||
mockQueryState.isPending = false;
|
||||
mockQueryState.isError = false;
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<ProviderHealthSection />
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("<ProviderHealthSection> — the last failure's HTTP status", () => {
|
||||
it("names the status beside the failure it belongs to", () => {
|
||||
expect(render([row()])).toContain("HTTP 401");
|
||||
});
|
||||
|
||||
it("says nothing where the failure never had a status to record", () => {
|
||||
expect(render([row({ lastFailureStatus: null })])).not.toContain("HTTP");
|
||||
});
|
||||
|
||||
it("carries no status for a provider that is not failing", () => {
|
||||
expect(
|
||||
render([
|
||||
row({ failing: 0, lastFailureAt: null, lastFailureStatus: 500 }),
|
||||
]),
|
||||
).not.toContain("HTTP");
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,12 @@ import { formatDateTime } from "@/lib/format";
|
||||
* read the database. The card shows one row per provider type: how many
|
||||
* users' chains touch it, how many of them are currently failing, the worst
|
||||
* uninterrupted failure run, and the last success / failure instants.
|
||||
*
|
||||
* The failure line also carries the HTTP status the ledger recorded for that
|
||||
* failure, because "failing" on its own does not say whose problem it is. A
|
||||
* 401 is a dead credential and waiting will not fix it; a 429 or a 503 is the
|
||||
* provider's day and waiting is exactly right. Absent for a network-class
|
||||
* failure, which never had a status to record.
|
||||
*/
|
||||
|
||||
interface ProviderHealthRow {
|
||||
@@ -27,6 +33,7 @@ interface ProviderHealthRow {
|
||||
maxConsecutiveFailures: number;
|
||||
lastOkAt: string | null;
|
||||
lastFailureAt: string | null;
|
||||
lastFailureStatus: number | null;
|
||||
}
|
||||
|
||||
export function ProviderHealthSection() {
|
||||
@@ -106,6 +113,13 @@ export function ProviderHealthSection() {
|
||||
at: formatDateTime(p.lastFailureAt),
|
||||
})}`
|
||||
: ""}
|
||||
{p.failing > 0 &&
|
||||
p.lastFailureAt &&
|
||||
p.lastFailureStatus !== null
|
||||
? ` · ${t("admin.providerHealth.lastFailureStatus", {
|
||||
status: p.lastFailureStatus,
|
||||
})}`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
{p.failing === 0 ? (
|
||||
|
||||
Reference in new issue
Block a user