diff --git a/.gitignore b/.gitignore index 000e99a..c3395e7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,12 @@ data/alerts.json *.log .env imput/ +logTest/ +log/ +CLAUDE.md +docs/ +.claude/ *.spec build/ dist/ +docs/ diff --git a/README.md b/README.md index cb65ed9..a598c3f 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,15 @@ Envoie des alertes email lorsque les seuils configures sont depasses. ## Fonctionnalites - **Dashboard temps reel** : CPU, RAM, disques, processus surveilles (rafraichissement auto) +- **Suivi des utilisateurs Amadea** : statut en temps reel (Actif / Inactif / Deconnecte), derniere action, nombre d'actions sur 24h, graphique d'activite horaire - **Alertes email** : envoi automatique quand un seuil est depasse, avec cooldown anti-spam - **Configuration complete via l'interface** : - Seuils d'alerte (CPU, RAM, disque) - Frequence de verification (en minutes) - Serveur SMTP + test d'envoi integre - Ajout/suppression de processus a surveiller + - Chemin du dossier de logs Amadea + - Seuils de statut utilisateurs (actif / inactif) - Port de l'application - Mot de passe administrateur - **Securite** : authentification par login/mot de passe, rate limiting anti-bruteforce, en-tetes HTTP securises @@ -138,10 +141,24 @@ supervision\ ├── templates\ # Pages HTML de l'interface ├── static\ # CSS └── data\ # (cree au 1er lancement) - ├── config.json # Configuration (seuils, SMTP, processus) + ├── config.json # Configuration (seuils, SMTP, processus, logs Amadea) └── alerts.json # Historique des alertes ``` +### Structure des sources (developpement) + +``` +supervision\ +├── app.py # Application Flask, routes +├── monitor.py # Surveillance CPU/RAM/disques/processus +├── user_monitor.py # Suivi utilisateurs Amadea (parsing logs) +├── alerter.py # Envoi d'alertes email +├── config_manager.py # Persistance configuration JSON +├── templates\ # Pages HTML Jinja2 +├── static\ # CSS +└── tests\ # Tests unitaires (pytest) +``` + > **Important** : ne pas supprimer le dossier `_internal\`, il est necessaire au fonctionnement. --- @@ -156,6 +173,9 @@ supervision\ | Seuil CPU | 90% | | Seuil RAM | 85% | | Seuil Disque | 90% | +| Chemin logs Amadea | `C:\ProgramData\ISoft\Amadea Web 8 x64\data\logs` | +| Statut Actif si | derniere action < 5 minutes | +| Statut Inactif si | derniere action < 30 minutes | ### Processus surveilles par defaut @@ -169,6 +189,47 @@ Tous les parametres sont modifiables depuis l'interface web. --- +## Suivi des utilisateurs Amadea + +L'onglet **Utilisateurs** affiche en temps reel les utilisateurs connectes a Amadea Web 8 x64. + +### Fichiers de logs lus + +| Fichier | Role | +|---------|------| +| `awevents_JJ-MM-AA_N.log(.gz)` | Source principale : actions utilisateurs, deconnexions explicites | +| `awevents.log` | Log actif du jour (sans date dans le nom, serveur HDS) | +| `isoft_JJ-MM-AA_N.log(.gz)` | Complement : evenements de session | + +La detection gere les deux cas du serveur HDS : log actif sans date dans le nom (`awevents.log`) et log zip en cours de journee avec date dans le nom (`awevents_26-04-13_1.log.gz`). + +### Tableau temps reel (aujourd'hui) + +- Colonnes : Utilisateur, Statut, Derniere action, Actions (24h), Depuis +- Tri : statut (actif → inactif → deconnecte), puis derniere action la plus recente en premier au sein de chaque groupe + +### Graphique 7 derniers jours + +- Affiche le pic d'utilisateurs simultanes par jour +- **Cliquer sur une barre** charge le tableau des utilisateurs de ce jour : Utilisateur, Derniere utilisation, Actions (jour), Duree de presence (premiere → derniere action) +- Tri par nombre d'actions decroissant + +### Regles de statut + +| Statut | Condition | +|--------|-----------| +| **ACTIF** | Derniere action < 5 min (configurable) | +| **INACTIF** | Derniere action entre 5 et 30 min (configurable) | +| **DECONNECTE** | Derniere action > 30 min ou deconnexion explicite detectee | + +### Configuration du chemin des logs + +Dans **Configuration > Chemin des logs Amadea**, renseignez le chemin complet du dossier contenant les fichiers de logs. + +Valeur par defaut : `C:\ProgramData\ISoft\Amadea Web 8 x64\data\logs` + +--- + ## Depannage | Probleme | Solution | @@ -178,3 +239,5 @@ Tous les parametres sont modifiables depuis l'interface web. | Pas d'email recu | Verifier la configuration SMTP et utiliser le bouton "Envoyer un email de test" | | Mot de passe oublie | Supprimer `data\config.json` et relancer (reinitialise a admin/admin) | | L'executable ne se lance pas | Verifier que le dossier `_internal\` est present a cote de `supervision.exe` | +| Onglet Utilisateurs vide ou erreur | Verifier le chemin des logs dans Configuration et s'assurer que le service Amadea tourne | +| Tous les utilisateurs affiches DECONNECTE | Normal si aucune activite recente — verifier que les fichiers `awevents_*.txt` du jour sont bien presents dans le dossier configure | diff --git a/alerter.py b/alerter.py index d18a5dd..ea58347 100644 --- a/alerter.py +++ b/alerter.py @@ -1,8 +1,11 @@ -"""Envoi d'alertes par email via SMTP.""" +"""Envoi d'alertes par email via Brevo API ou SMTP.""" +import json import smtplib -from email.mime.text import MIMEText +import urllib.error +import urllib.request from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText class EmailAlerter: @@ -14,28 +17,64 @@ class EmailAlerter: def is_configured(self): smtp = self._get_smtp_config() - return bool(smtp.get("server") and smtp.get("from_email") and smtp.get("to_emails")) + has_recipients = bool(smtp.get("from_email") and smtp.get("to_emails")) + if smtp.get("brevo_api_key"): + return has_recipients + return has_recipients and bool(smtp.get("server")) def send_alert(self, subject, body): - """Envoie un email d'alerte. Silencieux si SMTP non configure.""" + """Envoie un email d'alerte. Silencieux si non configure.""" if not self.is_configured(): - return False, "SMTP non configure" + return False, "Email non configure" return self._send_email(subject, body) def send_test(self): - """Envoie un email de test pour valider la configuration SMTP.""" + """Envoie un email de test pour valider la configuration.""" if not self.is_configured(): - return False, "Configuration SMTP incomplete" + return False, "Configuration email incomplete" subject = "[TEST] Supervision - Test de configuration email" body = ( "Ceci est un email de test.\n\n" - "Si vous recevez ce message, la configuration SMTP est correcte.\n\n" + "Si vous recevez ce message, la configuration est correcte.\n\n" "-- Supervision" ) return self._send_email(subject, body) def _send_email(self, subject, body): smtp_cfg = self._get_smtp_config() + if smtp_cfg.get("brevo_api_key"): + return self._send_via_brevo(subject, body, smtp_cfg) + return self._send_via_smtp(subject, body, smtp_cfg) + + def _send_via_brevo(self, subject, body, smtp_cfg): + """Envoi via l'API REST Brevo.""" + data = { + "sender": {"email": smtp_cfg["from_email"]}, + "to": [{"email": e} for e in smtp_cfg["to_emails"]], + "subject": subject, + "textContent": body, + } + req = urllib.request.Request( + "https://api.brevo.com/v3/smtp/email", + data=json.dumps(data).encode("utf-8"), + headers={ + "Content-Type": "application/json", + "api-key": smtp_cfg["brevo_api_key"], + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=15): + return True, "Email envoye via Brevo" + except urllib.error.HTTPError as e: + return False, f"Erreur Brevo API: {e.code} {e.reason}" + except urllib.error.URLError as e: + return False, f"Impossible de joindre Brevo: {e.reason}" + except Exception as e: + return False, f"Erreur Brevo: {str(e)}" + + def _send_via_smtp(self, subject, body, smtp_cfg): + """Envoi via SMTP classique.""" try: msg = MIMEMultipart() msg["From"] = smtp_cfg["from_email"] diff --git a/app.py b/app.py index 2fbbee9..f567c63 100644 --- a/app.py +++ b/app.py @@ -20,6 +20,7 @@ from werkzeug.security import check_password_hash, generate_password_hash from config_manager import ConfigManager from monitor import SystemMonitor from alerter import EmailAlerter +from user_monitor import UserMonitor # --- Init --- config = ConfigManager() @@ -53,6 +54,7 @@ login_manager.login_message = "Veuillez vous connecter." # Services alerter = EmailAlerter(config) monitor = SystemMonitor(config, alerter) +user_monitor = UserMonitor(config) class AdminUser(UserMixin): @@ -124,10 +126,8 @@ def settings(): smtp = cfg.get("smtp", {}) # Masquer le mot de passe SMTP dans l'affichage smtp_display = dict(smtp) - if smtp_display.get("password"): - smtp_display["password_masked"] = "*" * 8 - else: - smtp_display["password_masked"] = "" + smtp_display["password_masked"] = "*" * 8 if smtp_display.get("password") else "" + smtp_display["brevo_api_key_masked"] = "*" * 8 if smtp_display.get("brevo_api_key") else "" return render_template( "settings.html", config=cfg, @@ -181,6 +181,7 @@ def update_monitoring(): @login_required def update_smtp(): try: + old_smtp = config.get("smtp", {}) smtp = { "server": request.form["smtp_server"].strip(), "port": int(request.form["smtp_port"]), @@ -191,14 +192,12 @@ def update_smtp(): e.strip() for e in request.form["smtp_to"].split(",") if e.strip() ], } - # Ne mettre a jour le mot de passe que s'il est fourni + # Conserver le mot de passe si non fourni new_password = request.form.get("smtp_password", "") - if new_password: - smtp["password"] = new_password - else: - # Garder l'ancien mot de passe - old_smtp = config.get("smtp", {}) - smtp["password"] = old_smtp.get("password", "") + smtp["password"] = new_password if new_password else old_smtp.get("password", "") + # Conserver la cle Brevo si non fournie + new_brevo_key = request.form.get("brevo_api_key", "").strip() + smtp["brevo_api_key"] = new_brevo_key if new_brevo_key else old_smtp.get("brevo_api_key", "") config.set("smtp", smtp) flash("Configuration SMTP mise a jour.", "success") @@ -312,6 +311,84 @@ def toggle_monitoring(): return redirect(url_for("dashboard")) +@app.route("/users") +@login_required +def users(): + return render_template("users.html") + + +@app.route("/api/users") +@login_required +def api_users(): + cache = user_monitor.data + if cache.get("error"): + return jsonify({"error": cache["error"]}) + if cache.get("no_files"): + return jsonify({"no_files": True}) + users_list = [ + { + "login": u["login"], + "status": u["status"], + "last_action_time": u["last_action_time"].strftime("%H:%M:%S") if u.get("last_action_time") else None, + "last_action_label": u.get("last_action_label", ""), + "action_count_24h": u.get("action_count_24h", 0), + "connected_since": u["connected_since"].strftime("%H:%M") if u.get("connected_since") else None, + "explicit_logout": u.get("explicit_logout", False), + } + for u in cache.get("users", {}).values() + ] + return jsonify({"users": users_list, "hourly": cache.get("hourly", [])}) + + +@app.route("/api/users/activity/weekly") +@login_required +def api_users_weekly(): + return jsonify({"weekly": user_monitor.get_weekly_activity()}) + + +@app.route("/api/users/day/") +@login_required +def api_users_day(date_str): + from datetime import datetime as dt + try: + date = dt.strptime(date_str, "%Y-%m-%d").date() + except ValueError: + return jsonify({"error": "Date invalide"}), 400 + users = user_monitor.get_users_for_date(date) + return jsonify({"users": users, "date": date_str}) + + +@app.route("/settings/amadea-log-path", methods=["POST"]) +@login_required +def update_amadea_log_path(): + path = request.form.get("amadea_log_path", "").strip() + if not path: + flash("Le chemin ne peut pas etre vide.", "danger") + return redirect(url_for("settings")) + config.set("amadea_log_path", path) + flash("Chemin des logs Amadea mis a jour.", "success") + return redirect(url_for("settings")) + + +@app.route("/settings/user-thresholds", methods=["POST"]) +@login_required +def update_user_thresholds(): + try: + active = int(request.form["active_minutes"]) + inactive = int(request.form["inactive_minutes"]) + if active < 1 or inactive < 1: + flash("Les seuils doivent etre d'au moins 1 minute.", "danger") + return redirect(url_for("settings")) + if active >= inactive: + flash("Le seuil 'actif' doit etre inferieur au seuil 'inactif'.", "danger") + return redirect(url_for("settings")) + config.set("user_status_thresholds", {"active_minutes": active, "inactive_minutes": inactive}) + flash("Seuils utilisateurs mis a jour.", "success") + except (ValueError, KeyError) as e: + flash(f"Erreur: {e}", "danger") + return redirect(url_for("settings")) + + def check_port_available(port): """Verifie si un port est disponible.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: @@ -341,6 +418,9 @@ def main(): # Collecte initiale monitor.collect_metrics() + user_monitor.start() + user_monitor.parse_logs() + print("[Supervision] Monitoring actif") app.run(host="0.0.0.0", port=port, debug=False) diff --git a/config_manager.py b/config_manager.py index 5dbd7e9..9eee691 100644 --- a/config_manager.py +++ b/config_manager.py @@ -54,6 +54,12 @@ def get_default_config(): "password": "", "from_email": "", "to_emails": [], + "brevo_api_key": "", + }, + "amadea_log_path": r"C:\ProgramData\ISoft\Amadea Web 8 x64\data\logs", + "user_status_thresholds": { + "active_minutes": 5, + "inactive_minutes": 30, }, "admin": { "username": "admin", diff --git a/requirements.txt b/requirements.txt index 1fff652..72a64ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ flask-limiter==3.9.* psutil==6.1.* werkzeug==3.1.* pyinstaller==6.12.* +pytest==8.3.* diff --git a/templates/base.html b/templates/base.html index e4715cd..181715c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -38,6 +38,12 @@ Alertes +