From a3b7ca9a8cd56c6c9402fc29500cdd8c5a75aab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Wed, 9 Sep 2026 23:02:41 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20spec=20=E2=80=94=20merge=20ISP=20metric?= =?UTF-8?q?s=20into=20the=20site=20health=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proposition de l'utilisateur, retenue : fusionner les métriques ISP avec la barre « Internet Health » pour une liste d'état rapide des sites. Elle inverse la décision D4. La barre avait été écartée au motif que `internetIssues` est un tableau creux dont il faudrait reconstituer les périodes saines — vrai de cette source, mais `isp-metrics` fournit la même information déjà dense, une entrée par site, vérifié. La barre coûte alors une boucle de coloration, pas un moteur de graphe. D3 change aussi : liste d'état plutôt que tableau, la bande au centre plutôt qu'en colonne. HomarrDataTable n'a donc plus lieu d'être, et le tri disparaît. Limite assumée et consignée : au-delà de cinq ou six sites la forme tableau redeviendrait préférable. Point de justesse ajouté au contrat : une tranche horaire sans mesure vaut "unknown" et jamais "healthy". Un site enregistré depuis douze heures n'a que douze tranches sur vingt-quatre ; les peindre en vert affirmerait un bon fonctionnement jamais mesuré. Co-Authored-By: Claude Opus 5 --- ...9-unifi-site-manager-integration-design.md | 149 +++++++++++++----- 1 file changed, 106 insertions(+), 43 deletions(-) diff --git a/docs/superpowers/specs/2026-09-09-unifi-site-manager-integration-design.md b/docs/superpowers/specs/2026-09-09-unifi-site-manager-integration-design.md index 0d4455cc4..92e78cf80 100644 --- a/docs/superpowers/specs/2026-09-09-unifi-site-manager-integration-design.md +++ b/docs/superpowers/specs/2026-09-09-unifi-site-manager-integration-design.md @@ -25,8 +25,8 @@ console, donc un site. Le Site Manager voit tous les sites d'un compte. |---|---|---| | D1 | **Deux intégrations indépendantes** | `unifiOs` garde sa clé cloud optionnelle pour son bloc ISP ; `unifiSiteManager` a la sienne. Chacune reste installable et supprimable seule. Coût accepté : la clé est saisie deux fois si l'utilisateur emploie les deux, et deux appelants interrogent le cloud. | | D2 | **Vue d'ensemble des sites d'abord** | Les captures fournies montrent trois vues — liste des sites, métriques ISP historisées, topologie SD-WAN. Ce sont trois projets. Celui-ci construit aussi le socle (intégration, client, routeur) dont les deux autres dépendront. | -| D3 | **Tableau, pas cartes** | Réutilise `HomarrDataTable`, le composant partagé déjà employé par les widgets beszel, docker et downloads. Tri gratuit, visibilité responsive documentée par la skill `datatable-migration` du dépôt, et lisible au-delà de trois sites. | -| D4 | **Pas de barre « Internet Health » en v1** | Les autres colonnes sont des valeurs directes ; celle-ci est un mini-graphe déguisé en colonne (voir §3.3). Reportée, pas abandonnée. | +| D3 | **Liste d'état, pas tableau** | La bande de santé est l'élément qu'on lit en premier ; un tableau la réduirait à une colonne parmi d'autres. Une ligne par site : nom, bande pleine largeur, puis une ligne de chiffres discrète. Coût assumé : pas de tri, et la lisibilité se dégrade au-delà de cinq ou six sites (§12). | +| D4 | **La bande « Internet Health » EST dans la v1** | Décision inversée en cours de conception. Elle avait été écartée au motif que `internetIssues` est un tableau creux dont il faudrait reconstituer les périodes saines. C'est vrai de cette source — mais `isp-metrics` fournit la même information **déjà dense** (§3.3). La bande coûte alors une boucle de coloration, pas un moteur de graphe. | | D5 | **Rafraîchissement 5 min** | Les compteurs cloud n'évoluent pas plus vite. À contraster avec `unifiOs`, dont la source locale a justifié 5 s. | ## 3. Constatations vérifiées sur l'API @@ -57,21 +57,31 @@ Corrélation site ↔ host par `hostId`, présent des deux côtés. Contrairemen l'intégration `unifiOs`, aucune ruse par adresse MAC n'est nécessaire ici : la clé de jointure est explicite. -### 3.3 Ce que la v1 ne consomme pas, et pourquoi +### 3.3 Deux sources pour la santé, une seule utilisable -`statistics.internetIssues` alimente la barre « Internet Health » de l'interface -Ubiquiti. Sa forme a été décodée : un tableau **creux** d'anomalies, chacune -portant un `index` de tranche de 5 minutes, et soit `highLatency` avec +L'interface Ubiquiti affiche une barre « Internet Health ». Deux champs peuvent +la produire, et le choix entre eux décide du coût de la fonctionnalité. + +**`statistics.internetIssues` — écarté.** Un tableau **creux** d'anomalies, +chacune portant un `index` de tranche de 5 min, et soit `highLatency` avec `latencyAvgMs`/`latencyMaxMs`, soit `notReported` avec un `count`. L'index se -convertit en horodatage par `index × 300` secondes — vérifié. +convertit par `index × 300` secondes — vérifié. Mais les périodes **saines** n'y +sont pas : il faudrait les reconstituer par différence avec la fenêtre. -Rendre la barre suppose donc : décoder les index, **reconstituer les périodes -saines** entre les anomalies (elles ne sont pas dans la réponse), et dessiner une -frise dans quelques dizaines de pixels. C'est un graphe, pas une colonne. Reporté -par D4. +**`GET /v1/isp-metrics/{5m|1h}` — retenu.** Vérifié : renvoie **une entrée par +site** (`hostId`, `siteId`) avec une série **dense**, chaque période portant +`downtime`, `packetLoss`, `avgLatency`, `maxLatency`, `uptime` et `ispName`. +Granularité `1h` : 30 jours d'historique disponible. Granularité `5m` : environ +24 h. -Également hors v1 : `/v1/isp-metrics` (projet 2) et `/v1/sd-wan-configs` -(projet 3), dont les données ont été vérifiées disponibles et complètes. +La v1 consomme donc `isp-metrics` en `1h` sur une fenêtre de 24 h, soit +**24 segments par site**. Rien à reconstituer, rien à décoder. + +**Taille.** Le `GET` non borné pèse 216 Ko en `1h` et 133 Ko en `5m`. La v1 passe +par `POST /v1/isp-metrics/1h/query`, qui accepte plusieurs sites et une fenêtre +bornée dans une seule requête. + +Reste hors v1 : `/v1/sd-wan-configs` (projet 3), données vérifiées complètes. ## 4. Contrat de données @@ -91,6 +101,20 @@ export interface UnifiSiteManagerSite { pendingUpdateCount: number; /** Mise à jour de la CONSOLE. `null` si aucun host ne correspond au site. */ consoleUpdateAvailable: boolean | null; + /** 24 tranches horaires, la plus ancienne en premier. Toujours 24 entrées : + * une tranche sans mesure vaut `"unknown"`, jamais `"healthy"`. */ + health: UnifiSiteHealthSegment[]; + /** Dernière mesure de la fenêtre. `null` si le cloud n'en rapporte aucune. */ + latencyMs: number | null; + packetLossPct: number | null; +} + +export type UnifiSiteHealthState = "healthy" | "degraded" | "down" | "unknown"; + +export interface UnifiSiteHealthSegment { + /** Début de la tranche horaire. */ + at: Date; + state: UnifiSiteHealthState; } export interface UnifiSiteManagerOverview { @@ -103,10 +127,20 @@ signifie « aucun host ne correspond à ce site », ce qui n'est pas la même ch que `false` (« la console est à jour »). Le widget doit les distinguer, comme il distingue partout ailleurs l'inconnu du connu. +**Dérivation de `state`, par ordre de gravité décroissante :** `downtime > 0` +→ `"down"` ; sinon `packetLoss > 0` → `"degraded"` ; sinon `"healthy"`. Une +tranche de la fenêtre pour laquelle le cloud ne rapporte rien → `"unknown"`. + +**Cette dernière règle est le point délicat.** Un site enregistré depuis douze +heures n'a que douze tranches sur vingt-quatre. Les remplir en vert affirmerait +douze heures de bon fonctionnement qui n'ont jamais été mesurées. Le gris dit +« on ne sait pas », ce qui est la vérité — même principe que le tiret cadratin +ailleurs dans ces widgets. + **`pendingUpdateCount` et `consoleUpdateAvailable` ne mesurent pas la même chose.** Le premier compte les équipements réseau dont le firmware est en retard ; le second dit si UniFi OS lui-même a une version disponible. L'interface -Ubiquiti les affiche en deux colonnes séparées ; le contrat fait de même. +L'interface Ubiquiti les sépare ; le contrat fait de même. ## 5. Architecture @@ -171,19 +205,26 @@ const [sites, hosts] = await Promise.allSettled([ GET /v1/sites, // source obligatoire GET /v1/hosts, // enrichissement ]); +if (sites.status === "rejected") throw sites.reason; // pas de liste sans sites -if (sites.status === "rejected") throw sites.reason; // pas de tableau sans sites -// hosts rejeté → consoleUpdateAvailable vaut null partout, le tableau s'affiche +// Séquentiel : la requête bornée exige les hostId/siteId de l'étape précédente. +const metrics = await POST /v1/isp-metrics/1h/query // fenêtre 24 h, tous les sites + .catch(() => null); // enrichissement, jamais fatal ``` -Les deux appels partent en parallèle, mais leurs échecs ne se valent pas. -`/v1/sites` porte le tableau : son échec doit remonter, sinon le widget -afficherait « aucun site » alors qu'il n'a rien pu lire. `/v1/hosts` ne porte -qu'une colonne : son échec la met à `null` — inconnue, et non « à jour » — et -laisse le reste intact. +Trois appels, deux niveaux de criticité. -`Promise.allSettled` plutôt que `Promise.all` précisément pour rendre cette -asymétrie possible : `all` ferait échouer le tout sur l'échec de l'enrichissement. +`/v1/sites` porte la liste : son échec doit remonter, sinon le widget afficherait +« aucun site » alors qu'il n'a rien pu lire. + +`/v1/hosts` et `isp-metrics` sont des enrichissements. Leur échec met leur part à +l'inconnu — `consoleUpdateAvailable` à `null`, les vingt-quatre tranches à +`"unknown"` — et laisse le reste affiché. `Promise.allSettled` plutôt que +`Promise.all` sert exactement cette asymétrie. + +L'appel aux métriques est **séquentiel** et non parallèle : la requête bornée a +besoin des identifiants renvoyés par `/v1/sites`. C'est le prix de ne pas +rapatrier les 216 Ko de la variante non bornée. ## 7. Cache et gestion d'erreurs @@ -196,7 +237,7 @@ asymétrie possible : `all` ferait échouer le tout sur l'échec de l'enrichisse **Différence de fond avec `unifiOs`.** Là-bas, le cloud était un enrichissement optionnel : son indisponibilité vidait un bloc et le widget continuait. Ici le cloud **est** la source. Un échec est un vrai échec, et le widget doit afficher -une erreur — jamais un tableau vide, qui se lirait comme « aucun site », c'est-à-dire +une erreur — jamais une liste vide, qui se lirait comme « aucun site », c'est-à-dire comme une information alors qu'il n'y en a aucune. Un compte réellement sans site est un cas distinct et légitime : il affiche un @@ -204,30 +245,48 @@ Un compte réellement sans site est un cas distinct et légitime : il affiche un ## 8. Widget -`HomarrDataTable`, une ligne par site : +Une liste, pas un tableau. Une entrée par site : -| Colonne | Source | Note | -|---|---|---| -| Site | `name` | toujours visible | -| Uptime | `wanUptimePct` | `—` si `null` | -| ISP | `ispName` | `—` si `null` | -| IP publique | `externalIps` | joint par ` · ` ; `—` si vide | -| Équipements | `deviceCount`, `offlineDeviceCount` | `en ligne / total`, en rouge si des équipements sont hors ligne | -| Mises à jour | `pendingUpdateCount` | en jaune si > 0, masqué si 0 | -| Console | `consoleUpdateAvailable` | `—` si `null` — l'absence de host correspondant n'est pas « à jour » | +``` +● Nom du site 11 ms · 0 % + ████████████░░░░████████████████████████████████ + Nom FAI · 203.0.113.10 6/6 équipements · 1 maj · console à jour +``` -Colonnes triables. Visibilité responsive selon la largeur de tuile, suivant la -convention documentée par la skill `datatable-migration` du dépôt : les colonnes -les moins essentielles disparaissent en premier, la colonne Site jamais. +- **Pastille d'état** en tête : couleur de la tranche la plus récente. +- **Bande de santé** pleine largeur, vingt-quatre segments d'une heure, du plus + ancien au plus récent. Vert sain, orange dégradé, rouge coupé, gris inconnu. + Segments jointifs avec un filet de séparation, pour qu'on distingue deux + tranches consécutives de même couleur. +- **Latence et perte** à droite du nom, en encre atténuée. `—` si aucune mesure. +- **Ligne de détail** discrète : FAI, IP publique, équipements en ligne / total, + mises à jour d'équipements en attente, état de mise à jour de la console. -Badge de fraîcheur en tête, comme sur le widget `unifiOs`, alimenté par -`updatedAt` et `useIntegrationConnected`. +Chaque valeur inconnue s'affiche `—`, jamais zéro ni « à jour ». + +**Couleurs.** À valider par le validateur de palette avant écriture, en thème +clair et sombre, comme pour le graphe de débit. Les quatre états doivent rester +distinguables en vision déficiente : la bande n'a pas d'autre encodage que la +couleur, ce qui en fait le cas le plus exigeant du projet. Si un jeu à quatre +états ne passe pas, l'infobulle par segment devient obligatoire, pas optionnelle. + +**Infobulle par segment** : heure de la tranche, latence moyenne, perte. C'est +aussi ce qui rend la bande lisible sans distinguer les couleurs. + +**Badge de fraîcheur** en tête de widget, alimenté par `updatedAt`, comme sur le +widget `unifiOs`. + +**Limite assumée.** La forme liste se dégrade au-delà de cinq ou six sites : la +tuile devient un long défilement. C'est le prix de mettre la santé au centre +plutôt que dans une colonne, et c'est un choix explicite (D3). Au-delà, la forme +tableau redeviendrait préférable. ## 9. Tests | Niveau | Couvre | Réseau | |---|---|---| | Unitaire, mappers | mapping d'un site complet ; site sans WAN ; site sans `hostId` ; host absent → `consoleUpdateAvailable` à `null` et non `false` ; compte sans aucun site | Non | +| Unitaire, santé | dérivation `down` > `degraded` > `healthy` ; **fenêtre incomplète → tranches `"unknown"`, jamais `"healthy"`** ; fenêtre toujours longue de 24 ; métriques absentes → 24 tranches `"unknown"` | Non | | Unitaire, garde | **`unifi-site-manager-types.ts` n'importe pas `@homarr/common/server`** — la seule forme de test qui aurait attrapé le défaut de l'intégration précédente | Non | | Intégration | séquence complète sur fixtures anonymisées ; `/v1/hosts` en échec laisse le tableau affiché ; `/v1/sites` en échec propage l'erreur | Mocké | | Manuel | vérification contre le compte réel, hors CI | Oui | @@ -252,9 +311,11 @@ passer — dont un qui empêchait l'application de démarrer. ## 11. Hors périmètre -- **Barre « Internet Health »** — décodée (§3.3), reportée par D4. -- **Métriques ISP historisées** — projet 2. Données vérifiées disponibles via - `/v1/isp-metrics/{5m|1h}` et son endpoint `query`. +- **Vue d'analyse ISP** — projet 2. La v1 consomme déjà `isp-metrics` pour la + bande de santé ; ce qui reste hors périmètre est l'**outil d'analyse** de la + seconde capture : sélecteur de fenêtre 1D à 1M, courbes de latence superposées + par site, filtres par ISP, par perte et par uptime. Un résumé et un instrument + d'analyse ne sont pas deux tailles du même widget. - **Topologie SD-WAN** — projet 3. Données vérifiées complètes via `/v1/sd-wan-configs/{id}` (sites, réseaux nommés, sous-réseaux, état `paused`) et `/{id}/status` (connectivité par tunnel, ping, erreurs). @@ -272,4 +333,6 @@ passer — dont un qui empêchait l'application de démarrer. | La clé cloud voit tous les sites du compte, y compris non possédés | Sécurité | Assumé et **voulu** ici : c'est une vue multi-sites. Contrairement à `unifiOs`, aucun filtrage n'est souhaitable — mais la documentation du widget doit dire ce qu'il expose | | Limitation de débit sur `api.ui.com` | Fiabilité | Cadence 5 min, deux appels par cycle | | Deux intégrations interrogent le cloud en parallèle | Fiabilité | Conséquence acceptée de D1 ; caches indépendants, ~24 requêtes/heure au total | -| Le nombre de sites croît | Interface | Un tableau tient mieux que des cartes ; visibilité responsive des colonnes | +| Le nombre de sites croît | Interface | Limite connue et assumée (D3) : au-delà de cinq ou six sites la liste devient un long défilement. Le basculement vers une forme tableau est le remède, et reste possible sans toucher au contrat | +| Quatre états de santé distingués par la seule couleur | Accessibilité | Palette validée avant écriture ; infobulle par segment rendue obligatoire si le jeu à quatre états ne passe pas les contrôles de vision déficiente | +| Une tranche sans mesure affichée comme saine | Justesse | État `"unknown"` explicite au contrat, testé ; la fenêtre porte toujours 24 entrées |