docs: spec — add per-site SD-WAN tunnel status

Ajout demandé après validation : l'état des tunnels, indexé par siteId,
se greffe sur les lignes existantes. La topologie reste hors v1 — une
liste de quinze réseaux n'a pas sa place dans une ligne de tableau.

Relevé au passage : ping vaut 0 sur un tunnel connecté entre deux sites
distants, où une latence nulle est physiquement impossible. Le champ
signifie « non mesuré » et le contrat le traduit en null, plutôt que de
propager un zéro que l'interface afficherait comme une performance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
zacharyandClaude Opus 5 committed 2026-10-08 00:24:05 +02:00
1 parent 050b96f1e3
commit 5dd35e48f1
1 file changed
+52 -10
@@ -27,6 +27,7 @@ console, donc un site. Le Site Manager voit tous les sites d'un compte.
| 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, la bande en colonne** | Réutilise `HomarrDataTable`, le composant partagé déjà employé par les widgets beszel, docker et downloads. Tri par colonne gratuit, visibilité responsive documentée par la skill `datatable-migration` du dépôt, et lisible bien au-delà de dix sites. C'est aussi la forme qu'emploie l'interface Ubiquiti. La bande devient une colonne parmi d'autres : elle ne domine plus la lecture, mais elle reste présente et le widget tient à l'échelle. |
| 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. |
| D6 | **État SD-WAN par site, sans la topologie** | Ajouté après coup à la demande de l'utilisateur. L'état des tunnels — connecté, hors ligne, ping, erreurs — est indexé par `siteId` et se greffe donc sur les lignes existantes. La topologie (réseaux nommés, sous-réseaux) reste hors v1 : une liste de quinze réseaux n'a pas sa place dans une ligne de tableau. |
| 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
@@ -52,6 +53,8 @@ Une trentaine de chemins plausibles ont été sondés :
|---|---|
| `GET /v1/sites` | `siteId`, `hostId`, `meta.desc`, `meta.name`, `statistics.percentages.wanUptime`, `statistics.ispInfo.name`, `statistics.wans[*].externalIp`, `statistics.counts.{totalDevice,offlineDevice,pendingUpdateDevice}` |
| `GET /v1/hosts` | `id`, `reportedState.deviceState`, `reportedState.firmwareUpdate` |
| `GET /v1/sd-wan-configs` | `id`, `name` — nécessaire pour atteindre le statut, qui est indexé par configuration |
| `GET /v1/sd-wan-configs/{id}/status` | `status.connections[].sites[]` → `siteId`, `status.{connected,offline,ping,errors}` ; `status.sites[]` → `siteId`, `errors`, `warnings` |
Corrélation site ↔ host par `hostId`, présent des deux côtés. Contrairement à
l'intégration `unifiOs`, aucune ruse par adresse MAC n'est nécessaire ici : la
@@ -107,6 +110,18 @@ export interface UnifiSiteManagerSite {
/** Dernière mesure de la fenêtre. `null` si le cloud n'en rapporte aucune. */
latencyMs: number | null;
packetLossPct: number | null;
/** `null` si le site n'appartient à aucune configuration SD-WAN. */
sdWan: UnifiSiteSdWanStatus | null;
}
export interface UnifiSiteSdWanStatus {
configName: string;
connected: boolean;
offline: boolean;
/** `null` quand le cloud rapporte `0` — voir la note ci-dessous. */
pingMs: number | null;
errorCount: number;
warningCount: number;
}
export type UnifiSiteHealthState = "healthy" | "degraded" | "down" | "unknown";
@@ -137,6 +152,18 @@ 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.
**`ping: 0` ne veut pas dire zéro milliseconde.** Relevé sur un tunnel
`connected: true` reliant deux sites géographiquement distants — une latence
nulle y est physiquement impossible. Le champ signifie donc « non mesuré », et le
contrat le traduit par `pingMs: null` plutôt que de propager un zéro que
l'interface afficherait comme une performance.
**Un site peut appartenir à plusieurs configurations SD-WAN.** Le contrat n'en
porte qu'une : on retient la première configuration où le site apparaît, et le
nom de celle-ci accompagne l'état pour que la ligne reste interprétable. Si le
besoin de plusieurs se présente, c'est un élargissement du contrat, pas un
correctif.
**`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
@@ -208,19 +235,28 @@ const [sites, hosts] = await Promise.allSettled([
if (sites.status === "rejected") throw sites.reason; // pas de liste sans sites
// 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
const [metrics, sdWan] = await Promise.allSettled([
POST /v1/isp-metrics/1h/query, // fenêtre 24 h, tous les sites
GET /v1/sd-wan-configs // puis /{id}/status pour chacune
]);
// Les deux sont des enrichissements : leur échec n'est jamais fatal.
```
Trois appels, deux niveaux de criticité.
Quatre à cinq appels selon le nombre de configurations SD-WAN, deux niveaux de
criticité.
`/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.
`/v1/hosts`, `isp-metrics` et le SD-WAN sont des enrichissements. Leur échec met
leur part à l'inconnu — `consoleUpdateAvailable` à `null`, les vingt-quatre
tranches à `"unknown"`, `sdWan` à `null` — et laisse le reste affiché.
`Promise.allSettled` plutôt que `Promise.all` sert exactement cette asymétrie.
Le SD-WAN coûte deux appels : la liste des configurations, puis le statut de
chacune. Avec une seule configuration — le cas courant — cela fait deux requêtes.
Le plan borne ce nombre pour qu'un compte à dix configurations ne produise pas
onze requêtes par rafraîchissement.
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
@@ -257,6 +293,7 @@ Un compte réellement sans site est un cas distinct et légitime : il affiche un
| Équipements | `deviceCount`, `offlineDeviceCount` | `en ligne / total`, en rouge si des équipements sont hors ligne |
| Mises à jour | `pendingUpdateCount` | en jaune si > 0 ; `0` affiché normalement, c'est une vraie mesure |
| Console | `consoleUpdateAvailable` | `—` si `null` — l'absence de host correspondant n'est pas « à jour » |
| SD-WAN | `sdWan` | `—` si le site n'est dans aucune configuration ; sinon état du tunnel, en rouge si hors ligne, en jaune s'il porte des erreurs. Le ping n'apparaît que s'il est mesuré |
Colonnes triables. Visibilité responsive selon la largeur de tuile, suivant la
convention documentée par la skill `datatable-migration` : les colonnes les moins
@@ -304,6 +341,7 @@ widget `unifiOs`.
| 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, SD-WAN | site absent de toute configuration → `sdWan` à `null` ; **`ping: 0` → `pingMs: null` et non `0`** ; erreurs et avertissements comptés ; site présent dans plusieurs configurations → la première est retenue | 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é |
@@ -334,9 +372,11 @@ passer — dont un qui empêchait l'application de démarrer.
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).
- **Topologie SD-WAN** — l'**état** des tunnels entre dans la v1 (D6) ; la
topologie n'y entre pas. `/v1/sd-wan-configs/{id}` porte les sites, réseaux
nommés, sous-réseaux et l'état `paused` — vérifié complet — mais une liste de
quinze réseaux n'a pas sa place dans une ligne de tableau. Un widget dédié
reste possible et réutilisera le client de cette intégration.
- **Écriture** — l'API expose des mutations ; l'intégration reste en lecture seule.
- **Documentation Docusaurus et traductions au-delà de l'anglais** — fork
personnel, même décision que l'intégration précédente.
@@ -351,6 +391,8 @@ 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 |
| `ping: 0` interprété comme une latence nulle | Justesse | Traduit en `pingMs: null` au contrat, testé. Relevé sur un tunnel connecté entre deux sites distants, où zéro est physiquement impossible |
| Un compte à nombreuses configurations SD-WAN multiplie les requêtes | Fiabilité | Nombre de configurations interrogées borné par le plan ; au-delà, les sites concernés ont `sdWan` à `null` plutôt qu'une rafale de requêtes |
| Le nombre de sites croît | Interface | Le tableau absorbe la croissance là où des cartes ou une liste se dégraderaient ; visibilité responsive des colonnes, `Site` et `Santé` toujours conservées |
| La bande est étroite dans une colonne | Lisibilité | Conséquence directe de D3. L'infobulle par segment n'est donc pas un confort mais le moyen principal de lire une tranche |
| 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 |