diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c1cf69c Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3395e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +__pycache__/ +*.pyc +.venv/ +venv/ +data/config.json +data/alerts.json +*.log +.env +imput/ +logTest/ +log/ +CLAUDE.md +docs/ +.claude/ +*.spec +build/ +dist/ +docs/ diff --git a/Cargo.toml b/Cargo.toml index 0c96db3..f11d9a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tower-sessions = { version = "0.12", features = ["memory-store"] } tower = "0.4" -tower_governor = "0.4" tower-http = { version = "0.5", features = ["fs"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/src/main.rs b/src/main.rs index 8474808..3ecf2b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,271 @@ mod alerter; mod user_monitor; mod routes; -fn main() { - println!("Supervision"); +use std::sync::Arc; +use tokio::sync::Mutex as AsyncMutex; +use tower_sessions::{MemoryStore, SessionManagerLayer}; +use tower_http::services::ServeDir; +use axum::{ + Router, + routing::{get, post}, + http::HeaderValue, +}; + +use config::ConfigManager; +use monitor::SystemMonitor; +use alerter::Alerter; +use user_monitor::UserMonitor; +use routes::{ + AppState, + auth::{login_get, login_post, logout}, + dashboard::{dashboard, api_metrics, toggle_monitoring}, + settings::{ + settings_get, update_thresholds, update_monitoring, update_smtp, + test_smtp, update_processes, update_password, update_port, + update_amadea_log_path, update_user_thresholds, + }, + alerts::{alerts_get, clear_alerts}, + users::{users_get, api_users, api_users_weekly}, +}; + +pub async fn run_server() { + // Init config + let config_manager = Arc::new(AsyncMutex::new(ConfigManager::new())); + + // Init services + let alerter = Arc::new(Alerter); + let monitor = Arc::new(SystemMonitor::new( + config_manager.clone(), + alerter.clone(), + )); + let user_monitor = Arc::new(UserMonitor::new(config_manager.clone())); + + // Démarrer monitoring et user monitor + monitor.clone().start().await; + { + let m = monitor.clone(); + let _ = m.collect().await; + } + user_monitor.clone().start().await; + + // App state + let state = AppState::new( + config_manager.clone(), + monitor, + alerter, + user_monitor, + ); + + // Sessions + let session_store = MemoryStore::default(); + let session_layer = SessionManagerLayer::new(session_store) + .with_secure(false) + .with_name("supervision_session"); + + let app = Router::new() + .route("/login", get(login_get)) + .route("/login", post(login_post)) + .route("/logout", get(logout)) + .route("/", get(dashboard)) + .route("/api/metrics", get(api_metrics)) + .route("/api/monitoring/toggle", post(toggle_monitoring)) + .route("/settings", get(settings_get)) + .route("/settings/thresholds", post(update_thresholds)) + .route("/settings/monitoring", post(update_monitoring)) + .route("/settings/smtp", post(update_smtp)) + .route("/settings/smtp/test", post(test_smtp)) + .route("/settings/processes", post(update_processes)) + .route("/settings/password", post(update_password)) + .route("/settings/port", post(update_port)) + .route("/settings/amadea-log-path", post(update_amadea_log_path)) + .route("/settings/user-thresholds", post(update_user_thresholds)) + .route("/alerts", get(alerts_get)) + .route("/alerts/clear", post(clear_alerts)) + .route("/users", get(users_get)) + .route("/api/users", get(api_users)) + .route("/api/users/activity/weekly", get(api_users_weekly)) + .nest_service("/static", ServeDir::new("static")) + .layer(session_layer) + .with_state(state.clone()) + .layer(axum::middleware::map_response(|mut response: axum::response::Response| async move { + let headers = response.headers_mut(); + headers.insert("X-Content-Type-Options", HeaderValue::from_static("nosniff")); + headers.insert("X-Frame-Options", HeaderValue::from_static("DENY")); + headers.insert("X-XSS-Protection", HeaderValue::from_static("1; mode=block")); + response + })); + + let port = { + let cm = state.config_manager.lock().await; + cm.config.port + }; + + let addr = format!("0.0.0.0:{}", port); + tracing::info!("Supervision démarré sur http://localhost:{}", port); + + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + let args: Vec = std::env::args().collect(); + + #[cfg(windows)] + { + if args.get(1).map(|s| s.as_str()) == Some("install") { + service::install_service(); + return; + } + if args.get(1).map(|s| s.as_str()) == Some("uninstall") { + service::uninstall_service(); + return; + } + if service::is_running_as_service() { + service::run_service(); + return; + } + } + + #[cfg(not(windows))] + let _ = args; + + run_server().await; +} + +#[cfg(windows)] +mod service { + use std::ffi::OsString; + use std::time::Duration; + use windows_service::{ + define_windows_service, + service::{ + ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, + ServiceExitCode, ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher, + service_manager::{ServiceManager, ServiceManagerAccess}, + }; + + const SERVICE_NAME: &str = "Supervision"; + const SERVICE_DISPLAY: &str = "Supervision - Monitoring Système"; + const SERVICE_DESCRIPTION: &str = + "Surveille CPU, RAM, disques et processus. Interface web sur http://localhost:5000"; + + pub fn install_service() { + let manager = ServiceManager::local_computer( + None::<&str>, + ServiceManagerAccess::CREATE_SERVICE, + ) + .expect("Impossible d'ouvrir le Service Manager (lancer en administrateur)"); + + let exe_path = std::env::current_exe().unwrap(); + + let service_info = ServiceInfo { + name: OsString::from(SERVICE_NAME), + display_name: OsString::from(SERVICE_DISPLAY), + service_type: ServiceType::OWN_PROCESS, + start_type: ServiceStartType::AutoStart, + error_control: ServiceErrorControl::Normal, + executable_path: exe_path, + launch_arguments: vec![], + dependencies: vec![], + account_name: None, + account_password: None, + }; + + let service = manager + .create_service(&service_info, ServiceAccess::CHANGE_CONFIG) + .expect("Impossible de créer le service"); + + service + .set_description(SERVICE_DESCRIPTION) + .expect("Impossible de définir la description"); + + println!("Service '{}' installé avec succès.", SERVICE_NAME); + println!("Démarrer avec: sc start {}", SERVICE_NAME); + } + + pub fn uninstall_service() { + let manager = ServiceManager::local_computer( + None::<&str>, + ServiceManagerAccess::CONNECT, + ) + .expect("Impossible d'ouvrir le Service Manager"); + + let service = manager + .open_service(SERVICE_NAME, ServiceAccess::DELETE) + .expect("Service introuvable"); + + service.delete().expect("Impossible de supprimer le service"); + println!("Service '{}' désinstallé.", SERVICE_NAME); + } + + pub fn is_running_as_service() -> bool { + // Heuristique : variable SESSIONNAME absente = pas de session interactive + std::env::var("SESSIONNAME").is_err() + } + + define_windows_service!(ffi_service_main, service_main); + + fn service_main(_arguments: Vec) { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>(); + + let mut shutdown_tx = Some(shutdown_tx); + let status_handle = service_control_handler::register( + SERVICE_NAME, + move |control| match control { + ServiceControl::Stop => { + if let Some(tx) = shutdown_tx.take() { + let _ = tx.send(()); + } + ServiceControlHandlerResult::NoError + } + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + }, + ) + .unwrap(); + + status_handle + .set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }) + .unwrap(); + + tokio::select! { + _ = crate::run_server() => {}, + _ = shutdown_rx => {}, + } + + status_handle + .set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }) + .unwrap(); + }); + } + + pub fn run_service() { + service_dispatcher::start(SERVICE_NAME, ffi_service_main) + .expect("Impossible de démarrer le service dispatcher"); + } } diff --git a/src/user_monitor.rs b/src/user_monitor.rs index 100b588..ee1f9c9 100644 --- a/src/user_monitor.rs +++ b/src/user_monitor.rs @@ -1,4 +1,4 @@ -use chrono::{Duration, Local, NaiveDateTime}; +use chrono::{Duration, Local, NaiveDateTime, Timelike}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..78104a9 --- /dev/null +++ b/static/style.css @@ -0,0 +1,48 @@ +/* Supervision — Style */ + +body { + background-color: #f4f6f9; + font-size: 0.9rem; +} + +.metric-card { + transition: border-color 0.3s; +} + +.metric-card .metric-value { + font-size: 2.2rem; + font-weight: 700; + line-height: 1.1; +} + +.card { + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.badge { + font-size: 0.75rem; + text-transform: uppercase; +} + +.table th { + font-size: 0.8rem; + text-transform: uppercase; + color: #6c757d; + border-bottom-width: 1px; +} + +.navbar-brand i { + color: #4fc3f7; +} + +/* Statut couleurs */ +.border-success { border-left: 4px solid #198754 !important; } +.border-warning { border-left: 4px solid #ffc107 !important; } +.border-danger { border-left: 4px solid #dc3545 !important; } + +/* Responsive */ +@media (max-width: 768px) { + .metric-card .metric-value { + font-size: 1.8rem; + } +} diff --git a/templates/alerts.html b/templates/alerts.html new file mode 100644 index 0000000..2325caf --- /dev/null +++ b/templates/alerts.html @@ -0,0 +1,60 @@ +{% extends "base.html" %} +{% block title %}Supervision - Alertes{% endblock %} + +{% block content %} +
+

Historique des alertes

+ {% if alerts %} +
+ +
+ {% endif %} +
+ +{% if not alerts %} +
+ Aucune alerte enregistrée. +
+{% else %} +
+
+ + + + + + + + + + + + {% for alert in alerts %} + + + + + + + + {% endfor %} + +
DateTypeMessageValeurSeuil
+ {{ alert.timestamp_display }} + + {% if alert.type == "process_down" %} + Processus + {% else %} + Seuil + {% endif %} + {{ alert.message }}{{ alert.value }}{{ alert.threshold }}
+
+
+
+ {{ alerts | length }} alerte(s) — les 500 dernières sont conservées. +
+{% endif %} +{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..59d2082 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,80 @@ + + + + + + {% block title %}Supervision{% endblock %} + + + + + + {% if is_authenticated %} + + {% endif %} + +
+ {% if flash_messages %} + {% for item in flash_messages %} + + {% endfor %} + {% endif %} + + {% if default_pw is defined and default_pw %} +
+ + Sécurité : Le mot de passe par défaut est encore actif. + Changez-le maintenant. +
+ {% endif %} + + {% block content %}{% endblock %} +
+ + + {% block scripts %}{% endblock %} + + diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..95b12d5 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,243 @@ +{% extends "base.html" %} +{% block title %}Supervision - Tableau de bord{% endblock %} + +{% block content %} +
+

+ Tableau de bord + {% if metrics and metrics.hostname %} + — {{ metrics.hostname }} + {% endif %} +

+
+ +
+ {% if metrics and metrics.monitoring_active %} + + {% else %} + + {% endif %} +
+
+
+ +{% if not metrics %} +
+ Collecte des métriques en cours... +
+{% else %} + + +
+
+
+
+ {{ metrics.hostname }} + {{ metrics.os }} + Uptime: {{ metrics.uptime }} + {{ metrics.cpu.cores }} cœurs + {{ metrics.ram.total_gb }} Go RAM +
+
+
+
+ + +
+ +
+
+
+
+
CPU
+ {{ metrics.cpu.status }} +
+
{{ metrics.cpu.percent }}%
+
+
+
+ Seuil: {{ metrics.cpu.threshold }}% +
+
+
+ +
+
+
+
+
RAM
+ {{ metrics.ram.status }} +
+
{{ metrics.ram.percent }}%
+
+
+
+ + {{ metrics.ram.used_gb }} / + {{ metrics.ram.total_gb }} Go + — Seuil: {{ metrics.ram.threshold }}% + +
+
+
+ + {% for disk in metrics.disks %} +
+
+
+
+
{{ disk.drive }}
+ {{ disk.status }} +
+
{{ disk.percent }}%
+
+
+
+ + {{ disk.used_gb }} / {{ disk.total_gb }} Go + ({{ disk.free_gb }} Go libres) + — Seuil: {{ disk.threshold }}% + +
+
+
+ {% endfor %} +
+ + +
+
+
+
+
Processus surveillés
+
+
+ + + + + + + + + + + + + {% for proc in metrics.processes %} + + + + + + + + + {% endfor %} + +
ProcessusStatutInstancesMémoireCPUPID(s)
+ {{ proc.name }} +
pattern: {{ proc.pattern }} +
+ {% if not proc.enabled %} + Désactivé + {% elif proc.running %} + Actif + {% else %} + Arrêté + {% endif %} + {{ proc.instance_count }} + {{ proc.total_memory_mb }} Mo + {% if proc.memory_threshold_mb > 0 %} +
seuil: {{ proc.memory_threshold_mb }} Mo + {% endif %} +
{{ proc.total_cpu_percent }}%{{ proc.pids | join(sep=", ") }}
+
+
+
+
+ + +
+
+
+
+
Alertes récentes
+ Voir tout +
+
+ + + + +
+
+ Aucune alerte récente. +
+
+
+
+
+ +{% endif %} +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..7b84476 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,63 @@ + + + + + + Supervision - Connexion + + + + + +
+ +
+ + diff --git a/templates/settings.html b/templates/settings.html new file mode 100644 index 0000000..c446d89 --- /dev/null +++ b/templates/settings.html @@ -0,0 +1,320 @@ +{% extends "base.html" %} +{% block title %}Supervision - Configuration{% endblock %} + +{% block content %} +

Configuration

+ +
+
+
+
Seuils d'alerte (%)
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ +
+
+
Fréquence et alertes
+
+
+
+ + +
+
+ + +
Délai minimum entre deux alertes du même type.
+
+ +
+
+
+ +
+
Port de l'application
+
+
+
+ + +
+ +
+
+
+
+
+ + +
+
+
+
Configuration SMTP
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+
+ + +
Laissez vide pour conserver le mot de passe actuel.
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+
+
+
+ + +
+
+
+
Processus surveillés
+
+
+ + + + + + + + + {% for proc in config.processes %} + + + + + + + + + {% endfor %} + +
NomPatternSeuil mémoire (Mo)ActifAlerte si arrêté
+ +
0 = pas de seuil
+
+
+ +
+
+
+ +
+
+ +
+
+ + +
+
+
+
+
+
+ + +
+
+
+
Mot de passe administrateur
+
+
+
+ + +
+
+ + +
Minimum 8 caractères.
+
+
+ + +
+ +
+
+
+
+
+ + +
+
+
+
Chemin des logs Amadea
+
+
+
+ + +
Dossier contenant les fichiers isoft_*.txt et awevents_*.txt.
+
+ +
+
+
+
+
+
+
Seuils statut utilisateurs
+
+
+
+ + +
+
+ + +
Au-delà du seuil inactif, le statut passe à Déconnecté.
+
+ +
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/users.html b/templates/users.html new file mode 100644 index 0000000..4378ed3 --- /dev/null +++ b/templates/users.html @@ -0,0 +1,300 @@ +{% extends "base.html" %} +{% block title %}Supervision - Utilisateurs{% endblock %} + +{% block content %} +
+

Utilisateurs Amadea

+ +
+ +
+ + +
+
+
Utilisateurs actifs par heure
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ + Les donnees historiques des jours precedents ne sont pas disponibles (fichiers archives). +
+
+
+
+ + +
+
+
Utilisateurs connectes aujourd'hui
+
+
+ + + + + + + + + + + +
UtilisateurStatutDerniere actionActions (24h)Depuis
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %}