Compare commits

...

10 Commits

Author SHA1 Message Date
oussi
c3999d5215 doc: plan d'implémentation supervision-rs (14 tâches)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:25:26 +02:00
oussi
29df58331a doc: ajout du spec de design supervision-rs (réécriture Rust)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:04:57 +02:00
oussi
f2f41c47ae ajout du suivie utilisateur 2026-04-07 10:41:25 +02:00
oussi
537a3d6f55 ajout du suivi utilisateur 2026-04-02 14:41:11 +02:00
oussi
a9b505c73c ajout du suivi utilisateur 2026-04-02 14:39:25 +02:00
oussi
65edffbbc1 ajout du suivi utilisateur 2026-04-02 14:32:04 +02:00
oussi
b3e91bb0e3 ajout du suivi utilisateur 2026-04-02 12:18:53 +02:00
oussi
8887d97f90 ajout du suivi utilisateur 2026-04-02 11:45:21 +02:00
oussi
1abe0f2657 ajout du suivi utilisateur 2026-04-02 11:44:31 +02:00
oussi
7abe46a6c4 ajout du suivi utilisateur 2026-04-02 11:44:10 +02:00
7 changed files with 3846 additions and 1 deletions

5
.gitignore vendored
View File

@@ -7,6 +7,11 @@ data/alerts.json
*.log *.log
.env .env
imput/ imput/
logTest/
log/
CLAUDE.md
docs/
.claude/
*.spec *.spec
build/ build/
dist/ dist/

View File

