docs: spec — merge ISP metrics into the site health list
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
1c602a305b
commit
a3b7ca9a8c
1 file changed
+106
-43
@@ -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 |
|
||||
Reference in new issue
Block a user