chore: init projet — spec design + design system

Spec complète dans docs/superpowers/specs/2026-05-28-inventaire-hdd-design.md :
architecture 2 conteneurs Docker (FastAPI + nginx), script Python stdlib only,
SQLite avec serial comme clé de vérité, API ingest + dashboard + agents IA.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gilles Soulier
2026-05-28 19:46:54 +02:00
commit f9f805cd8b
11 changed files with 3755 additions and 0 deletions
@@ -0,0 +1,294 @@
# Inventaire HDD — Design Spec
**Date** : 2026-05-28
**Projet** : mes_hdd
**Statut** : approuvé
---
## Contexte
Outil de monitoring des disques physiques (HDD/SSD/NVMe) pour un parc de 1015 machines Proxmox, Debian et Ubuntu. Certaines machines sont des VMs avec disques en PCI passthrough (45 machines).
Le numéro de série est la **source de vérité unique** d'un disque : si un disque migre entre machines, si il tombe en panne ou est retiré, l'historique est conservé.
Le script est lancé **manuellement** par l'administrateur quand la configuration change (pas de cron, pas de daemon).
---
## Architecture globale
```
[Machine cliente — Proxmox / Debian / Ubuntu]
python3 inventaire.py (stdlib uniquement, pas de venv)
└── HTTP POST http://10.0.0.50:8088/api/ingest
[nginx:alpine — port 8088]
├── /api/* → proxy → [FastAPI :8000]
└── / → static (HTML + design system)
SQLite (volume Docker nommé)
```
**2 conteneurs Docker** via `docker-compose.yml` :
- `api` : FastAPI (Python), port interne 8000
- `web` : nginx:alpine, port exposé **8088**, sert les fichiers statiques et proxifie `/api/*`
SQLite dans un volume nommé (`mes_hdd_db`).
---
## Script client — `inventaire.py`
### Contraintes
- Python 3 uniquement, **stdlib exclusively** (`subprocess`, `json`, `urllib.request`)
- Aucun `pip install`, aucun venv, aucune dépendance externe
- Fonctionne sur Proxmox, Debian, Ubuntu
- **Doit être exécuté en root** (ou via `sudo`) : `smartctl` et `pvs`/`lvs` nécessitent des droits élevés
- Tolérant aux erreurs : un disque ou une commande qui échoue ne bloque pas l'inventaire des autres
- Si `smartctl` est absent → `smart.status = "unavailable"`
- Si `pvs`/`lvs` sont absents ou LVM non installé → partitions LVM ignorées, `lvm: null`
### Commandes utilisées
| Commande | Usage |
|---|---|
| `lsblk -J -o NAME,TYPE,SIZE,MODEL,SERIAL,FSTYPE,MOUNTPOINT,ROTA` | Structure des disques et partitions |
| `smartctl -H -A -i /dev/sdX` | Santé SMART + attributs |
| `df -B1` | Espace utilisé/libre par point de montage |
| `pvs --noheadings --reportformat json` | Volumes physiques LVM |
| `vgs --noheadings --reportformat json` | Groupes de volumes LVM |
| `lvs --noheadings --reportformat json -o lv_name,vg_name,lv_size,lv_path,lv_dm_path` | Volumes logiques LVM |
| `udevadm info --query=property --name=/dev/sdX` | Bus (sata/usb/nvme) |
| `find /dev/disk/by-id -type l` | Lien by-id (sans partitions) |
| `hostname`, `ip route get 1.1.1.1` | Identité de la machine |
### Payload JSON envoyé
```json
{
"hostname": "pve1",
"ip": "10.0.0.10",
"collected_at": "2026-05-28T15:30:00+02:00",
"disks": [
{
"device": "sda",
"path": "/dev/sda",
"by_id": "ata-ST1000LM024_W1234567",
"model": "ST1000LM024 HN-M101MBB",
"serial": "W1234567",
"type": "HDD",
"capacity_bytes": 1000204886016,
"capacity_human": "1.0 To",
"bus": "sata",
"smart": {
"status": "ok",
"label": "Bon état",
"detail": "2 847h d'utilisation · 38°C · aucun secteur défectueux",
"temperature_c": 38,
"power_on_hours": 2847,
"reallocated_sectors": 0,
"pending_sectors": 0,
"uncorrectable_sectors": 0
},
"partitions": [
{
"name": "sda1",
"fstype": "ext4",
"size_bytes": 536870912000,
"size_human": "500 Go",
"used_bytes": 128849018880,
"used_human": "120 Go",
"free_bytes": 408021991120,
"free_human": "380 Go",
"used_percent": 24,
"mountpoint": "/",
"lvm": null
},
{
"name": "sda2",
"fstype": "LVM2_member",
"size_bytes": 536870912000,
"size_human": "500 Go",
"mountpoint": null,
"lvm": {
"vg_name": "vg_data",
"logical_volumes": [
{
"lv_name": "lv_home",
"size_human": "300 Go",
"used_human": "50 Go",
"free_human": "250 Go",
"used_percent": 17,
"fstype": "ext4",
"mountpoint": "/home"
},
{
"lv_name": "lv_swap",
"size_human": "8 Go",
"fstype": "swap",
"mountpoint": null
}
]
}
}
]
}
]
}
```
### SMART — labels en français
| Cas | status | label | Exemple de detail |
|---|---|---|---|
| Test global PASSED, pas d'attribut dégradé | `ok` | Bon état | "2 847h · 38°C · aucun secteur défectueux" |
| PASSED mais attributs FAILING_NOW / In_the_past | `warn` | Attention | "3 secteurs réalloués · disque à surveiller" |
| Test global FAILED | `fail` | Défaillance probable | "Prévoir le remplacement du disque" |
| smartctl absent ou inaccessible | `unavailable` | SMART indisponible | "smartctl non disponible ou accès refusé" |
Les champs numériques (`temperature_c`, `power_on_hours`, `reallocated_sectors`, `pending_sectors`, `uncorrectable_sectors`) sont inclus quand disponibles, `null` sinon.
---
## Backend — FastAPI
### Routes
| Méthode | Route | Description |
|---|---|---|
| `POST` | `/api/ingest` | Reçoit le payload du script client |
| `GET` | `/api/disks` | Liste tous les disques (dernière observation par serial) |
| `GET` | `/api/disks/{serial}` | Historique complet d'un disque |
| `GET` | `/api/machines` | Liste des machines avec last_seen |
| `GET` | `/api/ai/summary` | Synthèse structurée pour agents IA |
| `GET` | `/api/ai/at-risk` | Disques avec status warn ou fail |
### Logique d'ingest
À chaque POST `/api/ingest` :
1. Upsert dans `machines` (hostname, ip, last_seen)
2. Pour chaque disque du payload :
- Upsert dans `disks` (serial comme PK) — met à jour `last_host`, `last_seen`, `status`
- Si `last_host``first_host` → le mouvement est détecté (visible via l'API)
- Insert dans `snapshots` (historique immuable)
---
## Base de données — SQLite
### Table `disks`
| Colonne | Type | Description |
|---|---|---|
| `serial` | TEXT PK | Numéro de série (source de vérité) |
| `model` | TEXT | Modèle du disque |
| `type` | TEXT | HDD / SSD / NVMe / inconnu |
| `capacity_bytes` | INTEGER | Capacité en octets |
| `capacity_human` | TEXT | Capacité lisible |
| `first_seen_host` | TEXT | Première machine observée |
| `first_seen_at` | TEXT | Date première observation |
| `last_seen_host` | TEXT | Dernière machine observée |
| `last_seen_at` | TEXT | Date dernière observation |
| `smart_status` | TEXT | Dernier status SMART |
### Table `snapshots`
| Colonne | Type | Description |
|---|---|---|
| `id` | INTEGER PK AUTOINCREMENT | |
| `serial` | TEXT FK → disks | |
| `hostname` | TEXT | Machine au moment de la collecte |
| `device` | TEXT | ex: sda, nvme0n1 |
| `smart_status` | TEXT | ok / warn / fail / unavailable |
| `smart_label` | TEXT | Label français |
| `smart_detail` | TEXT | Détail lisible |
| `smart_raw_json` | TEXT | Attributs bruts (JSON) |
| `partitions_json` | TEXT | Partitions + LVM (JSON) |
| `collected_at` | TEXT | ISO 8601 |
### Table `machines`
| Colonne | Type | Description |
|---|---|---|
| `hostname` | TEXT PK | |
| `ip` | TEXT | |
| `last_seen` | TEXT | ISO 8601 |
---
## Frontend (étape 2)
Design system **Gruvbox Seventies** — React 18 via CDN + Babel standalone (pas de build step).
Fichiers statiques servis par nginx, localisés dans `frontend/` :
- `index.html` — point d'entrée, charge tokens.css + ui-kit.jsx via CDN/local
- `app.jsx` — composant principal React (chargé Babel)
Structure de page prévue :
- Colonne gauche : `TreeNav` des machines, avec nombre de disques et statut global
- Zone principale : cartes disques par machine
- `StatusLed` (ok/warn/err) + modèle + serial
- `BatteryGauge` espace par partition/LV
- Détail partitions + LVM dans un `Popup`
- Indicateur visuel de disque déplacé (serial apparu sur nouvelle machine)
---
## Docker Compose
```yaml
services:
api:
build: ./api
restart: unless-stopped
volumes:
- mes_hdd_db:/data
environment:
DB_PATH: /data/mes_hdd.db
expose:
- "8000"
web:
image: nginx:alpine
restart: unless-stopped
ports:
- "8088:80"
volumes:
- ./frontend:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
volumes:
mes_hdd_db:
```
nginx proxifie `/api/*` vers `api:8000`, sert `/` depuis les fichiers statiques.
---
## Lancement du script client
```bash
# Télécharger et exécuter (depuis Gitea ou autre dépôt)
curl -fsSL http://<gitea>/mes_hdd/raw/main/inventaire.py | python3
# Ou localement
python3 inventaire.py
```
Aucun argument requis. L'URL de l'API est configurable via variable d'environnement :
```bash
MES_HDD_API=http://10.0.0.50:8088 python3 inventaire.py
```
---
## Ce qui est hors scope (étape 2 ou plus tard)
- Authentification sur l'API
- Alertes automatiques (email, Telegram) en cas de SMART dégradé
- Scraping périodique automatique (cron distant)
- Interface mobile