@@ -9,12 +9,15 @@ Envoie des alertes email lorsque les seuils configures sont depasses.
## Fonctionnalites ## Fonctionnalites
- **Dashboard temps reel** : CPU, RAM, disques, processus surveilles (rafraichissement auto) - **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 - **Alertes email** : envoi automatique quand un seuil est depasse, avec cooldown anti-spam
- **Configuration complete via l'interface** : - **Configuration complete via l'interface** :
- Seuils d'alerte (CPU, RAM, disque) - Seuils d'alerte (CPU, RAM, disque)
- Frequence de verification (en minutes) - Frequence de verification (en minutes)
- Serveur SMTP + test d'envoi integre - Serveur SMTP + test d'envoi integre
- Ajout/suppression de processus a surveiller - Ajout/suppression de processus a surveiller
- Chemin du dossier de logs Amadea
- Seuils de statut utilisateurs (actif / inactif)
- Port de l'application - Port de l'application
- Mot de passe administrateur - Mot de passe administrateur
- **Securite** : authentification par login/mot de passe, rate limiting anti-bruteforce, en-tetes HTTP securises - **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 ├── templates\ # Pages HTML de l'interface
├── static\ # CSS ├── static\ # CSS
└── data\ # (cree au 1er lancement) └── data\ # (cree au 1er lancement)
├── config.json # Configuration (seuils, SMTP, processus) ├── config.json # Configuration (seuils, SMTP, processus, logs Amadea)
└── alerts.json # Historique des alertes └── 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. > **Important** : ne pas supprimer le dossier `_internal\`, il est necessaire au fonctionnement.
--- ---
@@ -156,6 +173,9 @@ supervision\
| Seuil CPU | 90% | | Seuil CPU | 90% |
| Seuil RAM | 85% | | Seuil RAM | 85% |
| Seuil Disque | 90% | | 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 ### Processus surveilles par defaut
@@ -169,6 +189,35 @@ 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.txt` | Source principale : actions utilisateurs, deconnexions explicites |
| `isoft_JJ-MM-AA_N.txt` | Complement : evenements de session |
Seuls les fichiers du jour courant sont lus (les fichiers des jours precedents zipes sont ignores pour le tableau temps reel). Le graphique "7 derniers jours" utilise les fichiers `.txt` non zipes s'ils sont disponibles.
### 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 `awevents_*.txt` et `isoft_*.txt`.
Valeur par defaut : `C:\ProgramData\ISoft\Amadea Web 8 x64\data\logs`
---
## Depannage ## Depannage
| Probleme | Solution | | Probleme | Solution |
@@ -178,3 +227,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" | | 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) | | 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` | | 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 |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,151 @@
# Design — supervision-rs
**Date :** 2026-04-07
**Objectif :** Réécrire l'application de supervision système en Rust pour produire un exécutable Windows standalone, sans dépendances, déployable par simple copie.
---
## Contexte
L'application actuelle est en Python (Flask + psutil + PyInstaller). Le tuteur a recommandé une réécriture en Rust pour obtenir un exécutable plus stable sur Windows. Le binaire doit être autonome : on le copie sur n'importe quel serveur Windows, on l'installe comme service, et c'est tout.
---
## Périmètre fonctionnel
Parité complète avec l'app Python actuelle :
- Interface web sécurisée (login admin, cookie de session)
- Dashboard temps réel : CPU, RAM, disques, processus surveillés
- Alertes email (SMTP plain ou STARTTLS) avec cooldown configurable
- Configuration via UI (seuils, SMTP, intervalle, processus)
- Historique des alertes (max 500, rotation automatique)
- Fonctionne comme service Windows (démarre avec Windows, tourne en arrière-plan)
---
## Architecture
### Structure du projet
```
supervision-rs/
├── Cargo.toml
├── src/
│ ├── main.rs # Point d'entrée + intégration service Windows
│ ├── config.rs # Lecture/écriture config.json et alerts.json
│ ├── monitor.rs # Thread de collecte métriques + évaluation seuils
│ ├── alerter.rs # Envoi email SMTP
│ ├── auth.rs # Session admin, rate limiting login
│ └── routes.rs # Endpoints Axum (dashboard, API, config, login)
├── templates/ # Templates Tera (portés depuis Jinja2)
│ ├── base.html
│ ├── dashboard.html
│ ├── login.html
│ ├── config.html
│ └── alerts.html
└── data/ # Créé au premier lancement (gitignored)
├── config.json
└── alerts.json
```
### Crates
| Crate | Rôle | Équivalent Python |
|---|---|---|
| `axum` | Serveur HTTP async | Flask |
| `tokio` | Runtime async | — |
| `tera` | Templates HTML | Jinja2 |
| `sysinfo` | Métriques CPU/RAM/disque/processus | psutil |
| `lettre` | Email SMTP | smtplib |
| `serde` + `serde_json` | Sérialisation config/alertes | json |
| `windows-service` | Intégration service Windows | — |
| `tower-sessions` | Sessions auth (cookie signé) | Flask-Login |
| `tower_governor` | Rate limiting | Flask-Limiter |
| `tracing` | Logs | logging |
---
## Modes de démarrage
```
supervision.exe → mode console (test, développement)
supervision.exe install → installe le service Windows
supervision.exe uninstall → supprime le service Windows
(lancé par Windows SCM) → mode service (background automatique)
```
Détection automatique dans `main.rs` : si lancé par le Service Control Manager de Windows, entre en mode service. Sinon, mode console.
**Installation sur un serveur :**
```cmd
supervision.exe install
sc start Supervision
```
---
## State partagé (concurrence)
```
Arc<AppState>
├── config: RwLock<Config> # Config lue/écrite par routes + monitor
├── metrics: RwLock<Metrics> # Écrit par monitor, lu par /api/metrics
└── alerter: Alerter # Utilisé par monitor
```
Le thread de monitoring est une `tokio::task` qui tourne en arrière-plan. Il met à jour `metrics` via `RwLock`. Les routes Axum lisent ce state sans bloquer.
---
## Authentification
- Un seul admin, identifiants stockés dans `config.json` (mot de passe hashé bcrypt)
- Session via cookie signé (`tower-sessions`)
- Rate limiting sur `POST /login` : 10 tentatives/minute
- Toutes les routes (sauf `/login`, `/static`) redirigent vers login si non authentifié
---
## Métriques & seuils
Collecte via `sysinfo` :
- CPU : pourcentage global
- RAM : pourcentage utilisé, total/utilisé/disponible en Go
- Disques : partitions physiques ≥1 Go, pourcentage, espace total/utilisé/libre
- Processus : recherche par pattern dans nom ou ligne de commande, mémoire RSS, CPU
Niveaux de statut (identique au Python) :
- `ok` : < 80% du seuil
- `warning` : ≥ 80% du seuil
- `critical` : ≥ 100% du seuil
---
## Alertes email
- SMTP plain ou STARTTLS (configurable)
- Cooldown par clé (en mémoire, réinitialisé au redémarrage)
- Alertes déclenchées sur : CPU critique, RAM critique, disque critique, processus arrêté, mémoire processus critique
- Stockage dans `alerts.json` (max 500 entrées, rotation FIFO)
---
## Persistance
- `data/config.json` : configuration complète (seuils, SMTP, admin, processus surveillés, intervalle)
- `data/alerts.json` : historique des alertes
- Dossier `data/` créé automatiquement au premier lancement, dans le même répertoire que l'exe
- Pas de base de données
---
## Déploiement
1. Compiler sur Windows : `cargo build --release`
2. Récupérer `target/release/supervision.exe`
3. Copier l'exe seul sur le serveur cible
4. Lancer `supervision.exe install` puis `sc start Supervision`
5. Accéder à `http://localhost:5000` dans le navigateur
Aucune autre dépendance requise sur le serveur cible.

