Skip to content

Commit 3dab818

Browse files
committed
fix(admin): cache an aggregate health entry per integration
Integrations whose `manifest.healthCheck` is an array (e.g. fuel runs DE / AT / FR / ES probes; several overlay-* and transit-* declare multiple) wrote per-sub-check results to the cache under composite ids like `fuel:Tankerkoenig (DE)`, never under the bare integration id. The /admin/integrations list looks up the bare id via getCachedHealthStatus(integration.id) and got back `undefined`, so IntegrationStatusDot's `hasHealthCheck && health === null` branch fired and rendered orange "Unconfigured" even after a manual sweep. In executeAllIntegrationHealthChecks, after running the per-sub-check probes for a multi-check integration, synthesize an aggregate ServiceStatus under the bare integration id with this rollup: any sub-check `down` → bare id `down` else any sub-check `up` → bare id `up` else (all `unconfigured`) → bare id `unconfigured` Single-check integrations are unaffected — their existing single result already lives under the bare id. The per-integration GET /api/admin/integrations/:id/health response still returns the raw sub-check array; only the cache lookup at the bare id changes.
1 parent e7198c2 commit 3dab818

1 file changed

Lines changed: 60 additions & 1 deletion

File tree

apps/api/src/services/integration-health.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,23 @@ export async function executeAllIntegrationHealthChecks(
245245
integrations: LoadedIntegration[],
246246
): Promise<ServiceStatus[]> {
247247
const checks = integrations.filter((i) => i.manifest.healthCheck);
248-
const results = await Promise.all(checks.map(executeIntegrationHealthCheck));
248+
const results = await Promise.all(
249+
checks.map(async (integration) => {
250+
const subResults = await executeIntegrationHealthCheck(integration);
251+
// Multi-check integrations (e.g. fuel with 4 country sub-checks) write
252+
// entries under composite ids `<integration>:<sub>`, never under the
253+
// bare id. The integration-list endpoint looks up the bare id, gets
254+
// undefined, and the status dot renders orange "Unconfigured" even
255+
// when the underlying probes succeed. Synthesize an aggregate for
256+
// those integrations so callers that just want a single yes/no
257+
// signal can rely on the bare id too.
258+
if (subResults.length > 1 && subResults.every((r) => r.id !== integration.id)) {
259+
const aggregate = aggregateHealth(integration.id, subResults);
260+
if (aggregate) subResults.push(aggregate);
261+
}
262+
return subResults;
263+
}),
264+
);
249265
const filtered = results.flat();
250266

251267
// Update the shared health cache and persist to health_history
@@ -263,6 +279,49 @@ export async function executeAllIntegrationHealthChecks(
263279
return filtered;
264280
}
265281

282+
/**
283+
* Combine per-sub-check results into a single integration-level signal.
284+
* Priority: any down → down; else any up → up; else unconfigured. Returns
285+
* `null` if there are no sub-checks to aggregate. Only used for the cache
286+
* lookup by bare integration id; the per-integration GET endpoint still
287+
* returns the raw array.
288+
*/
289+
function aggregateHealth(integrationId: string, subResults: ServiceStatus[]): ServiceStatus | null {
290+
if (subResults.length === 0) return null;
291+
const anyDown = subResults.find((r) => r.status === "down");
292+
const anyUp = subResults.find((r) => r.status === "up");
293+
if (anyDown) {
294+
return {
295+
id: integrationId,
296+
name: anyDown.name.split(" — ")[0] ?? anyDown.name,
297+
category: anyDown.category,
298+
url: "",
299+
status: "down",
300+
error: anyDown.error,
301+
};
302+
}
303+
if (anyUp) {
304+
return {
305+
id: integrationId,
306+
name: anyUp.name.split(" — ")[0] ?? anyUp.name,
307+
category: anyUp.category,
308+
url: "",
309+
status: "up",
310+
responseTime: anyUp.responseTime,
311+
};
312+
}
313+
// All sub-checks are unconfigured.
314+
const first = subResults[0];
315+
if (!first) return null;
316+
return {
317+
id: integrationId,
318+
name: first.name.split(" — ")[0] ?? first.name,
319+
category: first.category,
320+
url: "",
321+
status: "unconfigured",
322+
};
323+
}
324+
266325
// Shared health cache: latest health status per integration ID
267326
const healthCache = new Map<string, ServiceStatus>();
268327
let healthCacheUpdatedAt = 0;

0 commit comments

Comments
 (0)