diff --git a/src/Web/autopilot-monitor-web/app/fleet/lib/__tests__/fleetRollup.test.ts b/src/Web/autopilot-monitor-web/app/fleet/lib/__tests__/fleetRollup.test.ts
index 8dfa7565..ace8a4d7 100644
--- a/src/Web/autopilot-monitor-web/app/fleet/lib/__tests__/fleetRollup.test.ts
+++ b/src/Web/autopilot-monitor-web/app/fleet/lib/__tests__/fleetRollup.test.ts
@@ -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);
diff --git a/src/Web/autopilot-monitor-web/app/fleet/lib/fleetRollup.ts b/src/Web/autopilot-monitor-web/app/fleet/lib/fleetRollup.ts
index ac3bf61b..50dee9d7 100644
--- a/src/Web/autopilot-monitor-web/app/fleet/lib/fleetRollup.ts
+++ b/src/Web/autopilot-monitor-web/app/fleet/lib/fleetRollup.ts
@@ -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 };
}
diff --git a/src/Web/autopilot-monitor-web/app/fleet/page.tsx b/src/Web/autopilot-monitor-web/app/fleet/page.tsx
index 10f05bd0..dbedb707 100644
--- a/src/Web/autopilot-monitor-web/app/fleet/page.tsx
+++ b/src/Web/autopilot-monitor-web/app/fleet/page.tsx
@@ -68,7 +68,7 @@ export default function FleetPage() {
0 ? "danger" : "default"} />
-
+
{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