diff --git a/packages/integrations/src/unifi-os/test/unifi-os-integration.spec.ts b/packages/integrations/src/unifi-os/test/unifi-os-integration.spec.ts index 74587e0dc..85b287f89 100644 --- a/packages/integrations/src/unifi-os/test/unifi-os-integration.spec.ts +++ b/packages/integrations/src/unifi-os/test/unifi-os-integration.spec.ts @@ -153,17 +153,31 @@ describe("UnifiOsIntegration getUnifiOsSummaryAsync", () => { expect(summary.wan.isp?.measuredAt).toEqual(new Date("2026-09-08T09:10:00Z")); }); - test("degrades only the affected block when one local endpoint fails", async () => { + test("degrades only the clients block when the clients endpoint fails", async () => { mockFetch.mockImplementation(async (url) => { const raw = String(url); - if (raw.includes("/vpn/servers")) return jsonResponse({ error: "boom" }, 500); + if (raw.includes("/clients")) return jsonResponse({ error: "boom" }, 500); return routeLocal(raw) ?? jsonResponse({}, 404); }); const summary = await createIntegration(localOnlySecrets).getUnifiOsSummaryAsync(); - expect(summary.clients.total).toBe(2); - expect(summary.devices.total).toBe(2); + expect(summary.clients).toBeNull(); + expect(summary.devices).toEqual({ total: 2, online: 1, offline: 1, firmwareUpdatable: 1 }); + expect(summary.gateway).not.toBeNull(); + }); + + test("degrades only the devices block when the devices endpoint fails", async () => { + mockFetch.mockImplementation(async (url) => { + const raw = String(url); + if (raw.includes("/devices") && !raw.includes("/statistics")) return jsonResponse({ error: "boom" }, 500); + return routeLocal(raw) ?? jsonResponse({}, 404); + }); + + const summary = await createIntegration(localOnlySecrets).getUnifiOsSummaryAsync(); + + expect(summary.devices).toBeNull(); + expect(summary.clients).toEqual({ total: 2, wired: 1, wireless: 1, vpn: 0, guests: 0 }); }); test("keeps clients and devices when the gateway cannot be identified", async () => { @@ -192,6 +206,6 @@ describe("UnifiOsIntegration getUnifiOsSummaryAsync", () => { expect(summary.gateway).toBeNull(); expect(summary.wan.throughput).toBeNull(); expect(summary.wan.state).toBe("unknown"); - expect(summary.clients.total).toBe(2); + expect(summary.clients).toEqual({ total: 2, wired: 1, wireless: 1, vpn: 0, guests: 0 }); }); }); diff --git a/packages/integrations/src/unifi-os/unifi-os-api.ts b/packages/integrations/src/unifi-os/unifi-os-api.ts index 2f63c430e..33eb59c89 100644 --- a/packages/integrations/src/unifi-os/unifi-os-api.ts +++ b/packages/integrations/src/unifi-os/unifi-os-api.ts @@ -11,7 +11,6 @@ import { unifiOsDeviceStatisticsSchema, unifiOsInfoSchema, unifiOsSiteSchema, - unifiOsVpnServerSchema, } from "./unifi-os-types"; export type UnifiOsFetch = (url: string, init?: RequestInit & { timeout?: number }) => Promise; @@ -27,7 +26,6 @@ const PARSE_ERROR_MESSAGE = "Invalid UniFi OS response"; const clientPageSchema = createUnifiOsPageSchema(unifiOsClientSchema); const devicePageSchema = createUnifiOsPageSchema(unifiOsDeviceSchema); const sitePageSchema = createUnifiOsPageSchema(unifiOsSiteSchema); -const vpnServerPageSchema = createUnifiOsPageSchema(unifiOsVpnServerSchema); /** * Client de l'API d'intégration locale d'UniFi OS. @@ -70,11 +68,6 @@ export class UnifiOsApi { ); } - public async getVpnServersAsync(siteId: string): Promise<{ enabled: boolean }[]> { - const page = await this.requestAsync(`/sites/${encodeURIComponent(siteId)}/vpn/servers`, vpnServerPageSchema); - return page.data; - } - /** * Sous le plafond de 200 clients, une seule page suffit et le comptage se fait * en mémoire. Au-delà, rapatrier tous les enregistrements pour n'en tirer que diff --git a/packages/integrations/src/unifi-os/unifi-os-integration.ts b/packages/integrations/src/unifi-os/unifi-os-integration.ts index 5e7b8d857..8ee4ee03c 100644 --- a/packages/integrations/src/unifi-os/unifi-os-integration.ts +++ b/packages/integrations/src/unifi-os/unifi-os-integration.ts @@ -8,16 +8,9 @@ import type { UnifiOsSummaryIntegration } from "../interfaces/unifi-os-summary/u import type { UnifiOsFetch } from "./unifi-os-api"; import { UnifiOsApi } from "./unifi-os-api"; import { UnifiSmApi } from "./unifi-sm-api"; -import type { UnifiOsDevice, UnifiOsDeviceStatistics, UnifiOsSummary } from "./unifi-os-types"; +import type { UnifiOsDeviceStatistics, UnifiOsSummary } from "./unifi-os-types"; import { findGateway, summarizeDevices } from "./unifi-os-types"; -const EMPTY_CLIENT_COUNTS = { total: 0, wired: 0, wireless: 0, vpn: 0, guests: 0 } as const; -const EMPTY_DEVICE_COUNTS = { total: 0, online: 0, offline: 0, firmwareUpdatable: 0 } as const; - -/** `Promise.allSettled` renvoie `rejected` sans valeur ; on retombe sur un défaut. */ -const settledValue = (result: PromiseSettledResult, fallback: T): T => - result.status === "fulfilled" ? result.value : fallback; - export class UnifiOsIntegration extends Integration implements UnifiOsSummaryIntegration { public async getUnifiOsSummaryAsync(): Promise { return await this.buildSummaryAsync(fetchWithTrustedCertificatesAsync as unknown as UnifiOsFetch); @@ -59,8 +52,10 @@ export class UnifiOsIntegration extends Integration implements UnifiOsSummaryInt api.getDevicesAsync(site.id), ]); - const clients = settledValue(clientsResult, { ...EMPTY_CLIENT_COUNTS }); - const devices = settledValue(devicesResult, []); + // `null` marks a failed call so the UI never mistakes it for a real zero + // count (a site with genuinely no clients or devices). + const clients = clientsResult.status === "fulfilled" ? clientsResult.value : null; + const devices = devicesResult.status === "fulfilled" ? devicesResult.value : []; const gatewayDevice = findGateway(devices); // Les statistiques dépendent du gateway : sans gateway identifié, il n'y a @@ -87,7 +82,7 @@ export class UnifiOsIntegration extends Integration implements UnifiOsSummaryInt isp, }, clients, - devices: devicesResult.status === "fulfilled" ? summarizeDevices(devices) : { ...EMPTY_DEVICE_COUNTS }, + devices: devicesResult.status === "fulfilled" ? summarizeDevices(devices) : null, gateway: gatewayDevice === null ? null diff --git a/packages/integrations/src/unifi-os/unifi-os-types.ts b/packages/integrations/src/unifi-os/unifi-os-types.ts index ba179b1ab..9023e20e2 100644 --- a/packages/integrations/src/unifi-os/unifi-os-types.ts +++ b/packages/integrations/src/unifi-os/unifi-os-types.ts @@ -148,8 +148,11 @@ export interface UnifiOsSummary { throughput: { txBps: number; rxBps: number } | null; isp: UnifiOsIspMetrics | null; }; - clients: UnifiOsClientCounts; - devices: UnifiOsDeviceCounts; + /** `null` when the call failed — never a zero count, which the UI could not + * tell apart from a site with genuinely no clients. */ + clients: UnifiOsClientCounts | null; + /** `null` when the call failed, for the same reason. */ + devices: UnifiOsDeviceCounts | null; gateway: UnifiOsGateway | null; }