View File

@@ -38,6 +38,12 @@
<i class="bi bi-bell"></i> Alertes <i class="bi bi-bell"></i> Alertes
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'users' %}active{% endif %}"
href="{{ url_for('users') }}">
<i class="bi bi-people"></i> Utilisateurs
</a>
</li>
</ul> </ul>
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">

View File

@@ -257,6 +257,62 @@
</div> </div>
</div> </div>
<!-- Chemin des logs Amadea + Seuils statut utilisateurs -->
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-folder2-open"></i> Chemin des logs Amadea</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('update_amadea_log_path') }}">
<div class="mb-3">
<label for="amadea_log_path" class="form-label">Dossier des logs</label>
<input type="text" class="form-control" id="amadea_log_path"
name="amadea_log_path"
value="{{ config.amadea_log_path }}"
placeholder="C:\ProgramData\ISoft\Amadea Web 8 x64\data\logs">
<div class="form-text">Dossier contenant les fichiers isoft_*.txt et awevents_*.txt.</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Enregistrer
</button>
</form>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-people"></i> Seuils statut utilisateurs</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('update_user_thresholds') }}">
<div class="mb-3">
<label for="active_minutes" class="form-label">Actif si derniere action &lt; (minutes)</label>
<input type="number" class="form-control" id="active_minutes"
name="active_minutes"
value="{{ config.user_status_thresholds.active_minutes }}"
min="1" required>
</div>
<div class="mb-3">
<label for="inactive_minutes" class="form-label">Inactif si derniere action &lt; (minutes)</label>
<input type="number" class="form-control" id="inactive_minutes"
name="inactive_minutes"
value="{{ config.user_status_thresholds.inactive_minutes }}"
min="1" required>
<div class="form-text">Au-dela du seuil inactif, le statut passe a Deconnecte.</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Enregistrer
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}

300
templates/users.html Normal file
View File

