# Design — Bargraph de remplissage de subnet

**Date :** 2026-07-14
**Statut :** validé (brainstorming)
**Inspiration :** vue Patch d'Avolites Titan (barre des 512 canaux DMX d'un univers)

## Objectif

Visualiser le remplissage d'un subnet dans `subnet.php` par une barre horizontale
unique. Transposition directe de Titan :

| Titan | DHCPMan |
|-------|---------|
| Univers DMX (512 canaux) | Subnet (plage d'IP) |
| Canal | Adresse IP |
| Appareil patché (bloc de canaux) | Réservation / pool |
| Trou (canaux libres) | Espace IP libre |
| Clic sur bloc → sélection appareil | Clic sur bloc → filtre le tableau sur la plage |
| Clic sur trou → prochaine adresse libre | Clic sur trou → ajout pré-rempli avec la 1re IP libre |

## Portée & contraintes

- **Tailles de subnet visées : /22 à /29** (1024 → 8 adresses). Pas de drill-down
  hiérarchique nécessaire (validé) ; une stratégie de **largeur minimale** suffit.
- **100 % côté client**, comme le reste de `subnet.php`. Aucune table, aucune
  migration SQL, aucun endpoint. On étend les données injectées + un module JS.
- **Offline** : aucun CDN. CSS dans `app.css`, thème clair/sombre via
  `@media (prefers-color-scheme: dark)`.
- **Infobulle maison** (pas Bootstrap Tooltip) pour un contenu riche et un
  positionnement fluide sur des segments fins.

## Décisions de design (validées)

1. **Échelle** : proportionnel + **plancher 5px** sur les blocs occupés, puis
   **renormalisation globale** pour que Σ des largeurs = 100 %. Les trous ne sont
   jamais planchés (un trou minuscule peut disparaître visuellement).
2. **Couleurs par type** (3 couleurs) : `reservation` (bleu), `dynamic` (ambre),
   `short-pool` (violet). Trous = hachures neutres. Pas de couleur par groupe sur
   le fond de la barre.
3. **Fusion des blocs par TYPE**, pas par groupe. Deux réservations contiguës
   fusionnent même si groupes différents (sinon barre illisible sur subnets à
   groupes « random »). L'infobulle liste les groupes présents + compte par groupe.
4. **Réservation short-lease avec IP fixe = bleu** (réservation normale). Le
   statut short-lease est porté par l'infobulle (« dont N short-lease »). Le type
   `short-pool` (violet) ne concerne que le *pool* short-lease du subnet.
5. **Emplacement** : barre principale dans le `card-header`. Taux de remplissage en
   **texte** (« 126 / 510 occupées · 25 % »), pas de 2e barre-résumé.
6. **Clic sur bloc réservation** → réutilise le champ filtre existant, enrichi pour
   comprendre une **syntaxe de range** (`192.168.2.10-192.168.2.19`). Clic sur bloc
   pool → rien (infobulle seule). Clic sur trou → `reservation_add.php` avec la 1re
   IP libre.
7. **Fichier de test versionné** (`subnet-bargraph.test.html`, offline).
8. **Page démo statique** (`subnet-bargraph-demo.html`) avec un /23 fictif en dur.
   Aucune modification de la base de données.

## Architecture — 3 couches pures

Nouveau module `public/assets/js/subnet-bargraph.js` exposant
`SubnetBargraph.init({ mount, rows, subnet, onFilter, canEdit })`.

### 1. Couche métier (calcul, sans DOM — testable)

```
computeBlocks(rows, subnet)  → [{ type, startInt, endInt, count, groups, hostIds }]
computeHoles(blocks, subnet) → [{ startInt, endInt, count, largestCidr }]
largestAlignedCidr(startInt, endInt) → "192.168.2.16/28"
maskForCount(startInt, endInt)       → masque équivalent affiché
```

**Règles de `computeBlocks` (ordre important) :**

1. Réservations avec IP réelle (`ip_int > 0`) triées par `ip_int`.
2. Fusion : `next.startInt === cur.endInt + 1` **et** même type → même bloc ;
   accumulation des groupes dans `groups` (`""` = sans groupe).
3. Pools (dynamique, short-lease) injectés comme blocs de leur type propre ; jamais
   fusionnés avec une réservation.
4. Réservations sans IP (allocation dynamique) → aucun bloc (elles vivent dans le pool).
5. Ensemble re-trié par `startInt`.

**Modèle d'un bloc :**
```js
{ type: "reservation"|"dynamic"|"short-pool",
  startInt, endInt, count,
  groups: { "serveurs": 6, "": 2 },   // réservations uniquement
  shortLeaseCount: 3,                  // dont N short-lease (réservations)
  hostIds: [12, 15, ...] }
```

**Modèle d'un trou :**
```js
{ startInt, endInt, count, largestCidr: "192.168.2.16/28" }
```

### 2. Couche layout (échelle — pure)

```
layoutSegments(blocks, holes, subnet, { barWidthPx, minPx: 5 })
  → [{ kind:"block"|"hole", ref, leftPct, widthPct }]
```

1. Séquence ordonnée couvrant `firstUsable → lastUsable`, sans trou ni chevauchement.
2. `widthPct = count / total * 100`.
3. Plancher : `minPct = minPx / barWidthPx * 100`. Tout **bloc occupé**
   `widthPct < minPct` → `minPct`. Les trous ne sont pas planchés.
4. Renormalisation globale : le surplus `E` est retiré proportionnellement aux
   segments au-dessus du plancher, Σ = 100 %. Itération bornée si un segment
   repasserait sous son plancher.
5. Recalcul debounced au `resize` (le plancher dépend de `barWidthPx`).

### 3. Couche rendu (DOM)

`renderBar(mount, segments)` + survol/clic + infobulle maison. Seule couche qui
touche le DOM.

## Données injectées par `subnet.php`

`ROWS` reste inchangé (contient déjà `ip_int`, `group_name`, `short_lease`,
`host_id`, `hostname`). On ajoute un objet `SUBNET` :

```js
var SUBNET = {
  cidr: "192.168.2.0/23",
  networkInt, broadcastInt, firstUsable, lastUsable,
  total,                         // lastUsable - firstUsable + 1
  dynStartInt, dynEndInt,        // null si pas de pool dynamique
  shortStartInt, shortEndInt,    // null si pas de pool short-lease
  addUrl: "reservation_add.php?subnet_id=7"
};
```

Calculé en PHP à partir du CIDR (réseau/masque) + colonnes pool déjà lues.

## Interactions

- **Survol bloc** → infobulle : type, plage IP, nb d'adresses, masque équivalent ;
  pour une réservation : groupes + comptes, « dont N short-lease ».
- **Survol trou** → infobulle : « Libre », plage IP, nb d'adresses, plus grand CIDR
  alignable.
- **Clic bloc réservation** → écrit `a.b.c.d-a.b.c.d` dans `#res-filter` + dispatch
  `input`.
- **Clic bloc pool** → rien (infobulle seule).
- **Clic trou** (si `canEdit`) → `reservation_add.php?subnet_id=X&ip=<1re IP libre>`.

### Enrichissement du filtre (`matches` dans `subnet.php`)

En tête de `matches()` : détection d'un motif de range (`a.b.c.d-a.b.c.d`, ou forme
courte `10-20` = octets de fin, complétés avec le préfixe réseau du subnet courant)
→ comparaison numérique `r.ip_int >= lo && r.ip_int <= hi`. Sinon → comportement
sous-chaîne **inchangé**. Bonus : range tapable au clavier.
(La forme à point initial `.10-.20` n'est pas reconnue : la regex exige un chiffre initial.)

## Cas limites

| Cas | Comportement |
|-----|--------------|
| Subnet vide | Un seul grand trou, « 0 / N · 0 % ». Composant affiché. |
| Subnet plein | Aucun trou ; renormalisation triviale. |
| Réservation hors CIDR | Ignorée du calcul (console.warn), reste dans le tableau. |
| Chevauchement bloc/pool | Clamp aux bornes ; arbitrage en faveur du pool (plage déclarée). |
| Toutes réservations dynamiques | Seuls pools + trous s'affichent. |
| `barWidthPx = 0` (conteneur masqué) | Layout différé jusqu'à `clientWidth > 0` (RAF), sinon proportionnel pur. |
| Trou de 1 adresse | `.42 – .42`, `largestCidr = /32`. |
| Pools qui se chevauchent | Chaque pool rendu séparément ; non-supporté proprement (documenté). |

## Tests

Couche métier **pure** → testable sans navigateur ni DB. Le projet n'a pas de
framework JS : **fichier de test autonome versionné**
`public/assets/js/subnet-bargraph.test.html` qui charge le module, exécute des
assertions (`computeBlocks`, `computeHoles`, `largestAlignedCidr`, `layoutSegments`)
et affiche vert/rouge. Zéro dépendance, offline.

## Données d'exemple (livrable)

Page démo statique `public/assets/js/subnet-bargraph-demo.html` (non liée à la DB),
un **/23** (510 adresses utilisables) avec :

- un pool dynamique,
- un pool short-lease,
- plusieurs réservations (dont groupées, dont une short-lease à IP fixe),
- **au moins deux trous de tailles différentes** (un grand + un d'1-2 adresses pour
  démontrer plancher + renormalisation).

Ces données servent de démo **et** de fixtures pour le fichier de test.

## Fichiers touchés

| Fichier | Action |
|---------|--------|
| `public/assets/js/subnet-bargraph.js` | **nouveau** — module (3 couches) |
| `public/assets/js/subnet-bargraph.test.html` | **nouveau** — tests couche métier |
| `public/assets/js/subnet-bargraph-demo.html` | **nouveau** — démo /23 statique |
| `public/subnet.php` | injection `SUBNET`, montage du composant, `matches()` enrichi (range), `<script src>` |
| `public/assets/css/app.css` | styles barre + segments + infobulle (thème clair/sombre) |

## Limites assumées

- Au-delà de /22, la barre reste rendue mais la lisibilité se dégrade (beaucoup de
  micro-blocs planchés) ; le drill-down hiérarchique est hors périmètre.
- Le plancher 5px introduit une imprécision d'échelle sur les micro-blocs, répartie
  invisiblement par la renormalisation globale — compromis assumé (comportement Titan).
- Chevauchements de pools déclarés incohérents : rendus mais non garantis.
