docs: design for UniFi OS integration
Conception validée pour une intégration UniFi OS en complément de unifiController, basée sur l'API d'intégration officielle (clé X-API-KEY) plutôt que sur l'ancienne API du Network Controller. Toutes les capacités et limites de l'API ont été mesurées sur une console réelle en 10.6.102, pas déduites de la documentation publiée (qui s'arrête à 10.4.57 et est déjà en retard sur les consoles). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7859faaa1d
commit
c47be12e69
1 file changed
+459
@@ -0,0 +1,459 @@
|
||||
# Intégration UniFi OS pour Homarr — conception
|
||||
|
||||
**Date :** 2026-09-08
|
||||
**Base :** homarr-labs/homarr `2b1a03849` (v1.77.0)
|
||||
**Branche :** `feat/unifi-os-integration`
|
||||
**Statut :** conception validée, prête pour le plan d'implémentation
|
||||
|
||||
> ⚠️ Aucune clé d'API ne figure dans ce document. Les clés utilisées pendant la
|
||||
> conception ont été fournies hors dépôt et doivent être révoquées en fin de projet.
|
||||
|
||||
---
|
||||
|
||||
## 1. Objectif
|
||||
|
||||
Ajouter à Homarr une intégration **UniFi OS**, en complément — et non en
|
||||
remplacement — de l'intégration `unifiController` existante.
|
||||
|
||||
L'intégration existante s'authentifie par identifiant/mot de passe et lit
|
||||
`/api/s/{site}/stat/sites`, l'API interne de l'ancien Network Controller.
|
||||
UniFi OS expose depuis la version 9 une **API d'intégration officielle**,
|
||||
authentifiée par clé d'API, stable et documentée. C'est celle que cible ce
|
||||
projet.
|
||||
|
||||
L'intégration existante n'est pas touchée. Les deux coexistent.
|
||||
|
||||
## 2. Décisions cadrées
|
||||
|
||||
| # | Décision | Justification |
|
||||
|---|---|---|
|
||||
| D1 | **Fork personnel**, pas de PR upstream | Choix de l'auteur. L'issue upstream [#4703](https://github.com/homarr-labs/homarr/issues/4703) demandait cette fonctionnalité et a été fermée par triage automatique, pas par refus technique : la porte reste ouverte pour plus tard. |
|
||||
| D2 | **Hybride local + cloud optionnel** | L'API locale ne fournit ni latence ni perte de paquets. L'API cloud Site Manager les fournit. Le cloud reste facultatif pour que l'intégration fonctionne sans compte UI ni accès Internet. |
|
||||
| D3 | **Widget dédié**, pas de réutilisation des widgets `networkController*` | Le contrat `NetworkControllerSummary` est taillé pour l'ancienne API. S'y conformer obligerait à inventer des valeurs. Un contrat propre évite ça. |
|
||||
| D4 | **Déploiement par rsync du build standalone** | Le LXC exécute déjà un build standalone piloté par systemd. C'est le changement le plus petit possible, donc le plus facile à annuler. |
|
||||
| D5 | **Structure client / mapping / orchestration séparés** | Modèle TrueNAS et Synology déjà présents dans le dépôt. Isole le cloud, optionnel et lent, de la partie locale, obligatoire et rapide. |
|
||||
|
||||
**Conséquence de D3 :** on ne modifie pas `NetworkControllerSummary`. Les champs
|
||||
`latency` / `ping` restent `number` obligatoires pour `unifiController` et
|
||||
`opnsense`. **Aucune régression possible sur les intégrations existantes.**
|
||||
|
||||
## 3. Constatations vérifiées sur l'API
|
||||
|
||||
Toutes les valeurs ci-dessous ont été relevées sur une console réelle
|
||||
(UniFi Network **10.6.102**) le 2026-09-08, et non déduites de la documentation.
|
||||
|
||||
### 3.1 API locale — `{url}/proxy/network/integration/v1`
|
||||
|
||||
Authentification : en-tête `X-API-KEY`, clé générée **dans l'application Network**
|
||||
(Paramètres → Control Plane → Integrations). Une clé du Site Manager cloud est
|
||||
refusée avec un `401` sur cette API — les deux systèmes de clés sont distincts.
|
||||
|
||||
| Endpoint | Renvoie | Relevé |
|
||||
|---|---|---|
|
||||
| `GET /info` | `applicationVersion` | `10.6.102` |
|
||||
| `GET /sites` | `id`, `internalReference`, `name` — **aucune statistique** | 1 site |
|
||||
| `GET /sites/{id}/clients` | `type`, `access.type`, `name`, `ipAddress`, `macAddress`, `uplinkDeviceId`, `connectedAt` | 51 clients |
|
||||
| — | **`limit` est plafonné à 200.** Une demande à 1000 renvoie `limit: 200`. | mesuré |
|
||||
| `GET /sites/{id}/devices` | `state`, `model`, `macAddress`, `firmwareVersion`, `firmwareUpdatable`, `features` | 6 équipements |
|
||||
| `GET /sites/{id}/devices/{id}/statistics/latest` | `uptimeSec`, `cpuUtilizationPct`, `memoryUtilizationPct`, `uplink.{tx,rx}RateBps` | — |
|
||||
| `GET /sites/{id}/vpn/servers` | `type`, `enabled` | 1 serveur WireGuard actif |
|
||||
| `GET /sites/{id}/wans` | `id`, `name` **uniquement** — inexploitable pour l'état | 2 WAN |
|
||||
|
||||
**Types de clients** (discriminant `type`) : `WIRED`, `WIRELESS`, `VPN`,
|
||||
`TELEPORT`. Sous-objet `access.type` : `DEFAULT`, `GUEST`.
|
||||
|
||||
**Filtrage.** Supporté, avec une syntaxe **préfixe** :
|
||||
|
||||
```
|
||||
✅ and(type.eq('WIRED'),access.type.eq('DEFAULT'))
|
||||
❌ type.eq('WIRED') and access.type.eq('DEFAULT') → 400 invalid-filter
|
||||
❌ type.eq('WIRED') & access.type.eq('DEFAULT') → 400 invalid-filter
|
||||
❌ type.eq('WIRED'),access.type.eq('DEFAULT') → 400 invalid-filter
|
||||
```
|
||||
|
||||
Combiné à `limit=1`, il permet d'obtenir un `totalCount` sans rapatrier les
|
||||
enregistrements. Utilisé uniquement au-delà de 200 clients (voir §7).
|
||||
|
||||
**Écart avec la documentation.** Ubiquiti ne publie l'OpenAPI que pour
|
||||
`v10.4.57`. La console en `10.6.102` renvoie des champs absents de ce spec
|
||||
(`macAddress`, `uplinkDeviceId` sur les clients). **Corollaire : on code contre
|
||||
les réponses réelles, validées par des schémas zod tolérants (`passthrough`),
|
||||
et on ne génère pas de client depuis le spec publié.**
|
||||
|
||||
### 3.2 API cloud Site Manager — `https://api.ui.com`
|
||||
|
||||
Authentification : en-tête `X-API-KEY`, clé générée sur `unifi.ui.com`.
|
||||
|
||||
| Endpoint | Utilité |
|
||||
|---|---|
|
||||
| `GET /v1/sites` | Corrélation (`meta.gatewayMac`), `ispInfo`, IP publique par WAN, `percentages.wanUptime` |
|
||||
| `POST /v1/isp-metrics/5m/query` | `avgLatency`, `maxLatency`, `packetLoss`, `uptime`, débits |
|
||||
|
||||
**Deux contraintes mesurées :**
|
||||
|
||||
1. `GET /v1/isp-metrics/5m` sans filtre renvoie **133 Ko** (288 points sur 24 h).
|
||||
Il faut passer par `POST .../query` borné dans le temps.
|
||||
2. Une fenêtre de 15 min renvoie un tableau **vide**. Une fenêtre de **3 h**
|
||||
fonctionne. Le point le plus récent accuse un retard mesuré de **~27 minutes**.
|
||||
|
||||
Ces deux faits pilotent le design du cache (§7) et l'affichage de la fraîcheur (§8).
|
||||
|
||||
### 3.3 Corrélation locale ↔ cloud
|
||||
|
||||
Une clé Site Manager donne accès à **tous** les sites du compte UI, y compris des
|
||||
consoles dont l'utilisateur n'est pas propriétaire. Le compte de référence en
|
||||
exposait deux. Prendre les métriques cloud sans discriminer afficherait la
|
||||
latence d'un autre réseau.
|
||||
|
||||
**Clé de jointure retenue : le MAC du gateway.**
|
||||
|
||||
```
|
||||
LOCAL devices[] → gateway.macAddress aa:bb:cc:11:22:33
|
||||
CLOUD sites[] → meta.gatewayMac aa:bb:cc:11:22:33 ✅ correspondance
|
||||
CLOUD sites[] → meta.gatewayMac dd:ee:ff:44:55:66 ✗ autre console du compte
|
||||
```
|
||||
|
||||
*(adresses anonymisées ; la correspondance a été vérifiée sur la console réelle)*
|
||||
|
||||
Retenu plutôt que le nom du site : un nom se renomme, un MAC non. **En l'absence
|
||||
de correspondance, `unifi-sm-api.ts` renvoie `null` et n'essaie aucune
|
||||
approximation.** Un manque visible vaut mieux qu'une donnée silencieusement fausse.
|
||||
|
||||
## 4. Contrat de données
|
||||
|
||||
```ts
|
||||
export interface UnifiOsSummary {
|
||||
site: {
|
||||
id: string;
|
||||
name: string;
|
||||
applicationVersion: string;
|
||||
};
|
||||
|
||||
wan: {
|
||||
/** Dérivé de l'état du gateway côté local. */
|
||||
state: "online" | "offline" | "unknown";
|
||||
/** Débit temps réel de l'uplink du gateway, en bits/s. Local. */
|
||||
throughput: { txBps: number; rxBps: number } | null;
|
||||
/** Bloc cloud. `null` si la clé Site Manager n'est pas configurée
|
||||
* ou si aucun site cloud ne correspond au MAC du gateway. */
|
||||
isp: {
|
||||
name: string;
|
||||
externalIp: string | null;
|
||||
latencyMs: number;
|
||||
maxLatencyMs: number;
|
||||
packetLossPct: number;
|
||||
uptimePct: number;
|
||||
/** Horodatage de la mesure. Rendu obligatoire : toute donnée cloud
|
||||
* voyage avec sa fraîcheur. */
|
||||
measuredAt: Date;
|
||||
} | null;
|
||||
};
|
||||
|
||||
clients: {
|
||||
total: number;
|
||||
wired: number;
|
||||
wireless: number;
|
||||
vpn: number; // VPN + TELEPORT
|
||||
guests: number; // access.type === "GUEST", tous types confondus
|
||||
};
|
||||
|
||||
devices: {
|
||||
total: number;
|
||||
online: number;
|
||||
offline: number;
|
||||
firmwareUpdatable: number;
|
||||
};
|
||||
|
||||
gateway: {
|
||||
name: string;
|
||||
model: string;
|
||||
macAddress: string;
|
||||
uptimeSec: number | null;
|
||||
cpuPct: number | null;
|
||||
memoryPct: number | null;
|
||||
} | null;
|
||||
}
|
||||
```
|
||||
|
||||
**`measuredAt` est obligatoire dans le bloc `isp`.** Le type impose que la
|
||||
fraîcheur accompagne la donnée cloud : il devient impossible d'afficher une
|
||||
latence sans savoir de quand elle date.
|
||||
|
||||
## 5. Architecture
|
||||
|
||||
### 5.1 Nouveaux fichiers
|
||||
|
||||
```
|
||||
packages/integrations/src/unifi-os/
|
||||
├── unifi-os-integration.ts # extends Integration — orchestre, mappe, dégrade
|
||||
├── unifi-os-api.ts # client HTTP local
|
||||
├── unifi-sm-api.ts # client HTTP cloud, optionnel, cache interne
|
||||
├── unifi-os-types.ts # schémas zod + mappers purs
|
||||
└── test/
|
||||
├── unifi-os-types.spec.ts
|
||||
└── unifi-os-integration.spec.ts
|
||||
```
|
||||
|
||||
Chaque unité répond à une question unique :
|
||||
|
||||
- `unifi-os-api.ts` — *comment parler à la console locale ?* Endpoints,
|
||||
pagination, résolution du `siteId`. Ne connaît pas Homarr.
|
||||
- `unifi-sm-api.ts` — *comment parler au cloud ?* Fenêtre glissante de 3 h,
|
||||
corrélation par MAC, cache interne. **Instancié uniquement si la clé cloud
|
||||
est présente.**
|
||||
- `unifi-os-types.ts` — *comment valider et transformer ?* Schémas zod et
|
||||
fonctions pures. Aucun accès réseau, donc testable sans mock.
|
||||
- `unifi-os-integration.ts` — *comment Homarr consomme tout ça ?* La seule
|
||||
unité qui connaît la classe `Integration`.
|
||||
|
||||
### 5.2 Points d'attache dans le monorepo
|
||||
|
||||
| Fichier | Modification |
|
||||
|---|---|
|
||||
| `packages/definitions/src/integration.ts` | Ajouter le secret `siteManagerApiKey` à `integrationSecretKindObject` ; ajouter `"unifiOs"` à `integrationCategories` ; ajouter le kind `unifiOs` avec `secretKinds: [["apiKey"], ["apiKey", "siteManagerApiKey"]]`, `category: ["unifiOs"]`, `defaultPort: 443` |
|
||||
| `packages/definitions/src/widget.ts` | Ajouter le `WidgetKind` `unifiOs` |
|
||||
| `packages/integrations/src/base/creator.ts` | Enregistrer `unifiOs: UnifiOsIntegration` |
|
||||
| `packages/integrations/src/types.ts` | Exporter `./unifi-os/unifi-os-types` |
|
||||
| `packages/integrations/src/interfaces/unifi-os-summary/` | Interface `UnifiOsSummaryIntegration` |
|
||||
| `packages/request-handler/src/unifi-os.ts` | `createIntegrationRequestHandler` |
|
||||
| `packages/api/src/router/widgets/unifi-os.ts` + `index.ts` | Routeur tRPC |
|
||||
| `packages/widgets/src/unifi-os/{index.ts,component.tsx}` | Définition + composant |
|
||||
| `packages/widgets/src/{manifest.ts,index.tsx,refetch-intervals.ts}` | Enregistrement |
|
||||
| `packages/translation/src/lang/en.json` | Libellés du widget + clé `integration.category.unifiOs` |
|
||||
|
||||
### 5.3 Pourquoi une catégorie dédiée, et pas `networkController`
|
||||
|
||||
`integrationCategories` n'est pas une étiquette de présentation, c'est un
|
||||
mécanisme de câblage. `getIntegrationKindsByCategory("networkController")`
|
||||
alimente trois endroits :
|
||||
|
||||
```
|
||||
widget-integration-map.ts → intégrations proposées par les widgets networkController*
|
||||
router/widgets/network-controller.ts → middleware tRPC
|
||||
request-handler/network-controller.ts → typé IntegrationKindByCategory<"networkController">,
|
||||
appelle getNetworkSummaryAsync()
|
||||
```
|
||||
|
||||
Déclarer `unifiOs` dans cette catégorie la rendrait sélectionnable dans les
|
||||
widgets de l'ancienne intégration **et** exigerait qu'elle implémente
|
||||
`getNetworkSummaryAsync()` — l'interface que la décision D3 écarte
|
||||
explicitement. Le typage `IntegrationKindByCategory` transforme cette
|
||||
contradiction en erreur de compilation, pas en défaut silencieux.
|
||||
|
||||
On ajoute donc une catégorie `unifiOs`. Le dépôt a déjà un précédent de
|
||||
catégorie mono-intégration : `beszel`. Une catégorie sans traduction ne casse
|
||||
rien — `beszel` n'en a pas — mais on ajoute quand même
|
||||
`integration.category.unifiOs`.
|
||||
|
||||
**Base d'URL.** L'utilisateur saisit l'origine de la console
|
||||
(`https://console.exemple.tld`). Le client local préfixe systématiquement
|
||||
`/proxy/network/integration/v1`. Ce préfixe appartient au client, pas à la
|
||||
configuration : l'utilisateur n'a pas à le connaître.
|
||||
|
||||
**Précédent de conception validé :** le champ `secretKinds` accepte plusieurs
|
||||
jeux alternatifs, et la classe de base expose `hasSecretValue()`. L'intégration
|
||||
`ntfy` utilise déjà exactement ce motif (`[["topic"], ["topic", "apiKey"]]`).
|
||||
La clé cloud optionnelle ne demande donc aucun mécanisme nouveau.
|
||||
|
||||
**Note TLS.** La classe de base `Integration` applique les certificats de
|
||||
confiance de Homarr à l'URL de l'intégration uniquement. L'appel vers
|
||||
`api.ui.com` sort de ce périmètre : c'est du HTTPS public à certificat valide,
|
||||
un `fetch` standard suffit. Il ne doit **pas** passer par
|
||||
`fetchWithTrustedCertificatesAsync`, pour ne pas laisser croire que les deux
|
||||
appels partagent la même configuration.
|
||||
|
||||
## 6. Séquence d'un rafraîchissement
|
||||
|
||||
```
|
||||
1. GET /info → applicationVersion
|
||||
2. GET /sites → siteId, name
|
||||
3. Promise.allSettled([
|
||||
GET /sites/{id}/clients?limit=200 → comptage par type (voir §7)
|
||||
GET /sites/{id}/devices → états, firmwares, gateway
|
||||
GET /sites/{id}/vpn/servers → VPN actif
|
||||
])
|
||||
4. GET /sites/{id}/devices/{gatewayId}/statistics/latest (si gateway trouvé)
|
||||
5. si clé cloud présente :
|
||||
unifi-sm-api.getIspMetricsAsync(gatewayMac) → cache 5 min
|
||||
```
|
||||
|
||||
**`Promise.allSettled`, pas `Promise.all`.** Un endpoint en échec dégrade son
|
||||
bloc à `null` ; il ne fait pas tomber le widget entier. L'étape 4 dépend de
|
||||
l'étape 3 : sans gateway identifié, `gateway` et `wan.throughput` valent `null`
|
||||
et le reste s'affiche normalement.
|
||||
|
||||
## 7. Cache et gestion d'erreurs
|
||||
|
||||
**Deux caches de rythmes différents**, parce que les deux sources n'évoluent pas
|
||||
à la même vitesse :
|
||||
|
||||
| Couche | TTL | Motif |
|
||||
|---|---|---|
|
||||
| `createIntegrationRequestHandler` | 60 s | Les compteurs locaux sont vivants |
|
||||
| Cache interne de `unifi-sm-api.ts`, clé = MAC gateway | 5 min | La donnée cloud ne change que toutes les 5 min et arrive avec ~27 min de retard |
|
||||
|
||||
Sans le second cache, on émettrait ~120 appels/h vers `api.ui.com` pour ~12
|
||||
valeurs distinctes, avec un risque de limitation de débit. Le motif du cache
|
||||
statique par instance est déjà utilisé par `TrueNasClient`.
|
||||
|
||||
**`fallbackToStaleOnError: true`** sur le handler. Une console injoignable
|
||||
pendant quelques secondes affiche la dernière valeur connue au lieu d'une tuile
|
||||
d'erreur — à condition que l'interface signale que la donnée est périmée (§8).
|
||||
|
||||
**Comptage des clients — règle explicite.** L'API plafonne `limit` à 200
|
||||
(mesuré). Le comportement est donc conditionné par `totalCount`, renvoyé dès la
|
||||
première page :
|
||||
|
||||
| Condition | Stratégie | Coût |
|
||||
|---|---|---|
|
||||
| `totalCount ≤ 200` | La première page contient tout. Comptage en mémoire par `type` et `access.type`. | 1 requête |
|
||||
| `totalCount > 200` | Bascule sur des requêtes filtrées `limit=1`, dont on ne lit que `totalCount`. | 5 requêtes de taille fixe |
|
||||
|
||||
La seconde branche utilise la syntaxe préfixe du §3.1, par exemple
|
||||
`and(type.eq('WIRELESS'),access.type.eq('GUEST'))`.
|
||||
|
||||
On ne pagine **pas** en boucle : au-delà de 200 clients, rapatrier tous les
|
||||
enregistrements pour n'en garder que des compteurs coûte plus cher que cinq
|
||||
requêtes bornées, et la charge croîtrait avec la taille du réseau.
|
||||
|
||||
*Écrire les deux branches maintenant est justifié : ce n'est pas de
|
||||
l'optimisation spéculative mais la gestion d'une limite d'API mesurée, qui
|
||||
produirait sinon des compteurs silencieusement faux au-delà de 200 clients.*
|
||||
|
||||
## 8. Widget
|
||||
|
||||
**Disposition retenue : bandeau WAN dominant + lignes** (option B des maquettes).
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ ● Nom du site │
|
||||
│ ┌────────────────────────────────────────────┐ │
|
||||
│ │ ● Internet ↑14,1 ↓3,3 Mb/s │ │
|
||||
│ │ Nom FAI · 203.0.113.10 │ │
|
||||
│ │ 11 ms · 0 % perte [27min]│ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
│ Clients 51 · 44 filaire · 7 Wi-Fi │
|
||||
│ Équipements 6 / 6 en ligne │
|
||||
│ ● Firmware 3 à mettre à jour │
|
||||
│ Network 10.6.102 Gateway up 11 j 16 h │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Justification : la hiérarchie visuelle suit l'ordre de consultation réel — on
|
||||
regarde d'abord si Internet fonctionne, ensuite le détail.
|
||||
|
||||
**Le badge de fraîcheur n'est pas cosmétique.** Il porte deux rôles distincts :
|
||||
signaler le retard structurel des métriques cloud, et signaler une donnée servie
|
||||
depuis le cache périmé après une erreur. Sans lui, le widget juxtaposerait des
|
||||
données de fraîcheurs très différentes sans le dire.
|
||||
|
||||
**Comportement dégradé :**
|
||||
|
||||
| Situation | Rendu |
|
||||
|---|---|
|
||||
| Clé cloud absente | Bloc ISP masqué. Le bandeau garde l'état WAN et le débit. |
|
||||
| Cloud injoignable | Idem, plus un indicateur discret. |
|
||||
| Aucune correspondance de MAC | Bloc ISP masqué. Jamais la latence d'un autre site. |
|
||||
| Gateway introuvable | `throughput`, `uptime`, CPU/RAM masqués ; clients et équipements affichés. |
|
||||
| Console injoignable | Dernière valeur connue + badge périmé, puis tuile d'erreur au-delà du TTL. |
|
||||
|
||||
**Taille.** `WidgetDefinition` n'expose aucune contrainte de taille minimale :
|
||||
l'adaptation est une affaire de CSS dans le composant. Le bandeau passe en deux
|
||||
lignes en tuile étroite, et les lignes de détail se réduisent à leurs chiffres.
|
||||
|
||||
**Rafraîchissement.** Entrée dans `widgetQueryRefetchIntervals` avec
|
||||
`intervalSeconds: 60`. Les widgets `networkController` sont à `null` — pas de
|
||||
rafraîchissement automatique — mais les compteurs de clients ont une valeur
|
||||
d'usage qui justifie l'écart.
|
||||
|
||||
## 9. Tests
|
||||
|
||||
| Niveau | Fichier | Couvre | Réseau |
|
||||
|---|---|---|---|
|
||||
| Unitaire | `unifi-os-types.spec.ts` | Comptage par type et par `access.type`, agrégation d'états, conversion bps, corrélation par MAC **y compris le cas sans correspondance**, tolérance aux champs inconnus | Non |
|
||||
| Intégration | `unifi-os-integration.spec.ts` | Séquence complète sur fixtures capturées, dégradation par bloc, absence de clé cloud, endpoint en échec | Mocké |
|
||||
| Manuel | script hors CI | Vérification contre une console réelle | Oui |
|
||||
|
||||
Le niveau unitaire est le plus important : c'est là que vivent les erreurs de
|
||||
comptage, et c'est le seul qui reste valable si Ubiquiti fait évoluer ses
|
||||
endpoints.
|
||||
|
||||
**Fixtures.** Capturées depuis la console réelle, puis **anonymisées** —
|
||||
adresses MAC, IP publique, noms de clients et identifiants de site remplacés.
|
||||
Un dépôt ne doit pas porter la cartographie d'un réseau privé.
|
||||
|
||||
**Cas de régression explicite :** l'issue upstream
|
||||
[#3222](https://github.com/homarr-labs/homarr/issues/3222) signalait des valeurs
|
||||
nulles en multi-site sur l'ancienne intégration. Un test couvre l'agrégation
|
||||
multi-site pour ne pas reproduire ce défaut.
|
||||
|
||||
## 10. Phasage
|
||||
|
||||
Chaque phase se vérifie seule. C'est ce qui permet de savoir de quel côté
|
||||
chercher quand quelque chose casse.
|
||||
|
||||
| Phase | Contenu | Critère de validation |
|
||||
|---|---|---|
|
||||
| 0 | Fork, `corepack enable`, `pnpm docker:dev:up`, `pnpm dev` | Homarr démarre en local |
|
||||
| 1 | `unifi-os-api.ts`, `unifi-sm-api.ts`, types et mappers, tests unitaires | `pnpm test` vert, sans réseau |
|
||||
| 2 | `UnifiOsIntegration` + enregistrement | Le test de connexion Homarr passe dans l'interface |
|
||||
| 3 | request-handler + routeur tRPC | La query renvoie les vraies valeurs |
|
||||
| 4 | Widget + traductions `en` | Le widget s'affiche sur un board local |
|
||||
| 5 | Build standalone + rsync vers le LXC | Le widget s'affiche en production |
|
||||
|
||||
À l'issue de la phase 2, toute la plomberie est prouvée. Une panne en phase 4
|
||||
est donc nécessairement côté interface.
|
||||
|
||||
## 11. Déploiement
|
||||
|
||||
Cible : LXC Ubuntu 24.04, `10.70.1.13`, 2 vCPU / 2 Go. Homarr y tourne en build
|
||||
standalone sous systemd (`/opt/homarr`, `homarr.service`,
|
||||
`EnvironmentFile=/etc/homarr/homarr.env`), avec nginx et Redis embarqués par
|
||||
`run.sh`.
|
||||
|
||||
**Le build se fait sur le poste de développement** (16 cœurs, 60 Go). Le LXC ne
|
||||
peut pas compiler ce monorepo Next.js.
|
||||
|
||||
Procédure :
|
||||
|
||||
1. `tar` de `/opt/homarr` **et** dump de la base.
|
||||
2. `systemctl stop homarr`.
|
||||
3. `rsync` du build standalone.
|
||||
4. `systemctl start homarr`, puis vérification des logs.
|
||||
|
||||
Rollback : restauration de l'archive et redémarrage.
|
||||
|
||||
**Contrôle préalable obligatoire :** comparer les migrations de
|
||||
`packages/db/migrations/` avec `/opt/homarr/db/migrations/`. Elles sont
|
||||
alignées à `0041_add_right_click_preference.sql` au 2026-09-08. Une divergence
|
||||
rendrait le rollback nettement plus complexe qu'une restauration d'archive.
|
||||
|
||||
**`SECRET_ENCRYPTION_KEY`** dans `/etc/homarr/homarr.env` chiffre les secrets
|
||||
d'intégration en base. Le rsync ne touche pas ce fichier, mais toute
|
||||
modification rendrait les intégrations existantes illisibles.
|
||||
|
||||
## 12. Hors périmètre
|
||||
|
||||
Volontairement exclus de cette spec :
|
||||
|
||||
- **Widget cloud multi-site** (SD-WAN, fabric, vue consolidée de plusieurs
|
||||
consoles). Idée retenue pour un projet ultérieur, qui réutilisera
|
||||
`unifi-sm-api.ts`. Sa mention ici sert à ne pas le construire maintenant.
|
||||
- **Actions d'écriture** (adoption d'équipement, redémarrage de port, gestion de
|
||||
vouchers). L'API les expose ; l'intégration reste en lecture seule.
|
||||
- **Traduction au-delà de l'anglais.** Conséquence de D1.
|
||||
- **Documentation Docusaurus.** Conséquence de D1.
|
||||
- **Pull request upstream.** Conséquence de D1 ; la structure du code ne
|
||||
l'interdit pas plus tard.
|
||||
- **Modification de l'intégration `unifiController`.** Elle reste en l'état.
|
||||
|
||||
## 13. Risques
|
||||
|
||||
| Risque | Portée | Traitement |
|
||||
|---|---|---|
|
||||
| L'OpenAPI publié (`10.4.57`) est en retard sur les consoles | Élevée — déjà constaté | Schémas zod tolérants, code écrit contre les réponses réelles |
|
||||
| La clé Site Manager donne accès à tout le compte UI | Sécurité | Cloud optionnel ; corrélation stricte par MAC ; clé de conception à révoquer |
|
||||
| Retard de ~27 min sur les métriques cloud | Interface | `measuredAt` obligatoire dans le contrat, badge de fraîcheur dans le widget |
|
||||
| Un fork diverge à chaque version de Homarr | Maintenance | Modifications concentrées dans un dossier neuf ; seuls les points d'attache du §5.2 touchent l'existant |
|
||||
| Limitation de débit sur `api.ui.com` | Fiabilité | Cache interne 5 min |
|
||||
| Le déploiement casse l'instance de production | Exploitation | Sauvegarde avant rsync, contrôle des migrations, rollback par restauration |
|
||||
Reference in new issue
Block a user