@@ -0,0 +1,300 @@
{% extends "base.html" %}
{% block title %}Supervision - Utilisateurs{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0"><i class="bi bi-people"></i> Utilisateurs Amadea</h4>
<span id="last-update" class="text-muted small"></span>
</div>
<div id="alert-zone"></div>
<!-- Graphique d'activite horaire -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-bar-chart"></i> Utilisateurs actifs par heure</h6>
<select id="period-select" class="form-select form-select-sm" style="width: auto;">
<option value="today">Aujourd'hui</option>
<option value="7days">7 derniers jours</option>
</select>
</div>
<div class="card-body">
<div style="display: flex; gap: 6px;">
<div id="chart-yaxis"
style="display: flex; flex-direction: column; justify-content: space-between;
align-items: flex-end; width: 20px; height: 100px; flex-shrink: 0;">
</div>
<div style="flex: 1; min-width: 0;">
<div id="chart-container"
style="height: 100px; display: flex; align-items: flex-end; gap: 3px;">
</div>
<div id="chart-labels" style="display: flex; gap: 3px; margin-top: 4px;"></div>
</div>
</div>
<div id="chart-unavailable" class="d-none">
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle"></i>
Les donnees historiques des jours precedents ne sont pas disponibles (fichiers archives).
</div>
</div>
</div>
</div>
<!-- Tableau utilisateurs -->
<div class="card">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-person-lines-fill"></i> Utilisateurs connectes aujourd'hui</h6>
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Utilisateur</th>
<th>Statut</th>
<th>Derniere action</th>
<th>Actions (24h)</th>
<th>Depuis</th>
</tr>
</thead>
<tbody id="users-tbody"></tbody>
</table>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
var STATUS_CONFIG = {
actif: { badge: 'bg-success', label: 'ACTIF' },
inactif: { badge: 'bg-warning text-dark', label: 'INACTIF' },
deconnecte: { badge: 'bg-secondary', label: 'DECONNECTE' },
};
var currentHourly = [];
var currentPeriod = 'today';
/* --- Graphique CSS pur --- */
function renderYAxis(max) {
var yaxis = document.getElementById('chart-yaxis');
yaxis.textContent = '';
var values = [max, Math.round(max / 2), 0];
values.forEach(function(v) {
var lbl = document.createElement('div');
lbl.style.fontSize = '0.6rem';
lbl.style.color = '#6c757d';
lbl.style.lineHeight = '1';
lbl.textContent = v;
yaxis.appendChild(lbl);
});
}
function renderChart(data) {
var container = document.getElementById('chart-container');
var labelsEl = document.getElementById('chart-labels');
var unavail = document.getElementById('chart-unavailable');
container.style.display = 'flex';
labelsEl.style.display = 'flex';
unavail.classList.add('d-none');
container.textContent = '';
labelsEl.textContent = '';
if (!data || data.length === 0) {
renderYAxis(0);
var msg = document.createElement('span');
msg.className = 'text-muted small';
msg.textContent = 'Aucune donnee disponible.';
container.appendChild(msg);
return;
}
var max = 1;
data.forEach(function(d) { if ((d.count || 0) > max) max = d.count; });
renderYAxis(max);
data.forEach(function(item) {
var count = item.count || 0;
var heightPct = count > 0 ? Math.max((count / max) * 100, 6) : 0;
var bar = document.createElement('div');
bar.style.flex = '1';
bar.style.minWidth = '14px';
bar.style.height = heightPct + '%';
bar.style.background = '#0d6efd';
bar.style.borderRadius = '3px 3px 0 0';
bar.style.opacity = count > 0 ? '1' : '0.15';
bar.style.transition = 'height 0.3s';
bar.title = item.hour !== undefined
? item.hour + 'h : ' + count + ' utilisateur(s)'
: item.date + ' : ' + count + ' utilisateur(s)';
container.appendChild(bar);
var lbl = document.createElement('div');
lbl.style.flex = '1';
lbl.style.minWidth = '14px';
lbl.style.textAlign = 'center';
lbl.style.fontSize = '0.6rem';
lbl.style.color = '#6c757d';
lbl.style.overflow = 'hidden';
if (item.hour !== undefined) {
lbl.textContent = item.hour % 3 === 0 ? item.hour + 'h' : '';
} else {
var d = new Date(item.date);
lbl.textContent = d.getUTCDate() + '/' + (d.getUTCMonth() + 1);
}
labelsEl.appendChild(lbl);
});
}
function renderWeekly(weekly) {
var unavail = document.getElementById('chart-unavailable');
var container = document.getElementById('chart-container');
var labelsEl = document.getElementById('chart-labels');
var allNull = weekly.every(function(d) { return d.count === null; });
if (allNull) {
container.style.display = 'none';
labelsEl.style.display = 'none';
document.getElementById('chart-yaxis').textContent = '';
unavail.classList.remove('d-none');
return;
}
container.style.display = 'flex';
labelsEl.style.display = 'flex';
unavail.classList.add('d-none');
renderChart(weekly.map(function(d) {
return { date: d.date, count: d.count === null ? 0 : d.count };
}));
}
/* --- Tableau --- */
function renderTable(users) {
var tbody = document.getElementById('users-tbody');
tbody.textContent = '';
if (!users || users.length === 0) {
var tr = document.createElement('tr');
var td = document.createElement('td');
td.colSpan = 5;
td.className = 'text-center text-muted py-3';
td.textContent = "Aucun utilisateur detecte aujourd'hui.";
tr.appendChild(td);
tbody.appendChild(tr);
return;
}
users.forEach(function(u) {
var sc = STATUS_CONFIG[u.status] || { badge: 'bg-secondary', label: (u.status || '').toUpperCase() };
var tr = document.createElement('tr');
/* Utilisateur */
var tdUser = document.createElement('td');
var strong = document.createElement('strong');
strong.textContent = u.login || '';
tdUser.appendChild(strong);
tr.appendChild(tdUser);
/* Statut */
var tdStatus = document.createElement('td');
var badge = document.createElement('span');
badge.className = 'badge ' + sc.badge;
badge.textContent = sc.label;
tdStatus.appendChild(badge);
tr.appendChild(tdStatus);
/* Derniere action */
var tdAction = document.createElement('td');
tdAction.textContent = u.last_action_time || '\u2014';
if (u.last_action_label) {
var br = document.createElement('br');
var small = document.createElement('small');
small.className = 'text-muted';
small.textContent = u.last_action_label;
tdAction.appendChild(br);
tdAction.appendChild(small);
}
tr.appendChild(tdAction);
/* Actions 24h */
var tdCount = document.createElement('td');
tdCount.textContent = String(u.action_count_24h || 0);
tr.appendChild(tdCount);
/* Depuis */
var tdSince = document.createElement('td');
tdSince.textContent = u.connected_since || '\u2014';
tr.appendChild(tdSince);
tbody.appendChild(tr);
});
}
/* --- Alertes --- */
function showAlert(type, message) {
var zone = document.getElementById('alert-zone');
zone.textContent = '';
var div = document.createElement('div');
div.className = 'alert alert-' + type;
var icon = document.createElement('i');
icon.className = 'bi bi-exclamation-triangle me-1';
div.appendChild(icon);
div.appendChild(document.createTextNode(message));
zone.appendChild(div);
}
function clearAlert() {
document.getElementById('alert-zone').textContent = '';
}
/* --- Refresh --- */
function refreshUsers() {
fetch('/api/users')
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) {
showAlert('warning', data.error);
renderTable([]);
renderChart([]);
return;
}
if (data.no_files) {
showAlert('info', "Aucun log disponible pour aujourd'hui.");
renderTable([]);
renderChart([]);
return;
}
clearAlert();
renderTable(data.users || []);
currentHourly = data.hourly || [];
if (currentPeriod === 'today') {
renderChart(currentHourly);
}
document.getElementById('last-update').textContent =
'Mis a jour : ' + new Date().toLocaleTimeString('fr-FR');
})
.catch(function() {});
}
document.getElementById('period-select').addEventListener('change', function() {
currentPeriod = this.value;
if (currentPeriod === 'today') {
document.getElementById('chart-container').style.display = 'flex';
document.getElementById('chart-labels').style.display = 'flex';
document.getElementById('chart-unavailable').classList.add('d-none');
renderChart(currentHourly);
} else {
fetch('/api/users/activity/weekly')
.then(function(r) { return r.json(); })
.then(function(data) { renderWeekly(data.weekly || []); })
.catch(function() {});
}
});
refreshUsers();
setInterval(refreshUsers, 30000);
</script>
{% endblock %}