diff --git a/ROADMAP.md b/ROADMAP.md index e61ed37..9c1ebc3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -30,14 +30,16 @@ - [x] Push vers /api/v1/metrics et /api/v1/events - [ ] `widget-agent-metrics` Glance — Phase 4 -## Phase 4 — UX & Personnalisation +## Phase 4 — Widgets Glance ✅ -- [ ] `widget-network-scan` Glance (tuile + popup) -- [ ] `widget-agent-metrics` Glance -- [ ] Popups détaillés widgets -- [ ] Filtres, tri, favoris -- [ ] Icônes locales (Heroicons / selfh.st) -- [ ] Personnalisation par équipement +- [x] `widget-network-scan` : liste équipements (état, IP, hostname, vendor, services, tri online/offline) +- [x] `widget-agent-metrics` : barres CPU/RAM/disque/température par agent, code couleur (ok/warn/crit) +- [x] CSS custom (`sentinelmesh.css`) : badges, barres de progression, points de statut +- [x] Page Glance exemple complète (`glance-page-example.yaml`) +- [x] Backend widgets enrichi : mac, ports, offline count, net_rx/tx_bps +- [ ] Popups détaillés — nécessite widget `extension` (serveur HTTP séparé) — Phase 4+ +- [ ] Icônes locales par type d'équipement — Phase 4+ +- [ ] Favoris / personnalisation par équipement — Phase 4+ ## Phase 5 — Déploiement & Distribution diff --git a/backend/src/routes/widgets.rs b/backend/src/routes/widgets.rs index 0e43d86..2d96ee0 100644 --- a/backend/src/routes/widgets.rs +++ b/backend/src/routes/widgets.rs @@ -13,45 +13,52 @@ pub struct NetworkWidget { pub last_scan_at: Option, pub total: usize, pub online: usize, + pub offline: usize, pub devices: Vec, } #[derive(Serialize)] pub struct DeviceSummary { - pub ip: String, - pub hostname: Option, - pub vendor: Option, - pub state: String, - pub services: Value, + pub ip: String, + pub hostname: Option, + pub vendor: Option, + pub mac: Option, + pub state: String, + pub services: Value, + pub open_ports: Value, + pub last_seen: String, } #[oapath(get, path = "/api/v1/widgets/network", responses((status = 200, description = "Données widget réseau pour Glance")))] pub async fn network(State(db): State) -> Result> { - let devices = sqlx::query_as::<_, Device>("SELECT * FROM devices ORDER BY state, ip") - .fetch_all(&db) - .await?; + // Online en premier, puis tri IP + let devices = sqlx::query_as::<_, Device>( + "SELECT * FROM devices ORDER BY CASE state WHEN 'online' THEN 0 ELSE 1 END, ip" + ) + .fetch_all(&db) + .await?; let online = devices.iter().filter(|d| d.state == "online").count(); + let offline = devices.len() - online; + let summary = devices .iter() .map(|d| DeviceSummary { - ip: d.ip.clone(), - hostname: d.hostname.clone(), - vendor: d.vendor.clone(), - state: d.state.clone(), - services: serde_json::from_str(&d.services).unwrap_or(Value::Array(vec![])), + ip: d.ip.clone(), + hostname: d.hostname.clone(), + vendor: d.vendor.clone(), + mac: d.mac.clone(), + state: d.state.clone(), + services: serde_json::from_str(&d.services).unwrap_or(Value::Array(vec![])), + open_ports: serde_json::from_str(&d.open_ports).unwrap_or(Value::Array(vec![])), + last_seen: d.last_seen.clone(), }) .collect(); let last_scan = devices.iter().map(|d| d.last_seen.clone()).max(); - Ok(Json(NetworkWidget { - last_scan_at: last_scan, - total: devices.len(), - online, - devices: summary, - })) + Ok(Json(NetworkWidget { last_scan_at: last_scan, total: devices.len(), online, offline, devices: summary })) } // --- Widget métriques --- @@ -66,10 +73,12 @@ pub struct AgentMetricSummary { pub agent_id: String, pub hostname: String, pub status: String, - pub cpu_percent: Option, - pub ram_percent: Option, - pub disk_percent: Option, - pub temperature_c: Option, + pub cpu_percent: f64, + pub ram_percent: f64, + pub disk_percent: f64, + pub temperature_c: f64, + pub net_rx_bps: i64, + pub net_tx_bps: i64, pub last_seen: String, } @@ -77,10 +86,15 @@ pub struct AgentMetricSummary { responses((status = 200, description = "Données widget métriques pour Glance")))] pub async fn metrics(State(db): State) -> Result> { let rows = sqlx::query_as::<_, ( - String, String, String, Option, Option, Option, Option, String, + String, String, String, + Option, Option, Option, Option, + Option, Option, + String, )>( - "SELECT a.id, a.hostname, a.status, m.cpu_percent, m.ram_percent, - m.disk_percent, m.temperature_c, a.last_seen + "SELECT a.id, a.hostname, a.status, + m.cpu_percent, m.ram_percent, m.disk_percent, m.temperature_c, + m.net_rx_bps, m.net_tx_bps, + a.last_seen FROM agents a LEFT JOIN metrics m ON m.agent_id = a.id WHERE a.agent_type = 'metric' ORDER BY a.hostname", @@ -90,9 +104,17 @@ pub async fn metrics(State(db): State) -> Result let agents = rows .into_iter() - .map(|(id, hostname, status, cpu, ram, disk, temp, last_seen)| AgentMetricSummary { - agent_id: id, hostname, status, cpu_percent: cpu, ram_percent: ram, - disk_percent: disk, temperature_c: temp, last_seen, + .map(|(id, hostname, status, cpu, ram, disk, temp, rx, tx, last_seen)| AgentMetricSummary { + agent_id: id, + hostname, + status, + cpu_percent: cpu.unwrap_or(0.0), + ram_percent: ram.unwrap_or(0.0), + disk_percent: disk.unwrap_or(0.0), + temperature_c: temp.unwrap_or(0.0), + net_rx_bps: rx.unwrap_or(0), + net_tx_bps: tx.unwrap_or(0), + last_seen, }) .collect(); diff --git a/widgets/glance-page-example.yaml b/widgets/glance-page-example.yaml new file mode 100644 index 0000000..ccaa5f9 --- /dev/null +++ b/widgets/glance-page-example.yaml @@ -0,0 +1,140 @@ +# Exemple de page Glance complète avec les deux widgets SentinelMesh +# À intégrer dans votre glance.yml +# +# Installation : +# 1. Copier sentinelmesh.css dans votre répertoire assets Glance +# 2. Référencer le CSS dans server: custom-css-file: /assets/sentinelmesh.css +# 3. Ajouter cette page dans votre glance.yml + +pages: + - name: Infrastructure + slug: infra + width: wide + columns: + - size: small + widgets: + # --- Métriques système --- + - type: custom-api + title: Métriques systèmes + cache: 5s + url: http://sentinelmesh:8080/api/v1/widgets/metrics + template: | +
    + {{ range .JSON.Array "agents" }} + {{- $cpu := .Float "cpu_percent" -}} + {{- $ram := .Float "ram_percent" -}} + {{- $disk := .Float "disk_percent" -}} + {{- $temp := .Float "temperature_c" -}} + {{- $up := eq (.String "status") "online" -}} + +
  • +
    + + {{ .String "hostname" }} + + +
    + + {{ if $up }} + +
    + CPU +
    +
    +
    + {{ printf "%.0f" $cpu }}% +
    + +
    + RAM +
    +
    +
    + {{ printf "%.0f" $ram }}% +
    + +
    + Disque +
    +
    +
    + {{ printf "%.0f" $disk }}% +
    + + {{ if gt $temp 0.0 }} +
    + Temp +
    +
    +
    + {{ printf "%.0f" $temp }}°C +
    + {{ end }} + + {{ else }} +

    Agent hors ligne

    + {{ end }} +
  • + {{ end }} +
