Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0df6dc2d8 | |||
| c4f672bb69 | |||
| 27c62fa54c |
+18
-5
@@ -1,13 +1,26 @@
|
||||
FROM rust:1.82-alpine AS builder
|
||||
FROM rust:1.86-alpine AS builder
|
||||
RUN apk add --no-cache musl-dev
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock* ./
|
||||
COPY src ./src
|
||||
RUN cargo build --release
|
||||
# Workspace
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY backend/Cargo.toml ./backend/Cargo.toml
|
||||
COPY agents/agent-scan-network/Cargo.toml ./agents/agent-scan-network/Cargo.toml
|
||||
COPY agents/agent-metric/Cargo.toml ./agents/agent-metric/Cargo.toml
|
||||
# Stubs pour cacher les dépendances
|
||||
RUN mkdir -p backend/src agents/agent-scan-network/src agents/agent-metric/src \
|
||||
&& echo 'fn main(){}' > backend/src/main.rs \
|
||||
&& echo 'fn main(){}' > agents/agent-scan-network/src/main.rs \
|
||||
&& echo 'fn main(){}' > agents/agent-metric/src/main.rs \
|
||||
&& cargo build --release -p sentinelmesh-backend
|
||||
# Sources réelles (les stubs agents restent en place)
|
||||
COPY backend/src ./backend/src
|
||||
COPY backend/migrations ./backend/migrations
|
||||
RUN touch backend/src/main.rs && cargo build --release -p sentinelmesh-backend
|
||||
|
||||
FROM alpine:3.21
|
||||
RUN apk add --no-cache ca-certificates
|
||||
RUN apk add --no-cache ca-certificates && mkdir -p /data
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/target/release/sentinelmesh-backend .
|
||||
COPY --from=builder /app/backend/migrations ./migrations
|
||||
EXPOSE 8080
|
||||
CMD ["./sentinelmesh-backend"]
|
||||
|
||||
+6
-2
@@ -1,9 +1,13 @@
|
||||
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
|
||||
use sqlx::{sqlite::{SqliteConnectOptions, SqlitePoolOptions}, SqlitePool};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub async fn connect(database_url: &str) -> anyhow::Result<SqlitePool> {
|
||||
let opts = SqliteConnectOptions::from_str(database_url)?
|
||||
.create_if_missing(true);
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(database_url)
|
||||
.connect_with(opts)
|
||||
.await?;
|
||||
|
||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
dockerfile: backend/Dockerfile
|
||||
image: sentinelmesh-backend:latest
|
||||
container_name: sentinelmesh-backend
|
||||
ports:
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
# Historique et mémoire complète — SentinelMesh
|
||||
|
||||
## Références dépôt
|
||||
|
||||
- **Gitea** : https://git.maison43gil.com/gilles/SentinelMesh.git
|
||||
- **Utilisateur** : gilles
|
||||
- **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 (ex: `rtk cargo build`, `rtk git status`)
|
||||
|
||||
---
|
||||
|
||||
## Stack technique
|
||||
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## Structure du workspace Cargo
|
||||
|
||||
```
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
## Backend (`backend/`)
|
||||
|
||||
### 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<AppState> 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<SqlitePool> de coexister avec State<AppState>
|
||||
impl FromRef<AppState> 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="<id>",hostname="<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<MqttConfig>
|
||||
├── scanner.rs # scan_all(), ping(), read_arp_table(), scan_ports(), detect_services()
|
||||
├── oui.rs # LazyLock<HashMap<&str,&str>> ~70 préfixes OUI
|
||||
├── backend.rs # BackendClient: register(), push_devices(); local_ip() via UDP 8.8.8.8:80
|
||||
├── api.rs # SharedState = Arc<RwLock<Vec<DiscoveredDevice>>>, GET /devices sur :9100
|
||||
└── mqtt.rs # publish_scan() → sentinelmesh/<host>/network/scan (QoS 0)
|
||||
```
|
||||
|
||||
### Configuration (`config.example.yaml`)
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
url: http://localhost:8080
|
||||
token: ""
|
||||
agent:
|
||||
id: "" # auto: scan-<hostname>
|
||||
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-<hostname>
|
||||
```
|
||||
|
||||
### 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/<hostname>/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/<hostname>/metrics/realtime (JSON, 1s, QoS 0)
|
||||
sentinelmesh/<hostname>/metrics/medium (JSON, 30min, QoS 1)
|
||||
sentinelmesh/<hostname>/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://<backend>: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/<type>.yaml` (chmod 600)
|
||||
3. Création service systemd `/etc/systemd/system/sentinelmesh-<type>.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<SwaggerUi>` 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)
|
||||
Reference in New Issue
Block a user