# Design — Sidebar figée, groupes de réservations et filtres

**Date :** 2026-07-10
**Périmètre :** améliorations d'interface sur la navigation des subnets et la vue détaillée d'un subnet.

## Contexte et problème

Sur l'instance de production, la liste des subnets dans la sidebar s'allonge et pousse
la navigation et l'administration hors de vue. De même, un subnet avec beaucoup de
réservations devient difficile à lire dans `subnet.php`.

Objectifs :

1. Figer Navigation et Administration dans la sidebar ; ne faire scroller que la liste
   des subnets, avec un filtre de recherche rapide en tête.
2. Ajouter une notion de **groupe** aux réservations, avec deux modes d'affichage
   (groupés / plat) et un tri par IP.
3. Ajouter un filtre multi-champs dans la vue d'un subnet.

## Contrainte forte : ne pas toucher au schéma Kea

Aucune colonne ne doit être ajoutée aux tables natives Kea (`hosts`, `dhcp4_*`, etc.).
Le groupe est une métadonnée applicative : il vit exclusivement dans `app_reservation_meta`
(table préfixée `app_`), au même titre que `description` et `short_lease`.

## Décisions retenues

- **Sidebar** : organisation « version A » — Navigation figée en haut, filtre + liste
  scrollable au milieu, Administration + pied figés en bas.
- **Groupe** : texte libre avec autocomplétion (pas de table de groupes ni d'écran de
  gestion). Stocké dans `app_reservation_meta.group_name`.
- **Affichage** : deux modes, bascule mémorisée en `localStorage`, rendu côté JS.
- **Contraste** : texte foncé sur fond gris clair validé (≈ 11:1), inversé en thème sombre.

---

## 1. Sidebar figée (`lib/layout_header.php` + `public/assets/css/app.css`)

`#sidebar` est déjà `position:fixed; display:flex; flex-direction:column`. On le
réorganise en trois zones verticales :

| Zone | Contenu | Comportement |
|------|---------|--------------|
| Haut | brand + section **Navigation** (Tableau de bord, Subnets, Leases, Recherche, Demandes) | figé (hauteur naturelle) |
| Milieu | titre « Subnets » + **champ de filtre** (figé) puis liste `.subnet-list` | seule la liste scrolle (`flex:1; overflow-y:auto`) |
| Bas | section **Administration** (Paramètres si admin, Journal) + `.sidebar-footer` | figé (`margin-top` retiré du footer, remplacé par la structure flex) |

### CSS

