From e0df6dc2d843338ffaa9de0dc88892690b90be0e Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Tue, 19 May 2026 07:06:32 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20historique=5Fmemoire.md=20=E2=80=94=20d?= =?UTF-8?q?ocumentation=20compl=C3=A8te=20du=20contexte=20projet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contient : stack, architecture, schéma BDD, routes API, configs agents, widgets Glance, CI/CD, bugs corrigés, références Gitea, règles de travail. Co-Authored-By: Claude Sonnet 4.6 --- historique_memoire.md | 442 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 384 insertions(+), 58 deletions(-) diff --git a/historique_memoire.md b/historique_memoire.md index 914d061..aceffe1 100644 --- a/historique_memoire.md +++ b/historique_memoire.md @@ -1,78 +1,404 @@ -# Historique et mémoire du projet SentinelMesh +# Historique et mémoire complète — SentinelMesh -## Références +## Références dépôt -- **Dépôt Gitea** : https://git.maison43gil.com/gilles/SentinelMesh.git +- **Gitea** : https://git.maison43gil.com/gilles/SentinelMesh.git - **Utilisateur** : gilles -- **Token** : 8bb9ee27860bd2f66c4113406dbcc0d545ba6ac6 -- **Push** : `git remote set-url origin "https://gilles:@git.maison43gil.com/gilles/SentinelMesh.git"` +- **Token PAT** : 8bb9ee27860bd2f66c4113406dbcc0d545ba6ac6 +- **Push** : + ```bash + git remote set-url origin "https://gilles:8bb9ee27860bd2f66c4113406dbcc0d545ba6ac6@git.maison43gil.com/gilles/SentinelMesh.git" + git push origin main + git remote set-url origin "https://git.maison43gil.com/gilles/SentinelMesh.git" + ``` --- ## Règles de travail -- Toutes les réponses, commentaires dans le code, et messages de commit sont **en français uniquement** -- Committer + pusher après chaque étape logique (pas seulement en fin de phase) -- Toujours utiliser le préfixe `rtk` pour les commandes shell (réduction des tokens) +- Toutes les réponses, commentaires dans le code et messages de commit sont **en français uniquement** +- Committer + pusher **après chaque étape logique**, pas seulement en fin de phase +- Toujours utiliser le préfixe `rtk` pour les commandes shell (ex: `rtk cargo build`, `rtk git status`) --- -## Historique des phases +## Stack technique -### Phase 1 — Architecture & Backend -- Workspace Cargo multi-membres (backend, agents/agent-scan-network, agents/agent-metric) -- Backend Axum 0.8 + SQLite via SQLx avec migrations -- Endpoints API v1 : `/agents`, `/network`, `/metrics`, `/events`, `/widgets` -- Spec OpenAPI générée sur `/api-docs/openapi.json` - -### Phase 2 — Découverte réseau -- `agent-scan-network` : ping sweep TCP, ARP via `/proc/net/arp`, lookup OUI -- Scan de ports TCP (SSH, HTTP, HTTPS, SMB, MQTT, Docker, Proxmox, HA…) -- API JSON locale sur `:9100`, push vers le backend -- Subnet configuré : `10.0.0.0/22` - -### Phase 3 — Métriques système -- `agent-metric` : CPU/RAM/réseau/charge chaque seconde (sysinfo 0.32) -- Disques, températures hwmon, SMART via smartctl (toutes les 30min) -- Infos DMI/hardware depuis `/sys` au boot + toutes les 12h -- API locale sur `:9101`, push vers `/api/v1/metrics` et `/api/v1/events` - -### Phase 4 — Widgets Glance -- `widget-network-scan` : liste équipements (état, IP, hostname, vendor, services) -- `widget-agent-metrics` : barres CPU/RAM/disque/température par agent -- CSS custom `sentinelmesh.css` : badges, barres de progression, points de statut -- Page Glance exemple complète (`glance-page-example.yaml`) - -### Phase 5 — Déploiement & Distribution -- `install/install.sh` : détection arch, téléchargement, config, systemd, enregistrement -- `install/uninstall.sh` : désinstallation propre -- Docker Compose production (healthcheck, réseau, volumes nommés) -- Dockerfiles agents multi-arch (amd64, arm64, armv7) -- Pipelines Gitea Actions : CI (check/clippy/fmt/test) + release multi-arch sur tag `v*` - -### Phase 6 — Extensions -- SSE temps réel : `GET /api/v1/stream` (events metrics + network) -- Historique métriques : `GET /api/v1/history/{agent_id}?hours=N`, rétention 7 jours -- Prometheus : `GET /metrics` format text/plain -- MQTT dans les deux agents (rumqttc, QoS 0/1) — topics realtime/medium/events/network/scan +| Couche | Technologies | +|--------|-------------| +| Backend | Rust, Axum 0.8, Tokio, Serde JSON, SQLx 0.8, SQLite | +| Agents | Rust, Tokio, sysinfo 0.32, rumqttc 0.24, serde_yaml | +| Widgets | HTML/JS vanilla, Glance `custom-api` | +| API | REST JSON `/api/v1/`, OpenAPI (utoipa 5), SSE, Prometheus | +| MQTT | rumqttc, QoS 0 (realtime) / QoS 1 (events) | +| Déploiement | Docker Compose, multi-arch: amd64, arm64, armv7 | +| CI/CD | Gitea Actions (GitHub Actions compatible), cross-rs | --- -## Bugs corrigés notables +## Structure du workspace Cargo -| Problème | Solution | -|---|---| -| utoipa-swagger-ui 8 incompatible avec axum 0.8 | Supprimé, OpenAPI JSON servi manuellement | -| `refresh(true)` supprimé en sysinfo 0.32 | `refresh()` sans argument | -| Edition 2024 requiert Rust 1.86+ | Dockerfile mis à jour | -| SQLite ne crée pas le fichier au premier démarrage | `create_if_missing(true)` dans db.rs | -| Dockerfile backend — contexte workspace mal configuré | Contexte depuis la racine, stubs membres du workspace | +``` +Cargo.toml # workspace, resolver = "2" +├── backend/ # sentinelmesh-backend +├── agents/agent-scan-network/ +└── agents/agent-metric/ +``` + +Dépendances partagées (workspace.dependencies) : tokio (features=full), serde (derive), serde_json, anyhow, tracing, tracing-subscriber (env-filter) --- -## État actuel (mai 2026) +## Backend (`backend/`) -- Toutes les 6 phases complètes et pushées -- Tag `v0.1.0` créé et pushé (déclenche le pipeline release multi-arch) -- Test de déploiement Docker réussi — backend opérationnel -- Fichiers locaux exclus du dépôt via `.gitignore` : `.claude/`, `doc_brainstorming/`, `repo_glance/`, `tokengite.md` +### Dépendances Cargo + +```toml +axum = "0.8" +sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "macros", "migrate"] } +tower-http = { version = "0.6", features = ["cors", "trace"] } +utoipa = { version = "5", features = ["axum_extras"] } +chrono = { version = "0.4", features = ["serde"] } +tokio-stream = { version = "0.1", features = ["sync"] } +futures = "0.3" +``` + +### Variables d'environnement + +| Variable | Défaut | Description | +|----------|--------|-------------| +| `DATABASE_URL` | `sqlite://sentinelmesh.sqlite` | Chemin SQLite | +| `LISTEN_ADDR` | `0.0.0.0:8080` | Adresse d'écoute | +| `RUST_LOG` | `info` | Niveau de log | + +### Architecture interne + +``` +src/ +├── main.rs # AppState::new(), router, listener +├── db.rs # SqliteConnectOptions::create_if_missing(true) + migrate!() +├── error.rs # AppError(anyhow::Error) → JSON {"error":"..."} +├── models.rs # Agent, Device, Metric, Event + structs Push* +├── state.rs # AppState { db, metrics_tx, network_tx } + FromRef for SqlitePool +└── routes/ + ├── mod.rs # api_router() — 14 routes + ├── health.rs # GET /api/v1/health → {"status":"ok","version":"0.1.0"} + ├── agents.rs # GET+POST /api/v1/agents, GET /api/v1/agents/{id} + ├── network.rs # GET+POST /api/v1/network, GET /api/v1/network/{ip} + ├── metrics.rs # GET+POST /api/v1/metrics, GET /api/v1/metrics/{agent_id} + ├── history.rs # GET /api/v1/history/{agent_id}?hours=N (max 168h, rétention 7j) + ├── events.rs # GET+POST /api/v1/events + ├── widgets.rs # GET /api/v1/widgets/network + /metrics + ├── sse.rs # GET /api/v1/stream — SSE metrics+network fusionnés + └── prometheus.rs # GET /metrics — format text/plain Prometheus +``` + +### Routes complètes + +| Méthode | Endpoint | Description | +|---------|----------|-------------| +| GET | `/api/v1/health` | Santé + version | +| GET/POST | `/api/v1/agents` | Liste / enregistrement | +| GET | `/api/v1/agents/{id}` | Détail agent | +| GET/POST | `/api/v1/network` | Équipements / push scan | +| GET | `/api/v1/network/{ip}` | Détail device | +| GET/POST | `/api/v1/metrics` | Métriques courantes / push | +| GET | `/api/v1/metrics/{agent_id}` | Métriques d'un agent | +| GET | `/api/v1/history/{agent_id}` | Historique (param: `?hours=24`) | +| GET/POST | `/api/v1/events` | Événements / push | +| GET | `/api/v1/widgets/network` | Widget Glance réseau | +| GET | `/api/v1/widgets/metrics` | Widget Glance métriques | +| GET | `/api/v1/stream` | SSE temps réel | +| GET | `/metrics` | Prometheus scrape | +| GET | `/api-docs/openapi.json` | Spec OpenAPI | + +### Schéma SQLite (`migrations/`) + +**001_init.sql** — Tables principales : +- `agents` (id PK, hostname, agent_type CHECK IN ('scan-network','metric'), ip, version, status CHECK IN ('online','offline'), last_seen, created_at) +- `devices` (ip PK, mac, hostname, vendor, state CHECK IN ('online','offline','sleep','unknown'), services JSON, open_ports JSON, last_seen, first_seen) +- `metrics` (agent_id PK REFERENCES agents, timestamp, cpu_percent, ram_percent, load_avg, temperature_c, disk_percent, net_rx_bps, net_tx_bps, extra JSON) — 1 ligne par agent, écrasée à chaque push +- `events` (id AUTOINCREMENT, agent_id, event_type, timestamp, data JSON) +- Index : idx_events_agent, idx_events_ts, idx_devices_state + +**002_history.sql** — Série temporelle : +- `metrics_history` (id AUTOINCREMENT, agent_id, timestamp, cpu_percent, ram_percent, disk_percent, temperature_c, net_rx_bps, net_tx_bps) +- Index : idx_mh_agent_ts ON (agent_id, timestamp DESC) +- Rétention 7 jours, purge probabiliste 1/60 (~1 fois/minute avec push à 1s) + +### Pattern AppState / FromRef + +```rust +// state.rs — permet aux routes State de coexister avec State +impl FromRef for SqlitePool { + fn from_ref(state: &AppState) -> Self { state.db.clone() } +} +// Canaux broadcast SSE : capacité 128 messages +let (metrics_tx, _) = broadcast::channel(128); +let (network_tx, _) = broadcast::channel(128); +``` + +### SSE (`routes/sse.rs`) + +- Fusionne deux `BroadcastStream` (metrics + network) via `StreamExt::merge()` +- Événements nommés : `metrics` et `network` +- `Sse::new(merged).keep_alive(KeepAlive::default())` +- Client JS : `new EventSource("/api/v1/stream")` puis `es.addEventListener("metrics", ...)` + +### Prometheus (`routes/prometheus.rs`) + +- Format text/plain manuel (pas de crate externe) +- Header `text/plain; version=0.0.4; charset=utf-8` +- Métriques : `sentinelmesh_cpu_percent`, `ram_percent`, `disk_percent`, `temperature_c`, `net_rx_bps`, `net_tx_bps` +- Labels : `agent="",hostname=""` +- Compatible `prometheus.yml` : `targets: ['sentinelmesh:8080']` + +--- + +## Agent scan-network (`agents/agent-scan-network/`) + +### Dépendances supplémentaires + +```toml +serde_yaml = "*" +reqwest = { version = "0.12", features = ["json"] } +chrono = "0.4" +ipnetwork = "0.20" +rumqttc = "0.24" +``` + +### Modules + +``` +src/ +├── main.rs # boucle scan + push MQTT +├── config.rs # Config: backend, agent, scan, api, mqtt: Option +├── scanner.rs # scan_all(), ping(), read_arp_table(), scan_ports(), detect_services() +├── oui.rs # LazyLock> ~70 préfixes OUI +├── backend.rs # BackendClient: register(), push_devices(); local_ip() via UDP 8.8.8.8:80 +├── api.rs # SharedState = Arc>>, GET /devices sur :9100 +└── mqtt.rs # publish_scan() → sentinelmesh//network/scan (QoS 0) +``` + +### Configuration (`config.example.yaml`) + +```yaml +backend: + url: http://localhost:8080 + token: "" +agent: + id: "" # auto: scan- + hostname: "" # auto: /etc/hostname +scan: + subnets: [10.0.0.0/22] + interval_seconds: 60 + ping_timeout_ms: 1000 + service_timeout_ms: 300 + concurrency: 50 + ports: [22, 80, 443, 445, 2049, 1883, 2375, 8006, 8123, 3000, 9090, 9100] +api: + listen: "0.0.0.0:9100" +mqtt: + enabled: false + broker: "localhost" + port: 1883 + topic_prefix: "sentinelmesh" + client_id: "" # auto: sentinelmesh-scan- +``` + +### Technique ping sans root + +Pas de ICMP (nécessite CAP_NET_RAW) — utilise des connexions TCP sur ports 80/22/443/8080 avec timeout configurable. Combiné avec la lecture de `/proc/net/arp` pour les hosts silencieux. + +### Services détectés sur ports + +22→SSH, 80→HTTP, 443→HTTPS, 445→SMB, 2049→NFS, 1883→MQTT, 2375→Docker-API, 8006→Proxmox, 8123→Home-Assistant, 3000→Grafana, 9090→Prometheus, 9100→Node-Exporter + +### MQTT publié + +Topic : `sentinelmesh//network/scan` +Payload : `{"total": N, "online": M, "devices": [...]}` + +--- + +## Agent metric (`agents/agent-metric/`) + +### Dépendances supplémentaires + +```toml +serde_yaml = "*" +reqwest = { version = "0.12", features = ["json"] } +chrono = "0.4" +sysinfo = "0.32" +rumqttc = "0.24" +``` + +### Modules + +``` +src/ +├── main.rs # tokio::select! rt_ticker + med_ticker +├── config.rs # Config: backend, agent, intervals, api, mqtt +├── backend.rs # push_realtime/medium/static/event +├── api.rs # AgentState {hardware,realtime,medium}, GET /metrics sur :9101 +├── mqtt.rs # publish_realtime/medium/event +└── collectors/ + ├── realtime.rs # CPU, RAM, réseau, charge (toutes les 1s) + ├── medium.rs # disques, hwmon, SMART (toutes les 30min) + └── static_info.rs # DMI, CPU model, RAM total (boot + 12h) +``` + +### Fréquences de collecte + +| Intervalle | Données | +|-----------|---------| +| 1s (realtime_ms: 1000) | CPU %, RAM %, débit réseau rx/tx bps, load avg | +| 30min (medium_s: 1800) | Disques usage %, températures hwmon, SMART | +| Boot + 12h | hostname, DMI, CPU model/cores, RAM total, interfaces réseau, BIOS, OS | +| Instantané | Événements boot, shutdown, sleep, wake | + +### Sources système + +- **CPU/RAM** : `sysinfo::System::new_all()`, `refresh_cpu_usage()`, `refresh_memory()` +- **Réseau** : `sysinfo::Networks::new_with_refreshed_list()`, delta octets/s +- **Disques** : `sysinfo::Disks::new_with_refreshed_list()`, `refresh()` (sans args en 0.32) +- **Températures** : `/sys/class/hwmon/hwmonN/tempN_input` (divisé par 1000 → °C) +- **SMART** : subprocess `smartctl -H /dev/sdX` → status ok/warn/unknown +- **DMI** : `/sys/devices/virtual/dmi/id/` (board_name, sys_vendor, bios_version, etc.) +- **hostname** : `/etc/hostname` + +### MQTT topics + +``` +sentinelmesh//metrics/realtime (JSON, 1s, QoS 0) +sentinelmesh//metrics/medium (JSON, 30min, QoS 1) +sentinelmesh//events (JSON, boot/shutdown…, QoS 1) +``` + +--- + +## Widgets Glance (`widgets/`) + +### widget-network-scan + +- Type Glance : `custom-api` +- Endpoint : `http://sentinelmesh/api/v1/widgets/network` +- Cache recommandé : `30s` +- Affichage : état (point coloré), IP, hostname, vendor, services (badges), tri online/offline +- CSS classes : `sm-state-dot`, `sm-badge`, `collapsible-container` (collapse après 8 items) + +### widget-agent-metrics + +- Type Glance : `custom-api` +- Endpoint : `http://sentinelmesh/api/v1/widgets/metrics` +- Cache recommandé : `1s` +- Barres avec seuils couleur : + - CPU : ok <60%, warn <85%, crit ≥85% + - RAM : ok <70%, warn <90%, crit ≥90% + - Disque : ok <75%, warn <90%, crit ≥90% + - Temp : ok <70°C, warn <85°C, crit ≥85°C + +### CSS custom (`sentinelmesh.css`) + +- `.sm-state-dot` : point coloré (online = var(--color-positive), offline = opacité 0.5) +- `.sm-badge` : badge texte service +- `.sm-bar-track` / `.sm-bar` : barre animée avec transition CSS +- `.sm-bar-ok/warn/crit` : couleurs selon seuil + +--- + +## Déploiement + +### Docker Compose production (`docker-compose.yml`) + +```yaml +context: . # racine workspace (IMPORTANT) +dockerfile: backend/Dockerfile +ports: ["8080:8080"] +volumes: [sentinelmesh-data:/data] +environment: + DATABASE_URL: sqlite:///data/sentinelmesh.sqlite + LISTEN_ADDR: 0.0.0.0:8080 +healthcheck: wget -qO- http://localhost:8080/api/v1/health +restart: unless-stopped +``` + +### Dockerfile backend + +- Image base : `rust:1.86-alpine` (édition 2024 + dépendances requièrent ≥1.86) +- Pattern layer cache : stubs `fn main(){}` pour tous les membres workspace → build deps → copie sources réelles → rebuild +- Image finale : `alpine:3.21`, mkdir `/data`, binary + migrations copiés + +### Agents Dockerfiles + +- Multi-arch via `ARG TARGETPLATFORM` + `case` shell → target Rust musl +- `agent-scan-network` : image finale alpine + iputils (ping) +- `agent-metric` : image finale alpine + smartmontools ; nécessite `--privileged` pour `/sys` + +### Script install (`install/install.sh`) + +Usage : `curl -fsSL http://:8080/install.sh | sudo bash -s -- --server URL --token TOKEN --agent-type TYPE` + +Étapes : +1. Détection arch (x86_64/aarch64/armv7l) → téléchargement binaire depuis Gitea releases +2. Création config YAML dans `/etc/sentinelmesh/.yaml` (chmod 600) +3. Création service systemd `/etc/systemd/system/sentinelmesh-.service` + - `agent-scan-network` : `AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN` +4. `daemon-reload + enable + restart` +5. Enregistrement POST `/api/v1/agents` + +--- + +## CI/CD Gitea Actions + +### `.gitea/workflows/ci.yaml` (sur push/PR main) + +- `cargo check --workspace` +- `cargo clippy --workspace -- -D warnings` +- `cargo fmt --all -- --check` +- `cargo test --workspace` (DATABASE_URL: sqlite://:memory:) + +### `.gitea/workflows/release.yaml` (sur tag `v*`) + +- Matrix 3 targets : x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, armv7-unknown-linux-gnueabihf +- Build via `cross-rs` (cross build --release) +- 9 binaires produits : backend + agent-scan-network + agent-metric × 3 archs +- Release Gitea créée automatiquement avec tableau markdown des binaires + +--- + +## Bugs corrigés (référence) + +| Bug | Cause | Solution | +|----|-------|----------| +| `Router: From` trait error | utoipa-swagger-ui 8 incompatible axum 0.8 | Supprimé, JSON servi manuellement | +| `refresh(true)` ne compile pas | API sysinfo 0.32 changée | `refresh()` sans argument | +| `edition2024` non supportée | Rust 1.82 trop vieux | Dockerfile → rust:1.86-alpine | +| `idna_adapter requires rustc 1.86` | Dépendance transitive | Rust 1.86 requis | +| SQLite "unable to open database file" | Fichier non créé au premier démarrage | `SqliteConnectOptions::create_if_missing(true)` | +| Build Docker échoue (workspace) | Contexte `./backend` sans Cargo.lock racine | Contexte `.` (racine), stubs workspace | +| `/data` introuvable dans container | Répertoire non créé dans image | `mkdir -p /data` dans Dockerfile | +| Commit parasite pushé sur Gitea | Fichiers `.claude/skills/` non gitignorés | Reset + force push + `.gitignore` étendu | +| Push HTTPS échoue (caractère spécial) | Mot de passe avec `*` dans l'URL | Utilisation d'un token PAT à la place | +| `publish_device_online` warning | Méthode jamais appelée | `#[allow(dead_code)]` | +| Prometheus format string error | `{}` positionnel sans argument | Nommé : `{name}` | + +--- + +## État au moment de la rédaction (mai 2026) + +- Toutes les 6 phases complètes et pushées sur Gitea +- Tag `v0.1.0` créé → pipeline release multi-arch déclenché +- Test de déploiement Docker réussi : backend opérationnel, endpoints validés +- Fichiers exclus du dépôt via `.gitignore` : `.claude/`, `doc_brainstorming/`, `repo_glance/`, `consigne_claude_project_sentinelmesh_md.md` +- `tokengite.md` versionné (token en clair, choix explicite de l'utilisateur) + +## Fonctionnalités futures (ROADMAP déferrées) + +- Home Assistant MQTT auto-discovery +- PostgreSQL (SQLite suffisant en homelab) +- WebSocket bidirectionnel +- InfluxDB / Grafana direct +- Popups détaillés Glance (nécessite widget `extension` avec serveur HTTP séparé) +- Icônes locales par type d'équipement +- Résolution DNS inverse (PTR)