+ + - size: full + widgets: + # --- Découverte réseau --- + - type: custom-api + title: Réseau local — 10.0.0.0/22 + cache: 30s + url: http://sentinelmesh:8080/api/v1/widgets/network + template: | + {{- $online := .JSON.Int "online" -}} + {{- $offline := .JSON.Int "offline" -}} + {{- $total := .JSON.Int "total" -}} + +
+ + {{ $online }} en ligne + {{ if gt $offline 0 }} · {{ $offline }} hors ligne{{ end }} +  ·  {{ $total }} total + + {{ if .JSON.Exists "last_scan_at" }} + + {{ end }} +
+ +
    + {{ range .JSON.Array "devices" }} + {{- $state := .String "state" -}} + {{- $host := .String "hostname" -}} + {{- $ip := .String "ip" -}} + {{- $vendor := .String "vendor" -}} +
  • + +
    + +
    +
    + + {{- if ne $host "" }}{{ $host }}{{ else }}{{ $ip }}{{ end -}} + + {{ $ip }} +
    + +
    + {{ if ne $vendor "" }} + {{ $vendor }} + {{ end }} + {{ range .Array "services" }} + {{ .String "" }} + {{ end }} +
    +
    + +
  • + {{ end }} +
diff --git a/widgets/sentinelmesh.css b/widgets/sentinelmesh.css new file mode 100644 index 0000000..e98be1f --- /dev/null +++ b/widgets/sentinelmesh.css @@ -0,0 +1,87 @@ +/* ============================================================ + SentinelMesh — CSS custom pour widgets Glance + Copier dans le répertoire assets de Glance et référencer : + custom-css-file: /assets/sentinelmesh.css + ============================================================ */ + +/* --- Widget réseau --- */ + +.sm-net-header { + align-items: center; +} + +.sm-device { + align-items: flex-start; + padding: 4px 0; +} + +/* Point de statut coloré */ +.sm-state-dot { + width: 8px; + height: 8px; + border-radius: 50%; + margin-top: 5px; + flex-shrink: 0; +} +.sm-dot-online { background: var(--color-positive, #4caf50); } +.sm-dot-offline { background: var(--color-negative, #f44336); opacity: 0.5; } + +/* Badges services / vendor */ +.sm-badge { + display: inline-block; + padding: 1px 6px; + border-radius: 4px; + background: var(--color-widget-content-background, rgba(255,255,255,0.06)); + color: var(--color-text-subdue, #aaa); + line-height: 1.6; +} +.sm-badge-vendor { + color: var(--color-primary, #7ca9d4); +} + +/* --- Widget métriques --- */ + +.sm-agent { + padding-bottom: 6px; + border-bottom: 1px solid var(--color-widget-content-background, rgba(255,255,255,0.06)); +} +.sm-agent:last-child { border-bottom: none; } + +.sm-metric-row { + display: flex; + align-items: center; + gap: 8px; + margin-top: 4px; +} + +.sm-metric-label { + width: 40px; + flex-shrink: 0; + text-align: right; +} + +.sm-metric-val { + width: 36px; + flex-shrink: 0; + text-align: right; +} + +/* Barre de progression */ +.sm-bar-track { + flex: 1; + height: 4px; + border-radius: 2px; + background: var(--color-widget-content-background, rgba(255,255,255,0.08)); + overflow: hidden; +} + +.sm-bar { + height: 100%; + border-radius: 2px; + min-width: 2px; + transition: width 0.4s ease; +} + +.sm-bar-ok { background: var(--color-positive, #4caf50); } +.sm-bar-warn { background: var(--color-base-500, #ff9800); } +.sm-bar-crit { background: var(--color-negative, #f44336); } diff --git a/widgets/widget-agent-metrics/widget.yaml b/widgets/widget-agent-metrics/widget.yaml new file mode 100644 index 0000000..0ecf012 --- /dev/null +++ b/widgets/widget-agent-metrics/widget.yaml @@ -0,0 +1,76 @@ +# Widget SentinelMesh — Métriques système +# À copier dans votre glance.yml +# +# Prérequis : +# - Backend SentinelMesh démarré (port 8080) +# - agent-metric en cours d'exécution sur chaque machine +# - Fichier CSS custom : assets/sentinelmesh.css + +- type: custom-api + title: Métriques systèmes + cache: 5s + url: http://sentinelmesh:8080/api/v1/widgets/metrics + template: | +
    + {{ range .JSON.Array "agents" }} + {{- $cpu := .Float "cpu_percent" -}} + {{- $ram := .Float "ram_percent" -}} + {{- $disk := .Float "disk_percent" -}} + {{- $temp := .Float "temperature_c" -}} + {{- $up := eq (.String "status") "online" -}} + +
  • +
    + + {{ .String "hostname" }} + + +
    + + {{ if $up }} + +
    + CPU +
    +
    +
    + {{ printf "%.0f" $cpu }}% +
    + +
    + RAM +
    +
    +
    + {{ printf "%.0f" $ram }}% +
    + +
    + Disque +
    +
    +
    + {{ printf "%.0f" $disk }}% +
    + + {{ if gt $temp 0.0 }} +
    + Temp +
    +
    +
    + {{ printf "%.0f" $temp }}°C +
    + {{ end }} + + {{ else }} +

    Agent hors ligne

    + {{ end }} +
  • + {{ end }} +
diff --git a/widgets/widget-network-scan/widget.yaml b/widgets/widget-network-scan/widget.yaml new file mode 100644 index 0000000..e020dd6 --- /dev/null +++ b/widgets/widget-network-scan/widget.yaml @@ -0,0 +1,61 @@ +# Widget SentinelMesh — Découverte réseau +# À copier dans votre glance.yml +# +# Prérequis : +# - Backend SentinelMesh démarré (port 8080) +# - agent-scan-network en cours d'exécution +# - Fichier CSS custom copié dans votre répertoire assets Glance +# et référencé dans glance.yml : custom-css-file: /assets/sentinelmesh.css + +- type: custom-api + title: Réseau local + cache: 30s + url: http://sentinelmesh:8080/api/v1/widgets/network + template: | + {{- $online := .JSON.Int "online" -}} + {{- $offline := .JSON.Int "offline" -}} + {{- $total := .JSON.Int "total" -}} + +
+ + {{ $online }} en ligne + {{ if gt $offline 0 }} · {{ $offline }} hors ligne{{ end }} +  ·  {{ $total }} total + + {{ if .JSON.Exists "last_scan_at" }} + + {{ end }} +
+ +
    + {{ range .JSON.Array "devices" }} + {{- $state := .String "state" -}} + {{- $host := .String "hostname" -}} + {{- $ip := .String "ip" -}} + {{- $vendor := .String "vendor" -}} +
  • + +
    + +
    +
    + + {{- if ne $host "" }}{{ $host }}{{ else }}{{ $ip }}{{ end -}} + + {{ $ip }} +
    + +
    + {{ if ne $vendor "" }} + {{ $vendor }} + {{ end }} + {{ range .Array "services" }} + {{ .String "" }} + {{ end }} +
    +
    + +
  • + {{ end }} +