mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
fix(web): fleet roll-up success rate is terminal-only, matching the cards [Phase 3c follow-up]
Addresses a Codex finding: the per-tenant cards show the backend success rate, which is terminal-only (succeeded / succeeded+failed). The fleet roll-up instead divided by total sessions (which includes active/pending), so the tile and the cards disagreed — e.g. a tenant with 90 succeeded / 10 failed / 900 active shows 90% on its card but the roll-up showed 9%. - computeFleetRollup: success rate now sum(succeeded) / (sum(succeeded)+sum(failed)), excluding active/pending from the denominator — same definition as DashboardStats. - successTone: red emphasis now gates on terminal-session count (not total), so a tenant with only active sessions reads 0% but isn't flagged as failing. - Tests: replaced the total-based weighted test with terminal-only weighting and added an explicit 90/10/900 case locking in 90% (not 9%). Web tsc clean; 481 web tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,16 +37,28 @@ describe("computeFleetRollup", () => {
|
||||
expect(r.failedLastNDays).toBe(2);
|
||||
});
|
||||
|
||||
it("weights the success rate by session volume, not by tenant", () => {
|
||||
// A: 900/1000 = 90%, B: 0/10 = 0%. A naive average would be 45%; the weighted rate is 900/1010 ≈ 89.1%.
|
||||
it("weights the success rate by terminal-session volume, not by tenant", () => {
|
||||
// A: 900 succeeded / 100 failed = 90%, B: 0 succeeded / 10 failed = 0%. A naive average would be 45%;
|
||||
// the weighted terminal rate is 900 / (900 + 100 + 10) = 900/1010 ≈ 89.1%.
|
||||
const r = computeFleetRollup([
|
||||
summary({ totalLastNDays: 1000, succeededLastNDays: 900 }),
|
||||
summary({ totalLastNDays: 10, succeededLastNDays: 0 }),
|
||||
summary({ succeededLastNDays: 900, failedLastNDays: 100, totalLastNDays: 1000 }),
|
||||
summary({ succeededLastNDays: 0, failedLastNDays: 10, totalLastNDays: 10 }),
|
||||
]);
|
||||
expect(r.successRatePct).toBeCloseTo(89.1, 1);
|
||||
});
|
||||
|
||||
it("reports 0% success when the fleet has no sessions", () => {
|
||||
it("excludes active/pending sessions from the success-rate denominator (terminal-only)", () => {
|
||||
// Matches the per-tenant card / backend semantic: 90 succeeded, 10 failed, 900 active. The card shows
|
||||
// 90/(90+10) = 90%; a total-based roll-up would wrongly show 90/1000 = 9%. Lock in terminal-only.
|
||||
const r = computeFleetRollup([
|
||||
summary({ succeededLastNDays: 90, failedLastNDays: 10, activeCount: 900, totalLastNDays: 1000 }),
|
||||
]);
|
||||
expect(r.successRatePct).toBe(90);
|
||||
expect(r.activeCount).toBe(900);
|
||||
expect(r.totalLastNDays).toBe(1000);
|
||||
});
|
||||
|
||||
it("reports 0% success when the fleet has no terminal sessions", () => {
|
||||
const r = computeFleetRollup([summary({ activeCount: 1 }), summary({})]);
|
||||
expect(r.totalLastNDays).toBe(0);
|
||||
expect(r.successRatePct).toBe(0);
|
||||
|
||||
@@ -18,14 +18,20 @@ export interface FleetRollup {
|
||||
totalLastNDays: number;
|
||||
succeededLastNDays: number;
|
||||
failedLastNDays: number;
|
||||
/** Weighted success rate across the fleet (sum succeeded / sum total), 1 decimal. 0 when no sessions. */
|
||||
/**
|
||||
* Weighted fleet success rate, 1 decimal. TERMINAL-ONLY: sum(succeeded) / (sum(succeeded) + sum(failed)) —
|
||||
* matches the per-tenant card / backend DashboardStats semantic (succeeded over terminal sessions), so the
|
||||
* roll-up and the cards never disagree. Active/pending sessions are NOT in the denominator. 0 when the
|
||||
* fleet has no terminal sessions.
|
||||
*/
|
||||
successRatePct: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates per-tenant summaries into a fleet roll-up. The success rate is WEIGHTED by session volume
|
||||
* (sum of succeeded / sum of total) — not an average of per-tenant rates — so a tenant with 1000 sessions
|
||||
* counts more than one with 3. Pure + testable.
|
||||
* Aggregates per-tenant summaries into a fleet roll-up. The success rate is WEIGHTED by terminal-session
|
||||
* volume (sum succeeded / sum terminal) — not an average of per-tenant rates — so a tenant with 1000
|
||||
* terminal sessions counts more than one with 3. Terminal-only matches the backend's per-tenant rate, so a
|
||||
* tenant card and the roll-up tile use the same definition. Pure + testable.
|
||||
*/
|
||||
export function computeFleetRollup(summaries: FleetSummary[]): FleetRollup {
|
||||
const acc = summaries.reduce(
|
||||
@@ -39,10 +45,9 @@ export function computeFleetRollup(summaries: FleetSummary[]): FleetRollup {
|
||||
{ activeCount: 0, totalLastNDays: 0, succeededLastNDays: 0, failedLastNDays: 0 }
|
||||
);
|
||||
|
||||
const terminal = acc.succeededLastNDays + acc.failedLastNDays;
|
||||
const successRatePct =
|
||||
acc.totalLastNDays > 0
|
||||
? Math.round((acc.succeededLastNDays / acc.totalLastNDays) * 1000) / 10
|
||||
: 0;
|
||||
terminal > 0 ? Math.round((acc.succeededLastNDays / terminal) * 1000) / 10 : 0;
|
||||
|
||||
return { tenantCount: summaries.length, ...acc, successRatePct };
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function FleetPage() {
|
||||
<RollupTile label="Tenants" value={String(myTenants.length)} />
|
||||
<RollupTile label="Active sessions" value={String(rollup.activeCount)} />
|
||||
<RollupTile label="Failed" value={String(rollup.failedLastNDays)} tone={rollup.failedLastNDays > 0 ? "danger" : "default"} />
|
||||
<RollupTile label="Success rate" value={`${rollup.successRatePct}%`} tone={successTone(rollup.successRatePct, rollup.totalLastNDays)} />
|
||||
<RollupTile label="Success rate" value={`${rollup.successRatePct}%`} tone={successTone(rollup.successRatePct, rollup.succeededLastNDays + rollup.failedLastNDays)} />
|
||||
</div>
|
||||
|
||||
{myTenants.length === 0 ? (
|
||||
@@ -88,9 +88,13 @@ export default function FleetPage() {
|
||||
|
||||
type Tone = "default" | "danger";
|
||||
|
||||
/** Amber/red emphasis only once we actually have sessions to judge — an empty tenant isn't "failing". */
|
||||
function successTone(pct: number, total: number): Tone {
|
||||
if (total === 0) return "default";
|
||||
/**
|
||||
* Red emphasis only once there are TERMINAL sessions to judge — the success rate is terminal-only
|
||||
* (succeeded / succeeded+failed), so a tenant with only active/pending sessions reads 0% but isn't
|
||||
* "failing"; don't flag it.
|
||||
*/
|
||||
function successTone(pct: number, terminalCount: number): Tone {
|
||||
if (terminalCount === 0) return "default";
|
||||
return pct < 80 ? "danger" : "default";
|
||||
}
|
||||
|
||||
@@ -123,7 +127,7 @@ function FleetCard({ tenant, summary }: { tenant: TenantInfo; summary?: FleetSum
|
||||
<Metric
|
||||
label="Success"
|
||||
value={summary ? `${summary.successRatePct}%` : "—"}
|
||||
danger={!!summary && successTone(summary.successRatePct, summary.totalLastNDays) === "danger"}
|
||||
danger={!!summary && successTone(summary.successRatePct, summary.succeededLastNDays + summary.failedLastNDays) === "danger"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user