fix(autopilot): show real suggestion count and link dashboard (#11102)

Validated on the combined batch board over release/v3.8.50 tip d91238b7: static gates clean, typecheck:core clean, focused tests green.

Real suggestionCount replaces the conflated link count (deprecated alias kept), dashboard deep-link fixed. Thank you @maxmad64bis!
This commit is contained in:
Dizzle
2026-08-22 19:39:22 +02:00
committed by GitHub
parent efc7134167
commit 78b4082361
5 changed files with 128 additions and 6 deletions

View File

@@ -0,0 +1 @@
- **Autopilot suggestion counter:** the combo health autopilot summary now reports `suggestionCount` (the real number of suggested actions across all issues) instead of conflating it with link counts, while keeping `actionableCount` as a deprecated alias for backward compatibility. The `run_combo_test` action now links to the dashboard with the combo id (`/dashboard/combos?test=<comboId>`) rather than the read-only API route, so operators can actually trigger a test from the UI ([#11102](https://github.com/diegosouzapw/OmniRoute/pull/11102)).

View File

@@ -291,7 +291,9 @@ function ComboAutopilotPanel({ report }: { report: ComboAutopilotReport }) {
icon="monitor_heart"
label={t("comboHealthIssues")}
value={report.summary.issueCount.toLocaleString()}
subValue={t("comboHealthActionable", { count: report.summary.actionableCount })}
subValue={t("comboHealthActionable", {
count: report.summary.suggestionCount ?? report.summary.actionableCount ?? 0,
})}
/>
<MetricBlock
icon="error"

View File

@@ -16,6 +16,7 @@ import type {
ComboForecastMetrics,
ComboForecastResponse,
ComboForecastRiskLevel,
ProviderAutopilotReport,
ComboHealthMetrics,
ComboHealthResponse,
ComboRecord,
@@ -34,6 +35,7 @@ export interface ComboHealthAutopilotOptions {
combos?: ComboRecord[];
healthResponse?: ComboHealthResponse;
forecastResponse?: ComboForecastResponse;
providerHealthResponse?: ProviderAutopilotReport;
}
type ProviderIssueView = {
@@ -103,7 +105,12 @@ function actionSet(
case "open_combo_editor":
return action(type, "Open combo editor", target, "/dashboard/combos");
case "run_combo_test":
return action(type, "Run combo test", target, "/dashboard/combos");
return action(
type,
"Run combo test",
target,
`/dashboard/combos?test=${encodeURIComponent(target.comboId)}`
);
case "open_provider_health_autopilot":
return action(type, "Open provider autopilot", target, "/dashboard/health");
case "review_quota_limits":
@@ -447,7 +454,8 @@ export async function buildComboHealthAutopilotReport(
now: options.now,
combos: combosSnapshot,
}),
buildProviderHealthAutopilotReport({ includeHealthy: false, includeActions: false }),
options.providerHealthResponse ??
buildProviderHealthAutopilotReport({ includeHealthy: false, includeActions: false }),
]);
const forecastsByComboId = new Map(forecast.combos.map((entry) => [entry.comboId, entry]));
@@ -470,7 +478,7 @@ export async function buildComboHealthAutopilotReport(
const degradedCount = allCombos.filter((combo) => combo.state === "degraded").length;
const healthyCount = allCombos.filter((combo) => combo.state === "healthy").length;
const issueCount = allCombos.reduce((sum, combo) => sum + combo.issues.length, 0);
const actionableCount = allCombos.reduce(
const suggestionCount = allCombos.reduce(
(sum, combo) =>
sum + combo.issues.reduce((issueSum, issue) => issueSum + issue.actions.length, 0),
0
@@ -487,7 +495,8 @@ export async function buildComboHealthAutopilotReport(
degradedCount,
downCount,
issueCount,
actionableCount,
suggestionCount,
actionableCount: suggestionCount,
},
combos,
};

View File

@@ -260,7 +260,9 @@ export interface ComboAutopilotReport {
degradedCount: number;
downCount: number;
issueCount: number;
actionableCount: number;
suggestionCount: number;
/** @deprecated Use suggestionCount instead. Kept as an alias for backward compatibility; remove after 2 releases. */
actionableCount?: number;
};
combos: ComboAutopilotCombo[];
}

View File

@@ -0,0 +1,108 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import type {
ComboForecastResponse,
ComboHealthResponse,
ProviderAutopilotReport,
} from "../../src/shared/types/utilization.ts";
import { buildComboHealthAutopilotReport } from "../../src/lib/monitoring/comboHealthAutopilot.ts";
function healthResponse(): ComboHealthResponse {
return {
timeRange: "24h",
combos: [
{
comboId: "c1",
comboName: "my-combo",
strategy: "fallback",
models: [],
targetHealth: [
{
executionKey: "e1",
stepId: "s1",
model: "m",
provider: "p",
connectionId: null,
label: null,
requests: 5,
successRate: 90,
avgLatencyMs: 100,
lastStatus: "error",
lastUsedAt: null,
quotaRemainingPct: 50,
quotaIsExhausted: false,
quotaTrend: "stable",
quotaScope: "provider",
},
],
quotaHealth: { providers: [], worstRemainingPct: 100 },
usageSkew: { modelDistribution: [], giniCoefficient: 0 },
performance: { avgLatencyMs: 100, successRate: 1.0, totalRequests: 10 },
},
],
};
}
function forecastResponse(): ComboForecastResponse {
return {
timeRange: "24h",
horizon: "30d",
asOf: new Date(0).toISOString(),
method: "linear_history",
combos: [],
};
}
function providerHealthResponse(): ProviderAutopilotReport {
return { providers: [] } as unknown as ProviderAutopilotReport;
}
function buildOptions() {
return {
range: "24h" as const,
horizon: "30d" as const,
healthResponse: healthResponse(),
forecastResponse: forecastResponse(),
providerHealthResponse: providerHealthResponse(),
};
}
describe("combo health autopilot counter", () => {
it("exposes suggestionCount and keeps actionableCount alias", async () => {
const report = await buildComboHealthAutopilotReport(buildOptions());
assert.equal(typeof report.summary.suggestionCount, "number");
assert.equal(report.summary.actionableCount, report.summary.suggestionCount);
const expected = report.combos.reduce(
(sum, combo) =>
sum + combo.issues.reduce((issueSum, issue) => issueSum + issue.actions.length, 0),
0
);
assert.equal(report.summary.suggestionCount, expected);
});
it("run_combo_test action links the dashboard with the combo id", async () => {
const report = await buildComboHealthAutopilotReport(buildOptions());
const actions = report.combos.flatMap((combo) => combo.issues.flatMap((i) => i.actions));
const runTest = actions.find((a) => a.type === "run_combo_test");
assert.ok(runTest, "run_combo_test action should exist");
assert.equal(typeof runTest.href, "string");
assert.ok(runTest.href?.includes("c1"), "href must carry the combo id");
assert.equal(
runTest.href?.includes("/api/combos/test?comboId="),
false,
"href must not target the GET-only API route (405)"
);
});
it("keeps every action in manual mode", async () => {
const report = await buildComboHealthAutopilotReport(buildOptions());
for (const combo of report.combos) {
for (const issue of combo.issues) {
for (const action of issue.actions) {
assert.equal(action.mode, "manual");
}
}
}
});
});