- La zone du milieu devient un conteneur flex-column ; `.subnet-list` porte
  `flex:1 1 auto; overflow-y:auto; min-height:0` (le `min-height:0` est requis pour
  qu'un enfant flex puisse rétrécir et scroller).
- Le titre de section « Subnets » et l'input de filtre restent hors de la zone
  scrollable (`flex:0 0 auto`).
- Retirer `overflow-y:auto` de `#sidebar` global (il passe sur `.subnet-list`).
- Le bloc Administration + footer forme la zone basse figée.

### Filtre sidebar

Input texte au-dessus de `.subnet-list`. JS instantané (événement `input`) :
masque/affiche chaque `<a>` de subnet selon que son nom (comparé en minuscules,
sans accents) contient la saisie. Purement client, pas de mémorisation, pas d'impact
serveur. Petit bouton ✕ pour vider.

### Cas particuliers

- Si aucun subnet ne correspond au filtre : afficher une ligne discrète « aucun subnet ».
- La sidebar reste masquée sous 768px (media query existante inchangée).

---

## 2. Groupe de réservation

### Migration SQL — `sql/add_reservation_group.sql`

```sql
ALTER TABLE app_reservation_meta
    ADD COLUMN group_name VARCHAR(64) NULL AFTER description;
```

Idempotence : la migration est appliquée une fois via
`mysql -h 192.168.1.2 -u claude -pclaudedev123 kea < sql/add_reservation_group.sql`.
Le fichier `sql/init_app_tables.sql` (installation fraîche) est mis à jour pour inclure
la colonne. La section « Migration » de `CLAUDE.md` documente le passage de version.

### `lib/Reservation.php`

- `getBySubnet()` : ajouter `m.group_name` au SELECT et le retourner dans chaque ligne
  (`$row['group_name']`, chaîne éventuellement vide/null).
- `getById()` : idem, pour pré-remplir le formulaire d'édition.
- `add()` / `update()` : écrire `group_name` dans le `INSERT` / l'`ON DUPLICATE KEY UPDATE`
  de `app_reservation_meta` (trim ; chaîne vide → `NULL`).
- Nouvelle méthode :

  ```php
  /** Liste distincte des groupes non vides utilisés sur un subnet (pour l'autocomplétion). */
  public static function getGroups(int $keaSubnetId): array
  ```

  Jointure `hosts` × `app_reservation_meta` filtrée par `dhcp4_subnet_id`,
  `group_name IS NOT NULL AND group_name <> ''`, `DISTINCT`, triée alphabétiquement.

### Formulaires `reservation_add.php` / `reservation_edit.php`

- Ajouter un champ texte « Groupe » (optionnel) avec un `<datalist>` alimenté par
  `Reservation::getGroups($keaSubnetId)` → autocomplétion native HTML, création à la volée.
- `reservation_add.php` accepte un éventuel paramètre GET `group=` pour pré-remplissage
  (cohérent avec les autres pré-remplissages `mac=`, `ip=`, etc.).
- La valeur est propagée dans `$data['group_name']` passé à `Reservation::add()/update()`.
- Pas de validation stricte (texte libre) ; simple `trim` + longueur max 64 (attribut
  `maxlength` côté HTML, tronqué côté PHP par sécurité).

### Import / Export CSV

- **Export** (`subnet_export.php`) : ajouter une colonne `group` après `description`.
  Préserve l'aller-retour (même logique que `no_gateway`).
- **Import** (`subnet_import.php`) : détecter une colonne optionnelle `group` (en-tête
  exact `group`, insensible aux espaces autour). La valeur alimente `$data['group_name']`
  dans `analyzeRows()` (aperçu) et à l'étape 2 (application). Absence de colonne → groupe
  vide, pas d'erreur (compatibilité anciens CSV).

---

## 3. Deux modes d'affichage dans `subnet.php`

### Approche : rendu côté JS

Comme `leases.php`, `subnet.php` injecte les réservations via `json_encode` et rend le
tableau en JavaScript. Cela permet un basculement mode/filtre instantané sans
rechargement. Le PHP se limite à préparer les données (déjà fait par `getBySubnet`,
enrichi de `group_name`).

Données injectées par réservation : `host_id`, `hostname`, `ip_address`, `ip_int`
(pour le tri numérique — calculé en PHP via `ipToInt`, `0` si dynamique),
`mac`, `mac_hex` (pour la recherche sans `:`), `description`, `group_name`,
`has_options`, `has_ntp`, `short_lease`, `updated_at`/`created_at`, plus l'`id` du subnet
pour les liens edit/delete. Le rôle courant (`viewer` ou non) conditionne l'affichage
des boutons d'action, injecté comme booléen.

### Contrôles UI (au-dessus du tableau)

- Un `btn-group` Bootstrap à deux boutons : **Groupés** | **Plat**.
- Le champ de filtre (section 4).
- Le mode actif est lu/écrit dans `localStorage` (clé ex. `dhcpman.subnet.viewmode`),
  défaut = Groupés.

### Mode Groupés (défaut)

- Regroupement par `group_name` ; les réservations sans groupe forment un groupe
  « (sans groupe) » affiché **en dernier**. Les autres groupes sont triés alphabétiquement.
- Chaque groupe = un bandeau repliable (clic sur le bandeau plie/déplie) portant le nom
  et un compteur (pill) du nombre de lignes **visibles** (recalculé quand le filtre change).
- Sous le bandeau, les réservations du groupe triées par `ip_int` croissant. Les
  réservations dynamiques (`ip_int = 0`, IP vide) se placent en tête (valeur 0 = plus
  petite), affichées « dynamique » comme aujourd'hui.
- Colonnes : Hostname, IP, MAC, Description, Modifié le, actions. Pas de colonne groupe
  (le bandeau la porte).
- État plié/déplié des groupes : non persisté (repart déplié à chaque visite).

### Mode Plat

- Liste unique triée par `ip_int` croissant (comportement actuel).
- Colonnes : Hostname, IP, MAC, **Groupe** (badge ; « — » grisé si vide), Description,
  Modifié le, actions.

### Contraste (clair + sombre)

- Bandeau : fond `#e2e8f0`, texte `#1e293b`, bordure `#cbd5e1`, compteur pill blanc sur
  `#475569`. « (sans groupe) » en italique grisé.
- Badge groupe (mode plat) : mêmes tons.
- Thème sombre : variantes inversées (fond sombre, texte clair) — via classes CSS
  dédiées dans `app.css`. (Le projet est actuellement en thème clair ; prévoir les deux
  jeux de couleurs pour rester cohérent si un thème sombre est introduit, sans surcoût.)

### Icônes existantes conservées

Les indicateurs actuels (options DHCP `bi-sliders`, NTP `bi-clock`, short-lease
`bi-hourglass-split`) restent affichés à côté du hostname dans les deux modes, avec
leurs tooltips. Réinitialisation des tooltips Bootstrap après chaque rendu JS.

---

## 4. Filtre de la vue subnet

- Input texte au-dessus du tableau, JS instantané (`input`).
- Champs testés : `hostname`, `ip_address`, `mac` **et** `mac_hex` (permet la saisie sans
  `:`), `description`, `group_name`. **Pas** la date.
- Comparaison insensible à la casse et aux accents (normalisation via
  `String.prototype.normalize('NFD')` + suppression des diacritiques, des deux côtés).
- Fonctionne dans les deux modes :
  - Groupés : une ligne masquée disparaît ; un groupe dont toutes les lignes sont masquées
    est caché ; les compteurs reflètent le nombre visible.
  - Plat : lignes masquées, tri conservé.
- Indicateur « N résultats sur M » à côté du champ.
- Bouton ✕ pour vider. Filtre non mémorisé entre visites.

---

## Fichiers touchés

| Fichier | Nature |
|---------|--------|
| `sql/add_reservation_group.sql` | **nouveau** — migration |
| `sql/init_app_tables.sql` | +colonne `group_name` (install fraîche) |
| `lib/Reservation.php` | `group_name` dans getBySubnet/getById/add/update ; `getGroups()` |
| `lib/layout_header.php` | réorganisation sidebar + filtre subnets |
| `public/assets/css/app.css` | zones figées/scrollable, bandeaux, badges, contraste |
| `public/subnet.php` | rendu JS, 2 modes, filtre multi-champs |
| `public/reservation_add.php` | champ groupe + datalist + GET `group=` |
| `public/reservation_edit.php` | champ groupe + datalist |
| `public/subnet_export.php` | colonne CSV `group` |
| `public/subnet_import.php` | colonne CSV `group` (parse, aperçu, application) |
| `CLAUDE.md` | doc migration + doc modes d'affichage/groupe |

## Hors périmètre (YAGNI)

- Pas de table de groupes ni d'écran de gestion (renommer/supprimer en masse).
- Pas de couleur personnalisée par groupe.
- Pas de groupes dans la page de recherche globale (`search.php`) ni dans `leases.php`
  pour cette itération.
- Pas de persistance de l'état plié/déplié des groupes.

## Risques et points d'attention

- **Rendu JS de `subnet.php`** : réécriture du tableau ; veiller à conserver tous les
  indicateurs et liens actuels (edit/delete avec `subnet_id`), et la ré-init des tooltips.
- **Échappement** : les données injectées en JSON doivent l'être via `json_encode` avec
  `JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_HEX_AMP` ; le rendu JS doit insérer le
  texte via `textContent` (jamais `innerHTML` sur des données utilisateur).
- **Migration** : appliquer sur la base avant déploiement ; sans la colonne, les requêtes
  `group_name` échoueraient.
