Compare commits
6 Commits
9f95547aba
...
rust
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebd5482070 | ||
|
|
607ff0629c | ||
|
|
c7892748dc | ||
|
|
ca69337afb | ||
|
|
ddfa84cfea | ||
|
|
eb62e74f08 |
43
.gitignore
vendored
43
.gitignore
vendored
@@ -1,18 +1,35 @@
|
||||
# Build Rust
|
||||
/target/
|
||||
|
||||
# Données runtime
|
||||
data/config.json
|
||||
data/alerts.json
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Variables d'environnement
|
||||
.env
|
||||
|
||||
# Dossiers non pertinents
|
||||
imput/
|
||||
logTest/
|
||||
log/
|
||||
docs/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# IDE / outils locaux
|
||||
.claude/
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Python (héritage)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
venv/
|
||||
data/config.json
|
||||
data/alerts.json
|
||||
*.log
|
||||
.env
|
||||
imput/
|
||||
logTest/
|
||||
log/
|
||||
CLAUDE.md
|
||||
docs/
|
||||
.claude/
|
||||
*.spec
|
||||
build/
|
||||
dist/
|
||||
docs/
|
||||
|
||||
60
CLAUDE.md
Normal file
60
CLAUDE.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Dev server (http://localhost:5000, credentials admin/admin)
|
||||
cargo run
|
||||
|
||||
# Release build — produces target/release/supervision (Linux) or target\release\supervision.exe (Windows)
|
||||
cargo build --release
|
||||
|
||||
# Cross-compile for Windows from Linux
|
||||
rustup target add x86_64-pc-windows-gnu
|
||||
cargo build --release --target x86_64-pc-windows-gnu
|
||||
|
||||
# Tests
|
||||
cargo test
|
||||
|
||||
# Linting
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
### Windows service (run as Administrator)
|
||||
|
||||
```cmd
|
||||
supervision.exe install # register as auto-start service named "Supervision"
|
||||
sc start Supervision
|
||||
sc stop Supervision
|
||||
supervision.exe uninstall
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Single-binary Axum web server. All shared mutable state is passed through `AppState` (defined in `src/routes/mod.rs`) which holds `Arc`-wrapped components.
|
||||
|
||||
**`src/main.rs`** — entry point: builds `AppState`, registers all routes, starts background threads (`start_monitoring`, `UserMonitor::start`), and handles Windows service scaffolding (feature-gated on `cfg(windows)`).
|
||||
|
||||
**`src/routes/mod.rs`** — defines `AppState`, the `AuthUser` extractor (redirects to `/login` if session is absent), and `build_tera()` which **embeds all templates at compile time** via `include_str!`. Modifying a template requires a recompile. Also provides `flash`/`get_and_clear_flash` session helpers and `render_html`.
|
||||
|
||||
**`src/routes/{auth,dashboard,settings,alerts,users}.rs`** — all HTTP route handlers. Each protected handler receives `AuthUser` as an extractor to enforce authentication.
|
||||
|
||||
**`src/config.rs`** — `Config` struct serialised to/from `data/config.json`. `ConfigManager` wraps `Config` and the `data/` path; callers lock `Arc<AsyncMutex<ConfigManager>>` to read or write config. Password hashing with bcrypt. Alerts ring-buffered to 500 entries in `data/alerts.json`.
|
||||
|
||||
**`src/monitor.rs`** — `SystemMonitor` collects CPU/RAM/disk/process data via `sysinfo`. `eval_status(value, threshold)` returns `"ok"` / `"warning"` / `"critical"` (warning ≥ 80 %, critical ≥ 100 % of threshold). The background thread calls `collect_metrics` → `check_thresholds`, applies per-key cooldown, persists alerts, and optionally sends email. Sleeps in 5-second chunks so `monitoring_active` changes are picked up promptly.
|
||||
|
||||
**`src/user_monitor.rs`** — `UserMonitor` parses Amadea `awevents_*` and `isoft_*` log files (plain or `.gz`) to build a per-user activity snapshot. `parse_awevents_line` extracts login/action/label from `awevents` files; `isoft` files provide `connected_since` (session open time via `OpenUserSession`). `compute_statuses` assigns `actif` / `inactif` / `absent` / `deconnecte` based on configurable minute thresholds — `absent` means inactive beyond `inactive_minutes` without an explicit logout. `compute_active_time` derives presence and active time by subtracting gaps exceeding `pause_threshold_minutes`. Also provides `get_weekly_activity` / `get_monthly_activity` (peak concurrent users per day) and `get_users_for_date` / `get_user_history`. Log file discovery handles both dated files (`awevents_YY-MM-DD_N.log.gz`) and undated active-log files (`awevents.log`). `UserMonitor.data` is guarded by `std::Mutex` (never held across `.await`).
|
||||
|
||||
**`src/alerter.rs`** — SMTP email dispatch via `lettre`. `is_configured` guards all sends. Uses STARTTLS by default; falls back to unencrypted when `use_tls = false`.
|
||||
|
||||
### Data directory
|
||||
|
||||
`data/` is created next to the binary at first launch:
|
||||
- `config.json` — all settings; written after every settings form submission.
|
||||
- `alerts.json` — ring buffer of the last 500 alerts, newest first.
|
||||
|
||||
### Template context conventions
|
||||
|
||||
Every protected page calls `base_context()` which injects `authenticated`, `flash_messages`, `default_pw`, and `username`. `apply_security_headers()` adds `X-Content-Type-Options`, `X-Frame-Options`, etc. to every response.
|
||||
2732
Cargo.lock
generated
Normal file
2732
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -27,9 +27,16 @@ async-trait = "0.1"
|
||||
http = "1"
|
||||
regex = "1"
|
||||
glob = "0.3"
|
||||
|
||||
flate2 = "1"
|
||||
form_urlencoded = "1"
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-service = "0.7"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
|
||||
180
README.md
Normal file
180
README.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# SuperVision
|
||||
|
||||
Outil de supervision système avec interface web, écrit en Rust. Surveille CPU, RAM, disques et processus en temps réel, avec alertes par email et suivi des utilisateurs Amadea.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- **Dashboard temps réel** — CPU, RAM, disques, uptime, statut par code couleur (ok / warning / critical)
|
||||
- **Surveillance de processus** — détection par pattern, alerte si processus arrêté, seuil mémoire configurable
|
||||
- **Alertes email (SMTP)** — envoi automatique avec cooldown configurable pour éviter le spam
|
||||
- **Suivi utilisateurs Amadea** — analyse des logs `awevents` et `isoft`, statuts actif/inactif/absent/déconnecté, temps de présence et temps actif, compteur d'erreurs par utilisateur (logs `isoft` niveau ERROR), graphe d'activité horaire, hebdomadaire et mensuel, historique par utilisateur
|
||||
- **Interface de configuration** — seuils, SMTP, processus, port, mot de passe admin, tout modifiable via l'UI
|
||||
- **Service Windows** — installation en tant que service système avec démarrage automatique
|
||||
|
||||
## Prérequis
|
||||
|
||||
- [Rust](https://rustup.rs/) (édition 2021, Rust 1.75+)
|
||||
- [Visual Studio Build Tools](https://visualstudio.microsoft.com/fr/visual-cpp-build-tools/) avec la charge de travail **Développement Desktop en C++**
|
||||
|
||||
## Générer l'exécutable
|
||||
|
||||
```cmd
|
||||
cd SuperVisionRust
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
L'exécutable est généré dans : `target\release\supervision.exe`
|
||||
|
||||
## Déploiement
|
||||
|
||||
Copier les éléments suivants dans un dossier de déploiement (ex: `C:\SuperVision\`) :
|
||||
|
||||
```
|
||||
supervision.exe
|
||||
static\
|
||||
templates\
|
||||
```
|
||||
|
||||
## Lancer l'application
|
||||
|
||||
### En mode console (test / debug)
|
||||
|
||||
```cmd
|
||||
supervision.exe
|
||||
```
|
||||
|
||||
L'interface web est accessible sur : **http://localhost:5000**
|
||||
|
||||
Identifiants par défaut : `admin` / `admin` — à changer immédiatement dans les paramètres.
|
||||
|
||||
### Changer le port
|
||||
|
||||
Dans les paramètres de l'interface web (`/settings`), ou en éditant `data\config.json` avant le premier lancement :
|
||||
|
||||
```json
|
||||
{
|
||||
"port": 5000
|
||||
}
|
||||
```
|
||||
|
||||
## Installation en tant que service Windows
|
||||
|
||||
L'invite de commande doit être lancée **en tant qu'administrateur**.
|
||||
|
||||
### Installer le service
|
||||
|
||||
```cmd
|
||||
supervision.exe install
|
||||
```
|
||||
|
||||
Le service est enregistré sous le nom `Supervision` avec un démarrage automatique.
|
||||
|
||||
### Démarrer / arrêter le service
|
||||
|
||||
```cmd
|
||||
sc start Supervision
|
||||
sc stop Supervision
|
||||
```
|
||||
|
||||
Ou via le Gestionnaire des services Windows (`services.msc`).
|
||||
|
||||
### Désinstaller le service
|
||||
|
||||
```cmd
|
||||
sc stop Supervision
|
||||
supervision.exe uninstall
|
||||
```
|
||||
|
||||
## Structure des données
|
||||
|
||||
Au premier lancement, un dossier `data\` est créé automatiquement dans le même répertoire que l'exécutable :
|
||||
|
||||
```
|
||||
data\
|
||||
config.json # Configuration complète (port, seuils, SMTP, admin...)
|
||||
alerts.json # Historique des alertes (500 entrées max)
|
||||
```
|
||||
|
||||
## Configuration SMTP
|
||||
|
||||
Dans l'interface web (`/settings`), section **SMTP** :
|
||||
|
||||
| Champ | Description |
|
||||
|-------|-------------|
|
||||
| Serveur | Adresse du serveur SMTP (ex: `smtp.office365.com`) |
|
||||
| Port | Généralement `587` (TLS) ou `465` (SSL) |
|
||||
| Utilisateur | Adresse email d'envoi |
|
||||
| Mot de passe | Mot de passe ou token d'application |
|
||||
| Destinataires | Emails séparés par des virgules |
|
||||
|
||||
Un bouton **Tester** permet de vérifier la configuration sans attendre une alerte réelle.
|
||||
|
||||
## Surveillance des utilisateurs Amadea
|
||||
|
||||
Renseigner le chemin des logs Amadea dans les paramètres (`/settings`) :
|
||||
|
||||
```
|
||||
C:\ProgramData\ISoft\Amadea Web 8 x64\data\logs
|
||||
```
|
||||
|
||||
SuperVision parse les fichiers `awevents_YY-MM-DD_*` et `isoft_YY-MM-DD_*` pour construire la liste des utilisateurs connectés et leur activité.
|
||||
|
||||
### Statuts utilisateurs
|
||||
|
||||
| Statut | Condition |
|
||||
|--------|-----------|
|
||||
| **actif** | action dans les `N` dernières minutes (défaut : 5 min) |
|
||||
| **inactif** | pas d'action depuis `N` à `M` minutes (défaut : 5 – 30 min) |
|
||||
| **absent** | pas d'action depuis plus de `M` minutes, sans déconnexion explicite (défaut : > 30 min) |
|
||||
| **déconnecté** | déconnexion explicite détectée dans les logs |
|
||||
|
||||
Les seuils sont configurables dans les paramètres (`/settings`, section **Seuils utilisateurs**) :
|
||||
- **Actif si** : délai max depuis la dernière action pour être considéré actif
|
||||
- **Inactif si** : délai au-delà duquel l'utilisateur devient inactif
|
||||
- **Seuil de pause** : durée minimale d'inactivité comptée comme une pause dans le calcul du temps actif
|
||||
|
||||
### Temps de présence et temps actif
|
||||
|
||||
Pour chaque utilisateur SuperVision calcule :
|
||||
- **Présence** — durée entre la première et la dernière action du jour
|
||||
- **Temps actif** — présence moins les pauses dépassant le seuil configuré
|
||||
|
||||
### Erreurs isoft
|
||||
|
||||
SuperVision analyse les fichiers `isoft_*` pour compter les lignes de niveau `ERROR`. Chaque erreur est rattachée à un utilisateur via le champ `ISI=<session_id>` présent dans le nom du thread, et la correspondance session → login est établie grâce aux événements `OpenUserSession` et `CloseUserSession`.
|
||||
|
||||
Le nombre d'erreurs est affiché :
|
||||
- Dans le **tableau temps réel** et le **tableau jour historique** (colonne « Erreurs », badge rouge si > 0)
|
||||
- Dans le **panneau historique utilisateur** (colonne « Erreurs » + tooltip sur les barres)
|
||||
- Dans les **tooltips des graphiques 7/30 jours** (total d'erreurs du jour)
|
||||
|
||||
### Tableau temps réel (aujourd'hui)
|
||||
|
||||
- Colonnes : Utilisateur, Statut, Dernière action, Actions (24h), Erreurs, Présence / Actif, Depuis
|
||||
- Tri : actif → inactif → absent → déconnecté, puis dernière action la plus récente en premier
|
||||
|
||||
### Graphiques d'activité
|
||||
|
||||
- **7 jours** et **30 jours** — pic d'utilisateurs simultanés par jour
|
||||
- **Cliquer sur une barre** charge le tableau des utilisateurs de ce jour : login, première/dernière action, nombre d'actions, erreurs, présence, temps actif, nombre de sessions
|
||||
- **Tooltip sur les barres** affiche le nombre d'utilisateurs et le total d'erreurs du jour
|
||||
- **Cliquer sur un utilisateur** (tableau du jour ou tableau temps réel) affiche son historique individuel sur 7 ou 30 jours
|
||||
|
||||
### Détection des fichiers de logs
|
||||
|
||||
SuperVision gère les deux cas du serveur HDS :
|
||||
- Log du jour sans date dans le nom (`awevents.log`) — log actif courant
|
||||
- Log du jour avec date dans le nom et zippé (`awevents_26-04-13_1.log.gz`) — rotation en cours de journée (forte activité)
|
||||
|
||||
## Lancer les tests
|
||||
|
||||
```cmd
|
||||
cargo test
|
||||
```
|
||||
|
||||
## Sécurité
|
||||
|
||||
- Sessions HTTP sécurisées avec cookie de session
|
||||
- Mots de passe stockés avec bcrypt
|
||||
- En-têtes de sécurité HTTP automatiques (`X-Frame-Options`, `X-XSS-Protection`, `X-Content-Type-Options`)
|
||||
- Toutes les routes (sauf `/login`) nécessitent une authentification
|
||||
@@ -6,7 +6,7 @@ use lettre::{
|
||||
};
|
||||
|
||||
pub fn is_smtp_configured(smtp: &SmtpConfig) -> bool {
|
||||
!smtp.server.is_empty() && !smtp.from_email.is_empty() && !smtp.to_emails.is_empty()
|
||||
!smtp.from_email.is_empty() && !smtp.to_emails.is_empty() && !smtp.server.is_empty()
|
||||
}
|
||||
|
||||
pub struct Alerter;
|
||||
@@ -14,15 +14,15 @@ pub struct Alerter;
|
||||
impl Alerter {
|
||||
pub async fn send(&self, smtp: &SmtpConfig, subject: &str, body: &str) -> (bool, String) {
|
||||
if !is_smtp_configured(smtp) {
|
||||
return (false, "SMTP non configure".into());
|
||||
return (false, "Email non configure".into());
|
||||
}
|
||||
match self.send_email(smtp, subject, body).await {
|
||||
match self.send_via_smtp(smtp, subject, body).await {
|
||||
Ok(msg) => (true, msg),
|
||||
Err(e) => (false, e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_email(
|
||||
async fn send_via_smtp(
|
||||
&self,
|
||||
smtp: &SmtpConfig,
|
||||
subject: &str,
|
||||
@@ -78,7 +78,7 @@ impl Alerter {
|
||||
|
||||
pub async fn send_test(&self, smtp: &SmtpConfig) -> (bool, String) {
|
||||
let subject = "[TEST] Supervision - Test de configuration email";
|
||||
let body = "Ceci est un email de test.\n\nSi vous recevez ce message, la configuration SMTP est correcte.\n\n-- Supervision";
|
||||
let body = "Ceci est un email de test.\n\nSi vous recevez ce message, la configuration est correcte.\n\n-- Supervision";
|
||||
self.send(smtp, subject, body).await
|
||||
}
|
||||
}
|
||||
@@ -103,21 +103,8 @@ mod tests {
|
||||
password: "pass".into(),
|
||||
from_email: "from@example.com".into(),
|
||||
to_emails: vec!["to@example.com".into()],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(is_smtp_configured(&smtp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_configured_when_no_recipients() {
|
||||
let smtp = SmtpConfig {
|
||||
server: "smtp.example.com".into(),
|
||||
port: 587,
|
||||
use_tls: true,
|
||||
username: "user".into(),
|
||||
password: "pass".into(),
|
||||
from_email: "from@example.com".into(),
|
||||
to_emails: vec![],
|
||||
};
|
||||
assert!(!is_smtp_configured(&smtp));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ pub struct SmtpConfig {
|
||||
pub struct UserStatusThresholds {
|
||||
pub active_minutes: u64,
|
||||
pub inactive_minutes: u64,
|
||||
#[serde(default = "default_pause_threshold")]
|
||||
pub pause_threshold_minutes: u64,
|
||||
}
|
||||
|
||||
fn default_pause_threshold() -> u64 {
|
||||
20
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -101,6 +107,7 @@ impl Default for Config {
|
||||
user_status_thresholds: UserStatusThresholds {
|
||||
active_minutes: 5,
|
||||
inactive_minutes: 30,
|
||||
pause_threshold_minutes: 20,
|
||||
},
|
||||
admin: AdminConfig {
|
||||
username: "admin".into(),
|
||||
|
||||
@@ -28,7 +28,7 @@ use routes::{
|
||||
update_amadea_log_path, update_user_thresholds,
|
||||
},
|
||||
alerts::{alerts_get, clear_alerts},
|
||||
users::{users_get, api_users, api_users_weekly},
|
||||
users::{users_get, api_users, api_users_weekly, api_users_monthly, api_users_day, api_user_history},
|
||||
};
|
||||
|
||||
pub async fn run_server() {
|
||||
@@ -87,6 +87,9 @@ pub async fn run_server() {
|
||||
.route("/users", get(users_get))
|
||||
.route("/api/users", get(api_users))
|
||||
.route("/api/users/activity/weekly", get(api_users_weekly))
|
||||
.route("/api/users/activity/monthly", get(api_users_monthly))
|
||||
.route("/api/users/day/:date", get(api_users_day))
|
||||
.route("/api/users/:login/history", get(api_user_history))
|
||||
.nest_service("/static", ServeDir::new("static"))
|
||||
.layer(session_layer)
|
||||
.with_state(state.clone())
|
||||
|
||||
@@ -10,7 +10,7 @@ use axum::{
|
||||
http::request::Parts,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::sync::Arc;
|
||||
use tera::Tera;
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tower_sessions::Session;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::{Form, State},
|
||||
response::{IntoResponse, Redirect},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tower_sessions::Session;
|
||||
use form_urlencoded;
|
||||
|
||||
use crate::routes::{flash, get_and_clear_flash, render_html, AppState, AuthUser};
|
||||
|
||||
@@ -179,32 +181,30 @@ pub async fn test_smtp(
|
||||
Redirect::to("/settings")
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ProcessesForm {
|
||||
#[serde(rename = "proc_name[]")]
|
||||
pub proc_name: Option<Vec<String>>,
|
||||
#[serde(rename = "proc_pattern[]")]
|
||||
pub proc_pattern: Option<Vec<String>>,
|
||||
#[serde(rename = "proc_mem_threshold[]")]
|
||||
pub proc_mem_threshold: Option<Vec<String>>,
|
||||
#[serde(rename = "proc_enabled[]")]
|
||||
pub proc_enabled: Option<Vec<String>>,
|
||||
#[serde(rename = "proc_alert_down[]")]
|
||||
pub proc_alert_down: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub async fn update_processes(
|
||||
_auth: AuthUser,
|
||||
session: Session,
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<ProcessesForm>,
|
||||
body: Bytes,
|
||||
) -> impl IntoResponse {
|
||||
use crate::config::ProcessConfig;
|
||||
let names = form.proc_name.unwrap_or_default();
|
||||
let patterns = form.proc_pattern.unwrap_or_default();
|
||||
let mem_thresholds = form.proc_mem_threshold.unwrap_or_default();
|
||||
let enableds = form.proc_enabled.unwrap_or_default();
|
||||
let alert_downs = form.proc_alert_down.unwrap_or_default();
|
||||
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
let mut patterns: Vec<String> = Vec::new();
|
||||
let mut mem_thresholds: Vec<String> = Vec::new();
|
||||
let mut enableds: Vec<String> = Vec::new();
|
||||
let mut alert_downs: Vec<String> = Vec::new();
|
||||
|
||||
for (key, value) in form_urlencoded::parse(body.as_ref()) {
|
||||
match key.as_ref() {
|
||||
"proc_name[]" => names.push(value.into_owned()),
|
||||
"proc_pattern[]" => patterns.push(value.into_owned()),
|
||||
"proc_mem_threshold[]" => mem_thresholds.push(value.into_owned()),
|
||||
"proc_enabled[]" => enableds.push(value.into_owned()),
|
||||
"proc_alert_down[]" => alert_downs.push(value.into_owned()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut processes = Vec::new();
|
||||
for (i, name) in names.iter().enumerate() {
|
||||
@@ -361,6 +361,7 @@ pub async fn update_amadea_log_path(
|
||||
pub struct UserThresholdsForm {
|
||||
pub active_minutes: u64,
|
||||
pub inactive_minutes: u64,
|
||||
pub pause_threshold_minutes: u64,
|
||||
}
|
||||
|
||||
pub async fn update_user_thresholds(
|
||||
@@ -369,7 +370,7 @@ pub async fn update_user_thresholds(
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<UserThresholdsForm>,
|
||||
) -> impl IntoResponse {
|
||||
if form.active_minutes < 1 || form.inactive_minutes < 1 {
|
||||
if form.active_minutes < 1 || form.inactive_minutes < 1 || form.pause_threshold_minutes < 1 {
|
||||
flash(
|
||||
&session,
|
||||
"danger",
|
||||
@@ -391,6 +392,7 @@ pub async fn update_user_thresholds(
|
||||
let mut cm = state.config_manager.lock().await;
|
||||
cm.config.user_status_thresholds.active_minutes = form.active_minutes;
|
||||
cm.config.user_status_thresholds.inactive_minutes = form.inactive_minutes;
|
||||
cm.config.user_status_thresholds.pause_threshold_minutes = form.pause_threshold_minutes;
|
||||
cm.save();
|
||||
}
|
||||
flash(&session, "success", "Seuils utilisateurs mis à jour.").await;
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
extract::{Path, Query, State},
|
||||
response::{IntoResponse, Json},
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::routes::{get_and_clear_flash, render_html, AppState, AuthUser};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct HistoryQuery {
|
||||
#[serde(default = "default_days")]
|
||||
pub days: i64,
|
||||
}
|
||||
|
||||
fn default_days() -> i64 {
|
||||
7
|
||||
}
|
||||
|
||||
pub async fn users_get(
|
||||
_auth: AuthUser,
|
||||
session: Session,
|
||||
@@ -43,6 +55,10 @@ pub async fn api_users(
|
||||
"action_count_24h": u.action_count_24h,
|
||||
"connected_since": u.connected_since.map(|t| t.format("%H:%M").to_string()),
|
||||
"explicit_logout": u.explicit_logout,
|
||||
"session_count": u.session_count,
|
||||
"presence_str": u.presence_str,
|
||||
"active_time_str": u.active_time_str,
|
||||
"error_count": u.error_count,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -56,3 +72,34 @@ pub async fn api_users_weekly(
|
||||
let weekly = state.user_monitor.get_weekly_activity().await;
|
||||
Json(json!({ "weekly": weekly }))
|
||||
}
|
||||
|
||||
pub async fn api_users_monthly(
|
||||
_auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let monthly = state.user_monitor.get_monthly_activity().await;
|
||||
Json(json!({ "monthly": monthly }))
|
||||
}
|
||||
|
||||
pub async fn api_users_day(
|
||||
_auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(date_str): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let date = match NaiveDate::parse_from_str(&date_str, "%Y-%m-%d") {
|
||||
Ok(d) => d,
|
||||
Err(_) => return Json(json!({ "error": "Date invalide" })),
|
||||
};
|
||||
let users = state.user_monitor.get_users_for_date(date).await;
|
||||
Json(json!({ "users": users, "date": date_str }))
|
||||
}
|
||||
|
||||
pub async fn api_user_history(
|
||||
_auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(login): Path<String>,
|
||||
Query(q): Query<HistoryQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let history = state.user_monitor.get_user_history(&login, q.days).await;
|
||||
Json(json!({ "login": login, "history": history, "days": q.days }))
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use chrono::{Duration, Local, NaiveDateTime, Timelike};
|
||||
use chrono::{Duration, Local, NaiveDate, NaiveDateTime, Timelike};
|
||||
use flate2::read::GzDecoder;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
@@ -19,6 +21,13 @@ pub struct UserEntry {
|
||||
pub explicit_logout: bool,
|
||||
pub logout_time: Option<NaiveDateTime>,
|
||||
pub connected_since: Option<NaiveDateTime>,
|
||||
/// Timestamps collectés pendant le parsing — vidés après calcul de présence.
|
||||
#[serde(skip)]
|
||||
pub timestamps: Vec<NaiveDateTime>,
|
||||
pub session_count: u32,
|
||||
pub presence_str: Option<String>,
|
||||
pub active_time_str: Option<String>,
|
||||
pub error_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -35,23 +44,203 @@ pub struct UserData {
|
||||
pub no_files: bool,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers fichiers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn read_log_file(path: &std::path::PathBuf) -> Option<String> {
|
||||
if path.to_string_lossy().ends_with(".gz") {
|
||||
let file = fs::File::open(path).ok()?;
|
||||
let mut decoder = GzDecoder::new(file);
|
||||
let mut content = String::new();
|
||||
decoder.read_to_string(&mut content).ok()?;
|
||||
Some(content)
|
||||
} else {
|
||||
fs::read_to_string(path).ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn log_files_for_date(log_path: &Path, prefix: &str, date_str: &str) -> Vec<std::path::PathBuf> {
|
||||
let pattern = format!("{}/{}_{}_*", log_path.to_string_lossy(), prefix, date_str);
|
||||
let re = Regex::new(r"_(\d+)\.[^.]+$").unwrap();
|
||||
let mut files: Vec<_> = glob::glob(&pattern)
|
||||
let re_seq = Regex::new(r"_(\d+)\.log(\.gz)?$").unwrap();
|
||||
let re_has_date = Regex::new(r"_\d{2}-\d{2}-\d{2}[_.]").unwrap();
|
||||
|
||||
let pattern_with_date = format!("{}/{}_{}_*", log_path.to_string_lossy(), prefix, date_str);
|
||||
let mut files: Vec<_> = glob::glob(&pattern_with_date)
|
||||
.unwrap_or_else(|_| glob::glob("__nonexistent__").unwrap())
|
||||
.filter_map(|f| f.ok())
|
||||
.filter(|f| !f.to_string_lossy().ends_with(".zip"))
|
||||
.filter(|f| {
|
||||
let s = f.to_string_lossy();
|
||||
!s.ends_with(".zip") && (s.ends_with(".log") || s.ends_with(".log.gz"))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let today_str = Local::now().format("%y-%m-%d").to_string();
|
||||
if date_str == today_str {
|
||||
let pattern_active = format!("{}/{}*", log_path.to_string_lossy(), prefix);
|
||||
let active_files: Vec<_> = glob::glob(&pattern_active)
|
||||
.unwrap_or_else(|_| glob::glob("__nonexistent__").unwrap())
|
||||
.filter_map(|f| f.ok())
|
||||
.filter(|f| {
|
||||
let s = f.to_string_lossy();
|
||||
let fname = f.file_name().unwrap_or_default().to_string_lossy().to_string();
|
||||
!s.ends_with(".zip")
|
||||
&& (s.ends_with(".log") || s.ends_with(".log.gz"))
|
||||
&& !re_has_date.is_match(&fname)
|
||||
})
|
||||
.collect();
|
||||
files.extend(active_files);
|
||||
}
|
||||
|
||||
files.sort_by_key(|f| {
|
||||
re.captures(&f.to_string_lossy())
|
||||
re_seq
|
||||
.captures(&f.to_string_lossy())
|
||||
.and_then(|c| c.get(1))
|
||||
.and_then(|m| m.as_str().parse::<u32>().ok())
|
||||
.unwrap_or(0)
|
||||
});
|
||||
files.dedup();
|
||||
files
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Label : décodage URL et extraction du module
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn percent_decode(s: &str) -> String {
|
||||
let raw = s.as_bytes();
|
||||
let mut bytes: Vec<u8> = Vec::with_capacity(raw.len());
|
||||
let mut i = 0;
|
||||
while i < raw.len() {
|
||||
if raw[i] == b'%' && i + 2 < raw.len() {
|
||||
let hi = char::from(raw[i + 1]).to_digit(16);
|
||||
let lo = char::from(raw[i + 2]).to_digit(16);
|
||||
if let (Some(h), Some(l)) = (hi, lo) {
|
||||
bytes.push(((h << 4) | l) as u8);
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
bytes.push(raw[i]);
|
||||
i += 1;
|
||||
}
|
||||
String::from_utf8_lossy(&bytes).into_owned()
|
||||
}
|
||||
|
||||
/// Extrait le 2e segment du chemin et décode l'URL.
|
||||
/// "Main/Page%0ASyntheses/Accueil" → "Page Syntheses"
|
||||
/// Labels sans "/" → retourné tel quel (tronqué à 60 cars).
|
||||
pub fn extract_module_from_label(label: &str) -> String {
|
||||
let segments: Vec<&str> = label.splitn(4, '/').collect();
|
||||
let raw = match segments.get(1) {
|
||||
Some(s) if !s.is_empty() => *s,
|
||||
_ => return label.chars().take(60).collect(),
|
||||
};
|
||||
let decoded = percent_decode(raw);
|
||||
decoded
|
||||
.chars()
|
||||
.map(|c| if c.is_ascii_control() { ' ' } else { c })
|
||||
.collect::<String>()
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Calcul du temps de présence et du temps actif
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Retourne (presence_minutes, active_minutes).
|
||||
/// active = presence − somme des pauses > pause_threshold_minutes.
|
||||
pub fn compute_active_time(timestamps: &[NaiveDateTime], pause_threshold_minutes: u64) -> (i64, i64) {
|
||||
if timestamps.len() < 2 {
|
||||
return (0, 0);
|
||||
}
|
||||
let mut sorted = timestamps.to_vec();
|
||||
sorted.sort_unstable();
|
||||
let presence = (*sorted.last().unwrap() - sorted[0]).num_minutes().max(0);
|
||||
let pause_total: i64 = sorted
|
||||
.windows(2)
|
||||
.map(|w| (w[1] - w[0]).num_minutes().max(0))
|
||||
.filter(|&gap| gap as u64 > pause_threshold_minutes)
|
||||
.sum();
|
||||
let active = (presence - pause_total).max(0);
|
||||
(presence, active)
|
||||
}
|
||||
|
||||
fn format_duration(minutes: i64) -> String {
|
||||
if minutes <= 0 {
|
||||
"0min".to_string()
|
||||
} else if minutes >= 60 {
|
||||
format!("{}h{:02}", minutes / 60, minutes % 60)
|
||||
} else {
|
||||
format!("{}min", minutes)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Erreurs isoft : mapping session → login, comptage par utilisateur
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Parcourt les fichiers isoft et retourne login → nombre d'erreurs (lignes ERROR).
|
||||
/// Utilise `OpenUserSession` (ISI dans le thread) et `CloseUserSession` (ID dans le message)
|
||||
/// pour construire la table session_id → login.
|
||||
fn parse_isoft_errors(files: &[std::path::PathBuf]) -> HashMap<String, u32> {
|
||||
let re_isi = Regex::new(r"ISI=([A-Za-z0-9_]+)").unwrap();
|
||||
let re_close = Regex::new(r"CloseUserSession.*,ID=([A-Za-z0-9_]+)").unwrap();
|
||||
let re_login = Regex::new(r"login=([A-Za-z0-9_]+)").unwrap();
|
||||
|
||||
let mut session_to_login: HashMap<String, String> = HashMap::new();
|
||||
let mut error_sessions: Vec<String> = Vec::new();
|
||||
|
||||
for file in files {
|
||||
let Some(content) = read_log_file(file) else { continue };
|
||||
for line in content.lines() {
|
||||
if line.contains("OpenUserSession") {
|
||||
if let (Some(isi), Some(lg)) = (re_isi.captures(line), re_login.captures(line)) {
|
||||
session_to_login.entry(isi[1].to_string()).or_insert_with(|| lg[1].to_string());
|
||||
}
|
||||
}
|
||||
if line.contains("CloseUserSession") {
|
||||
if let (Some(id), Some(lg)) = (re_close.captures(line), re_login.captures(line)) {
|
||||
session_to_login.entry(id[1].to_string()).or_insert_with(|| lg[1].to_string());
|
||||
}
|
||||
}
|
||||
if line.contains(";ERROR;") {
|
||||
if let Some(isi) = re_isi.captures(line) {
|
||||
error_sessions.push(isi[1].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result: HashMap<String, u32> = HashMap::new();
|
||||
for sid in &error_sessions {
|
||||
if let Some(login) = session_to_login.get(sid) {
|
||||
*result.entry(login.clone()).or_default() += 1;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Compte le total de lignes ERROR dans les fichiers isoft (toutes sessions confondues).
|
||||
fn count_daily_errors(files: &[std::path::PathBuf]) -> u32 {
|
||||
let mut count = 0u32;
|
||||
for file in files {
|
||||
if let Some(content) = read_log_file(file) {
|
||||
for line in content.lines() {
|
||||
if line.contains(";ERROR;") {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parsing ligne par ligne
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn parse_awevents_line(
|
||||
line: &str,
|
||||
users: &mut HashMap<String, UserEntry>,
|
||||
@@ -80,22 +269,31 @@ pub fn parse_awevents_line(
|
||||
};
|
||||
|
||||
let is_logout = label.to_lowercase().contains("se deconnecter");
|
||||
let module_label = extract_module_from_label(&label);
|
||||
|
||||
let entry = users.entry(login.clone()).or_insert_with(|| UserEntry {
|
||||
login: login.clone(),
|
||||
last_action_time: ts,
|
||||
last_action_label: label.chars().take(60).collect(),
|
||||
last_action_label: module_label.clone(),
|
||||
action_count_24h: 0,
|
||||
status: "deconnecte".into(),
|
||||
explicit_logout: is_logout,
|
||||
logout_time: if is_logout { Some(ts) } else { None },
|
||||
connected_since: Some(ts),
|
||||
timestamps: Vec::new(),
|
||||
session_count: 1,
|
||||
presence_str: None,
|
||||
active_time_str: None,
|
||||
error_count: 0,
|
||||
});
|
||||
|
||||
entry.timestamps.push(ts);
|
||||
|
||||
if ts > entry.last_action_time {
|
||||
entry.last_action_time = ts;
|
||||
entry.last_action_label = label.chars().take(60).collect();
|
||||
entry.last_action_label = module_label;
|
||||
}
|
||||
|
||||
if is_logout {
|
||||
entry.explicit_logout = true;
|
||||
entry.logout_time = Some(ts);
|
||||
@@ -104,6 +302,7 @@ pub fn parse_awevents_line(
|
||||
if ts > lt {
|
||||
entry.explicit_logout = false;
|
||||
entry.logout_time = None;
|
||||
entry.session_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +314,12 @@ pub fn parse_awevents_line(
|
||||
hourly.entry(ts.hour() as u32).or_default().insert(login);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Statuts utilisateurs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// absent = inactif depuis > inactive_min SANS déconnexion explicite.
|
||||
/// deconnecte = déconnexion explicite OU (anciennement, même condition).
|
||||
pub fn compute_statuses(
|
||||
users: &mut HashMap<String, UserEntry>,
|
||||
active_min: u64,
|
||||
@@ -126,7 +331,7 @@ pub fn compute_statuses(
|
||||
user.status = if user.explicit_logout {
|
||||
"deconnecte".into()
|
||||
} else if delta > inactive_min {
|
||||
"deconnecte".into()
|
||||
"absent".into()
|
||||
} else if delta > active_min {
|
||||
"inactif".into()
|
||||
} else {
|
||||
@@ -135,6 +340,10 @@ pub fn compute_statuses(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UserMonitor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct UserMonitor {
|
||||
config_manager: Arc<AsyncMutex<ConfigManager>>,
|
||||
pub data: Arc<Mutex<UserData>>,
|
||||
@@ -151,12 +360,13 @@ impl UserMonitor {
|
||||
}
|
||||
|
||||
pub async fn parse_logs(&self) {
|
||||
let (log_path, active_min, inactive_min) = {
|
||||
let (log_path, active_min, inactive_min, pause_threshold_minutes) = {
|
||||
let cm = self.config_manager.lock().await;
|
||||
(
|
||||
cm.config.amadea_log_path.clone(),
|
||||
cm.config.user_status_thresholds.active_minutes,
|
||||
cm.config.user_status_thresholds.inactive_minutes,
|
||||
cm.config.user_status_thresholds.pause_threshold_minutes,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -189,7 +399,7 @@ impl UserMonitor {
|
||||
(0..24).map(|h| (h, HashSet::new())).collect();
|
||||
|
||||
for file in &awevents_files {
|
||||
if let Ok(content) = fs::read_to_string(file) {
|
||||
if let Some(content) = read_log_file(file) {
|
||||
for line in content.lines() {
|
||||
parse_awevents_line(line, &mut users, cutoff_24h, &mut hourly);
|
||||
}
|
||||
@@ -200,8 +410,9 @@ impl UserMonitor {
|
||||
r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*method=OpenUserSession.*login=([A-Za-z0-9_]+)",
|
||||
)
|
||||
.unwrap();
|
||||
for file in log_files_for_date(log_dir, "isoft", &date_str) {
|
||||
if let Ok(content) = fs::read_to_string(file) {
|
||||
let isoft_files: Vec<_> = log_files_for_date(log_dir, "isoft", &date_str);
|
||||
for file in &isoft_files {
|
||||
if let Some(content) = read_log_file(file) {
|
||||
for line in content.lines() {
|
||||
if let Some(m) = re_isoft.captures(line) {
|
||||
let login = m[2].to_string();
|
||||
@@ -218,16 +429,38 @@ impl UserMonitor {
|
||||
}
|
||||
}
|
||||
}
|
||||
let error_counts = parse_isoft_errors(&isoft_files);
|
||||
for (login, count) in &error_counts {
|
||||
if let Some(u) = users.get_mut(login) {
|
||||
u.error_count = *count;
|
||||
}
|
||||
}
|
||||
|
||||
// Calcul présence et temps actif, puis libération des timestamps
|
||||
for user in users.values_mut() {
|
||||
let (presence_mins, active_mins) =
|
||||
compute_active_time(&user.timestamps, pause_threshold_minutes);
|
||||
if presence_mins > 0 {
|
||||
user.presence_str = Some(format_duration(presence_mins));
|
||||
user.active_time_str = Some(format_duration(active_mins));
|
||||
}
|
||||
user.timestamps.clear();
|
||||
}
|
||||
|
||||
compute_statuses(&mut users, active_min, inactive_min, now);
|
||||
|
||||
let status_order = |s: &str| match s {
|
||||
"actif" => 0,
|
||||
"inactif" => 1,
|
||||
_ => 2,
|
||||
"absent" => 2,
|
||||
_ => 3,
|
||||
};
|
||||
let mut sorted: Vec<UserEntry> = users.into_values().collect();
|
||||
sorted.sort_by_key(|u| status_order(&u.status));
|
||||
sorted.sort_by(|a, b| {
|
||||
status_order(&a.status)
|
||||
.cmp(&status_order(&b.status))
|
||||
.then_with(|| b.last_action_time.cmp(&a.last_action_time))
|
||||
});
|
||||
|
||||
let hourly_data: Vec<HourlyCount> = {
|
||||
let mut v: Vec<_> = hourly.iter().collect();
|
||||
@@ -250,6 +483,15 @@ impl UserMonitor {
|
||||
}
|
||||
|
||||
pub async fn get_weekly_activity(&self) -> Vec<serde_json::Value> {
|
||||
self.get_peak_activity(7).await
|
||||
}
|
||||
|
||||
pub async fn get_monthly_activity(&self) -> Vec<serde_json::Value> {
|
||||
self.get_peak_activity(30).await
|
||||
}
|
||||
|
||||
/// Calcule le pic d'utilisateurs simultanés par jour sur `days` jours.
|
||||
async fn get_peak_activity(&self, days: i64) -> Vec<serde_json::Value> {
|
||||
let log_path = {
|
||||
let cm = self.config_manager.lock().await;
|
||||
cm.config.amadea_log_path.clone()
|
||||
@@ -266,7 +508,7 @@ impl UserMonitor {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for delta in (0..=6i64).rev() {
|
||||
for delta in (0..days).rev() {
|
||||
let day = today - Duration::days(delta);
|
||||
let date_str = day.format("%y-%m-%d").to_string();
|
||||
let files = log_files_for_date(log_dir, "awevents", &date_str);
|
||||
@@ -277,7 +519,7 @@ impl UserMonitor {
|
||||
let mut hourly: HashMap<u32, HashSet<String>> =
|
||||
(0..24u32).map(|h| (h, HashSet::new())).collect();
|
||||
for file in &files {
|
||||
if let Ok(content) = fs::read_to_string(file) {
|
||||
if let Some(content) = read_log_file(file) {
|
||||
for line in content.lines() {
|
||||
if let Some(m) = re.captures(line) {
|
||||
let hour: u32 = m[2].parse().unwrap_or(0);
|
||||
@@ -290,9 +532,172 @@ impl UserMonitor {
|
||||
}
|
||||
}
|
||||
let max_concurrent = hourly.values().map(|s| s.len()).max().unwrap_or(0);
|
||||
result.push(
|
||||
serde_json::json!({ "date": day.to_string(), "count": max_concurrent }),
|
||||
);
|
||||
let isoft_files = log_files_for_date(log_dir, "isoft", &date_str);
|
||||
let total_errors = count_daily_errors(&isoft_files);
|
||||
result.push(serde_json::json!({
|
||||
"date": day.to_string(),
|
||||
"count": max_concurrent,
|
||||
"errors": total_errors,
|
||||
}));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn get_users_for_date(&self, date: NaiveDate) -> Vec<serde_json::Value> {
|
||||
let (log_path, pause_threshold_minutes) = {
|
||||
let cm = self.config_manager.lock().await;
|
||||
(
|
||||
cm.config.amadea_log_path.clone(),
|
||||
cm.config.user_status_thresholds.pause_threshold_minutes,
|
||||
)
|
||||
};
|
||||
let log_dir = Path::new(&log_path);
|
||||
if !log_dir.is_dir() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let date_str = date.format("%y-%m-%d").to_string();
|
||||
let cutoff = date.and_hms_opt(0, 0, 0).unwrap_or_default();
|
||||
let files = log_files_for_date(log_dir, "awevents", &date_str);
|
||||
|
||||
let mut users: HashMap<String, UserEntry> = HashMap::new();
|
||||
let mut hourly: HashMap<u32, HashSet<String>> =
|
||||
(0..24).map(|h| (h, HashSet::new())).collect();
|
||||
|
||||
for file in &files {
|
||||
if let Some(content) = read_log_file(file) {
|
||||
for line in content.lines() {
|
||||
parse_awevents_line(line, &mut users, cutoff, &mut hourly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result: Vec<_> = users.into_values().collect();
|
||||
result.sort_by(|a, b| b.action_count_24h.cmp(&a.action_count_24h));
|
||||
|
||||
let isoft_files = log_files_for_date(log_dir, "isoft", &date_str);
|
||||
let error_counts = parse_isoft_errors(&isoft_files);
|
||||
|
||||
result
|
||||
.iter()
|
||||
.map(|u| {
|
||||
let (presence_mins, active_mins) =
|
||||
compute_active_time(&u.timestamps, pause_threshold_minutes);
|
||||
let first_action = u.timestamps.iter().min()
|
||||
.map(|t| t.format("%H:%M").to_string());
|
||||
serde_json::json!({
|
||||
"login": u.login,
|
||||
"last_action_time": u.last_action_time.format("%H:%M:%S").to_string(),
|
||||
"last_action_label": u.last_action_label,
|
||||
"action_count": u.action_count_24h,
|
||||
"first_action_time": first_action,
|
||||
"presence": if presence_mins > 0 { Some(format_duration(presence_mins)) } else { None },
|
||||
"active_time": if active_mins > 0 { Some(format_duration(active_mins)) } else { None },
|
||||
"sessions": u.session_count,
|
||||
"error_count": error_counts.get(&u.login).copied().unwrap_or(0),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Activité d'un utilisateur spécifique sur les `days` derniers jours (7 ou 30).
|
||||
pub async fn get_user_history(&self, login: &str, days: i64) -> Vec<serde_json::Value> {
|
||||
let days = days.clamp(1, 30);
|
||||
let (log_path, pause_threshold_minutes) = {
|
||||
let cm = self.config_manager.lock().await;
|
||||
(
|
||||
cm.config.amadea_log_path.clone(),
|
||||
cm.config.user_status_thresholds.pause_threshold_minutes,
|
||||
)
|
||||
};
|
||||
let log_dir = Path::new(&log_path);
|
||||
if !log_dir.is_dir() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let today = Local::now().date_naive();
|
||||
let re = Regex::new(
|
||||
r#"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d+;[^;]*;;;;"login=([^,]+),action=([^,]+),Label=(.+?)"?\s*$"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut result = Vec::new();
|
||||
for delta in (0..days).rev() {
|
||||
let day = today - Duration::days(delta);
|
||||
let date_str = day.format("%y-%m-%d").to_string();
|
||||
let files = log_files_for_date(log_dir, "awevents", &date_str);
|
||||
|
||||
if files.is_empty() {
|
||||
result.push(serde_json::json!({ "date": day.to_string(), "action_count": null }));
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut timestamps: Vec<NaiveDateTime> = Vec::new();
|
||||
let mut action_count: u32 = 0;
|
||||
let mut session_count: u32 = 1;
|
||||
let mut explicit_logout = false;
|
||||
let mut logout_time: Option<NaiveDateTime> = None;
|
||||
|
||||
for file in &files {
|
||||
if let Some(content) = read_log_file(file) {
|
||||
for line in content.lines() {
|
||||
let m = match re.captures(line) {
|
||||
Some(m) => m,
|
||||
None => continue,
|
||||
};
|
||||
if m[2].trim() != login {
|
||||
continue;
|
||||
}
|
||||
let ts = match NaiveDateTime::parse_from_str(&m[1], "%Y-%m-%d %H:%M:%S") {
|
||||
Ok(t) => t,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let is_logout = m[4].to_lowercase().contains("se deconnecter");
|
||||
|
||||
action_count += 1;
|
||||
timestamps.push(ts);
|
||||
|
||||
if is_logout {
|
||||
explicit_logout = true;
|
||||
logout_time = Some(ts);
|
||||
} else if explicit_logout {
|
||||
if let Some(lt) = logout_time {
|
||||
if ts > lt {
|
||||
explicit_logout = false;
|
||||
logout_time = None;
|
||||
session_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if action_count == 0 {
|
||||
result.push(serde_json::json!({ "date": day.to_string(), "action_count": null }));
|
||||
continue;
|
||||
}
|
||||
|
||||
timestamps.sort_unstable();
|
||||
let first_action = timestamps.first().map(|t| t.format("%H:%M").to_string());
|
||||
let last_action = timestamps.last().map(|t| t.format("%H:%M").to_string());
|
||||
let (presence_mins, active_mins) =
|
||||
compute_active_time(×tamps, pause_threshold_minutes);
|
||||
|
||||
let isoft_files = log_files_for_date(log_dir, "isoft", &date_str);
|
||||
let error_counts = parse_isoft_errors(&isoft_files);
|
||||
let user_errors = error_counts.get(login).copied().unwrap_or(0);
|
||||
|
||||
result.push(serde_json::json!({
|
||||
"date": day.to_string(),
|
||||
"action_count": action_count,
|
||||
"first_action": first_action,
|
||||
"last_action": last_action,
|
||||
"presence": if presence_mins > 0 { Some(format_duration(presence_mins)) } else { None },
|
||||
"active_time": if active_mins > 0 { Some(format_duration(active_mins)) } else { None },
|
||||
"sessions": session_count,
|
||||
"error_count": user_errors,
|
||||
}));
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -317,17 +722,38 @@ impl UserMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_user(login: &str, last: NaiveDateTime, explicit_logout: bool) -> UserEntry {
|
||||
UserEntry {
|
||||
login: login.into(),
|
||||
last_action_time: last,
|
||||
last_action_label: "test".into(),
|
||||
action_count_24h: 1,
|
||||
status: "deconnecte".into(),
|
||||
explicit_logout,
|
||||
logout_time: if explicit_logout { Some(last) } else { None },
|
||||
connected_since: Some(last),
|
||||
timestamps: vec![],
|
||||
session_count: 1,
|
||||
presence_str: None,
|
||||
active_time_str: None,
|
||||
error_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_awevents_line_extracts_user_and_action() {
|
||||
let line =
|
||||
r#"2026-04-07 14:23:45.123;server;;;;"login=jdupont,action=consulter,Label=Consulter dossier""#;
|
||||
let mut users = HashMap::new();
|
||||
let cutoff = chrono::Local::now().naive_local()
|
||||
- chrono::Duration::hours(25);
|
||||
let cutoff = chrono::Local::now().naive_local() - chrono::Duration::hours(25);
|
||||
let mut hourly = (0..24u32).map(|h| (h, HashSet::new())).collect();
|
||||
parse_awevents_line(line, &mut users, cutoff, &mut hourly);
|
||||
assert!(users.contains_key("jdupont"));
|
||||
@@ -347,20 +773,59 @@ mod tests {
|
||||
fn compute_statuses_marks_recent_as_active() {
|
||||
let now = chrono::Local::now().naive_local();
|
||||
let mut users = HashMap::new();
|
||||
users.insert(
|
||||
"alice".into(),
|
||||
UserEntry {
|
||||
login: "alice".into(),
|
||||
last_action_time: now,
|
||||
last_action_label: "test".into(),
|
||||
action_count_24h: 1,
|
||||
status: "deconnecte".into(),
|
||||
explicit_logout: false,
|
||||
logout_time: None,
|
||||
connected_since: Some(now),
|
||||
},
|
||||
);
|
||||
users.insert("alice".into(), make_user("alice", now, false));
|
||||
compute_statuses(&mut users, 5, 30, now);
|
||||
assert_eq!(users["alice"].status, "actif");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_statuses_absent_when_no_explicit_logout() {
|
||||
let now = chrono::Local::now().naive_local();
|
||||
let old = now - chrono::Duration::minutes(60);
|
||||
let mut users = HashMap::new();
|
||||
users.insert("bob".into(), make_user("bob", old, false));
|
||||
compute_statuses(&mut users, 5, 30, now);
|
||||
assert_eq!(users["bob"].status, "absent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_statuses_deconnecte_on_explicit_logout() {
|
||||
let now = chrono::Local::now().naive_local();
|
||||
let recent = now - chrono::Duration::minutes(1);
|
||||
let mut users = HashMap::new();
|
||||
users.insert("carol".into(), make_user("carol", recent, true));
|
||||
compute_statuses(&mut users, 5, 30, now);
|
||||
assert_eq!(users["carol"].status, "deconnecte");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_count_increments_on_reconnect() {
|
||||
let line1 = r#"2026-04-07 14:00:00.000;server;;;;"login=jdupont,action=consulter,Label=Main/Page/Accueil""#;
|
||||
let line2 = r#"2026-04-07 14:30:00.000;server;;;;"login=jdupont,action=quitter,Label=Se deconnecter""#;
|
||||
let line3 = r#"2026-04-07 15:00:00.000;server;;;;"login=jdupont,action=consulter,Label=Main/Page/Accueil""#;
|
||||
let mut users = HashMap::new();
|
||||
let cutoff =
|
||||
NaiveDateTime::parse_from_str("2026-04-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
|
||||
let mut hourly = (0..24u32).map(|h| (h, HashSet::new())).collect();
|
||||
parse_awevents_line(line1, &mut users, cutoff, &mut hourly);
|
||||
parse_awevents_line(line2, &mut users, cutoff, &mut hourly);
|
||||
parse_awevents_line(line3, &mut users, cutoff, &mut hourly);
|
||||
assert_eq!(users["jdupont"].session_count, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_module_from_path_label() {
|
||||
assert_eq!(
|
||||
extract_module_from_label("Main/Page%0ASyntheses/Accueil"),
|
||||
"Page Syntheses"
|
||||
);
|
||||
assert_eq!(
|
||||
extract_module_from_label("Consulter dossier"),
|
||||
"Consulter dossier"
|
||||
);
|
||||
assert_eq!(
|
||||
extract_module_from_label("Main/DossierPatient/Detail"),
|
||||
"DossierPatient"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
target/.DS_Store
vendored
Normal file
BIN
target/.DS_Store
vendored
Normal file
Binary file not shown.
1
target/.rustc_info.json
Normal file
1
target/.rustc_info.json
Normal file
@@ -0,0 +1 @@
|
||||
{"rustc_fingerprint":11362256309182063473,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/oussi/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.1 (e408947bf 2026-03-25)\nbinary: rustc\ncommit-hash: e408947bfd200af42db322daf0fadfe7e26d3bd1\ncommit-date: 2026-03-25\nhost: aarch64-apple-darwin\nrelease: 1.94.1\nLLVM version: 21.1.8\n","stderr":""},"6027984484328994041":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\nlib___.a\n___.dll\n/Users/oussi/.rustup/toolchains/stable-aarch64-apple-darwin\noff\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""}},"successes":{}}
|
||||
3
target/CACHEDIR.TAG
Normal file
3
target/CACHEDIR.TAG
Normal file
@@ -0,0 +1,3 @@
|
||||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by cargo.
|
||||
# For information about cache directory tags see https://bford.info/cachedir/
|
||||
BIN
target/debug/.DS_Store
vendored
Normal file
BIN
target/debug/.DS_Store
vendored
Normal file
Binary file not shown.
0
target/debug/.cargo-lock
Normal file
0
target/debug/.cargo-lock
Normal file
BIN
target/debug/.fingerprint/adler2-c7e8d4d29fedd94f/dep-lib-adler2
Normal file
BIN
target/debug/.fingerprint/adler2-c7e8d4d29fedd94f/dep-lib-adler2
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
45fa2d90705c3738
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[]","declared_features":"[\"core\", \"default\", \"rustc-dep-of-std\", \"std\"]","target":6569825234462323107,"profile":5347358027863023418,"path":13865193194691325206,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/adler2-c7e8d4d29fedd94f/dep-lib-adler2","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
813f18dedd477879
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"default\", \"perf-literal\", \"std\"]","declared_features":"[\"default\", \"logging\", \"perf-literal\", \"std\"]","target":7534583537114156500,"profile":5347358027863023418,"path":4756945086599699681,"deps":[[1363051979936526615,"memchr",false,109361320982092791]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aho-corasick-51b1aa38875d91ab/dep-lib-aho_corasick","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
e49754717aec48cd
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[]","declared_features":"[]","target":5116616278641129243,"profile":3033921117576893,"path":9569430659015419926,"deps":[[4289358735036141001,"proc_macro2",false,14434019325570031488],[10420560437213941093,"syn",false,15033751812477789552],[13111758008314797071,"quote",false,15466326529690543119]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-trait-4d24afdfaf5578bb/dep-lib-async_trait","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
ae903d2998de7442
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[]","declared_features":"[\"portable-atomic\"]","target":14411119108718288063,"profile":5347358027863023418,"path":5906372349847045464,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/atomic-waker-2ae53bd7dc42b1c5/dep-lib-atomic_waker","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
dc498293fcd160e6
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[]","declared_features":"[]","target":6962977057026645649,"profile":3033921117576893,"path":15486874332424941,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/autocfg-12f89cc5520a8a2d/dep-lib-autocfg","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
target/debug/.fingerprint/axum-03015be23fc8692d/dep-lib-axum
Normal file
BIN
target/debug/.fingerprint/axum-03015be23fc8692d/dep-lib-axum
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
1
target/debug/.fingerprint/axum-03015be23fc8692d/lib-axum
Normal file
1
target/debug/.fingerprint/axum-03015be23fc8692d/lib-axum
Normal file
@@ -0,0 +1 @@
|
||||
2df9339f4c62aac3
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"default\", \"form\", \"http1\", \"json\", \"macros\", \"matched-path\", \"original-uri\", \"query\", \"tokio\", \"tower-log\", \"tracing\"]","declared_features":"[\"__private_docs\", \"default\", \"form\", \"http1\", \"http2\", \"json\", \"macros\", \"matched-path\", \"multipart\", \"original-uri\", \"query\", \"tokio\", \"tower-log\", \"tracing\", \"ws\"]","target":13920321295547257648,"profile":5347358027863023418,"path":7907914990833577576,"deps":[[784494742817713399,"tower_service",false,7841524536641349667],[1363051979936526615,"memchr",false,109361320982092791],[2251399859588827949,"pin_project_lite",false,16008755560344406462],[2517136641825875337,"sync_wrapper",false,2262787393906636474],[2620434475832828286,"http",false,6071307373105905826],[3626672138398771397,"hyper",false,3679066200079735321],[3632162862999675140,"tower",false,8865620563452385240],[3870702314125662939,"bytes",false,2841784438664644701],[4246786359834650171,"tokio",false,10896739638684196198],[4359148418957042248,"axum_core",false,8729791837833250442],[5532778797167691009,"itoa",false,6451755352924673826],[5898568623609459682,"futures_util",false,16386763764708151285],[6803352382179706244,"percent_encoding",false,10755141293663985388],[7712452662827335977,"tower_layer",false,14491143023105329304],[7940089053034940860,"axum_macros",false,5676342163752640776],[9678799920983747518,"matchit",false,11028109377757724220],[10229185211513642314,"mime",false,12540024495573264604],[11976082518617474977,"hyper_util",false,17177556051408080709],[13548984313718623784,"serde",false,4755198247368830486],[13795362694956882968,"serde_json",false,14317311717721046485],[14084095096285906100,"http_body",false,9737993903290812655],[14156967978702956262,"rustversion",false,16981750216962319693],[14757622794040968908,"tracing",false,17213290732951300318],[14814583949208169760,"serde_path_to_error",false,11248637185294131992],[16542808166767769916,"serde_urlencoded",false,18036353298262058364],[16611674984963787466,"async_trait",false,14792332986729928676],[16900715236047033623,"http_body_util",false,16645273110637047968]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/axum-03015be23fc8692d/dep-lib-axum","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
target/debug/.fingerprint/axum-728d8b94a3ac7891/dep-lib-axum
Normal file
BIN
target/debug/.fingerprint/axum-728d8b94a3ac7891/dep-lib-axum
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
1
target/debug/.fingerprint/axum-728d8b94a3ac7891/lib-axum
Normal file
1
target/debug/.fingerprint/axum-728d8b94a3ac7891/lib-axum
Normal file
@@ -0,0 +1 @@
|
||||
3958e49c55d4e2f2
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"default\", \"form\", \"http1\", \"json\", \"macros\", \"matched-path\", \"original-uri\", \"query\", \"tokio\", \"tower-log\", \"tracing\"]","declared_features":"[\"__private_docs\", \"default\", \"form\", \"http1\", \"http2\", \"json\", \"macros\", \"matched-path\", \"multipart\", \"original-uri\", \"query\", \"tokio\", \"tower-log\", \"tracing\", \"ws\"]","target":13920321295547257648,"profile":5347358027863023418,"path":7907914990833577576,"deps":[[784494742817713399,"tower_service",false,7841524536641349667],[1363051979936526615,"memchr",false,109361320982092791],[2251399859588827949,"pin_project_lite",false,16008755560344406462],[2517136641825875337,"sync_wrapper",false,18279144976257510731],[2620434475832828286,"http",false,6071307373105905826],[3626672138398771397,"hyper",false,5089235541509902119],[3632162862999675140,"tower",false,55602173807675257],[3870702314125662939,"bytes",false,2841784438664644701],[4246786359834650171,"tokio",false,10896739638684196198],[4359148418957042248,"axum_core",false,9949484912272237105],[5532778797167691009,"itoa",false,6451755352924673826],[5898568623609459682,"futures_util",false,7397329748735179940],[6803352382179706244,"percent_encoding",false,10755141293663985388],[7712452662827335977,"tower_layer",false,14491143023105329304],[7940089053034940860,"axum_macros",false,5676342163752640776],[9678799920983747518,"matchit",false,11028109377757724220],[10229185211513642314,"mime",false,12540024495573264604],[11976082518617474977,"hyper_util",false,10701819961723662506],[13548984313718623784,"serde",false,4755198247368830486],[13795362694956882968,"serde_json",false,14317311717721046485],[14084095096285906100,"http_body",false,9737993903290812655],[14156967978702956262,"rustversion",false,16981750216962319693],[14757622794040968908,"tracing",false,17213290732951300318],[14814583949208169760,"serde_path_to_error",false,11248637185294131992],[16542808166767769916,"serde_urlencoded",false,18036353298262058364],[16611674984963787466,"async_trait",false,14792332986729928676],[16900715236047033623,"http_body_util",false,16645273110637047968]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/axum-728d8b94a3ac7891/dep-lib-axum","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
8ac627916c732679
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"tracing\"]","declared_features":"[\"__private_docs\", \"tracing\"]","target":2565713999752801252,"profile":5347358027863023418,"path":13979972293121727953,"deps":[[784494742817713399,"tower_service",false,7841524536641349667],[2251399859588827949,"pin_project_lite",false,16008755560344406462],[2517136641825875337,"sync_wrapper",false,2262787393906636474],[2620434475832828286,"http",false,6071307373105905826],[3870702314125662939,"bytes",false,2841784438664644701],[5898568623609459682,"futures_util",false,16386763764708151285],[7712452662827335977,"tower_layer",false,14491143023105329304],[10229185211513642314,"mime",false,12540024495573264604],[14084095096285906100,"http_body",false,9737993903290812655],[14156967978702956262,"rustversion",false,16981750216962319693],[14757622794040968908,"tracing",false,17213290732951300318],[16611674984963787466,"async_trait",false,14792332986729928676],[16900715236047033623,"http_body_util",false,16645273110637047968]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/axum-core-0aa5bfffba341c65/dep-lib-axum_core","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
31621826d0ab138a
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"tracing\"]","declared_features":"[\"__private_docs\", \"tracing\"]","target":2565713999752801252,"profile":5347358027863023418,"path":13979972293121727953,"deps":[[784494742817713399,"tower_service",false,7841524536641349667],[2251399859588827949,"pin_project_lite",false,16008755560344406462],[2517136641825875337,"sync_wrapper",false,18279144976257510731],[2620434475832828286,"http",false,6071307373105905826],[3870702314125662939,"bytes",false,2841784438664644701],[5898568623609459682,"futures_util",false,7397329748735179940],[7712452662827335977,"tower_layer",false,14491143023105329304],[10229185211513642314,"mime",false,12540024495573264604],[14084095096285906100,"http_body",false,9737993903290812655],[14156967978702956262,"rustversion",false,16981750216962319693],[14757622794040968908,"tracing",false,17213290732951300318],[16611674984963787466,"async_trait",false,14792332986729928676],[16900715236047033623,"http_body_util",false,16645273110637047968]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/axum-core-7cc7d47275f878a6/dep-lib-axum_core","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
08610c961f6bc64e
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"default\"]","declared_features":"[\"__private\", \"default\"]","target":7759748055708476646,"profile":3033921117576893,"path":8690295758019298297,"deps":[[4289358735036141001,"proc_macro2",false,14434019325570031488],[10420560437213941093,"syn",false,15033751812477789552],[13111758008314797071,"quote",false,15466326529690543119]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/axum-macros-841d8d16b5b5f282/dep-lib-axum_macros","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
target/debug/.fingerprint/base64-a2c323877c4533ad/dep-lib-base64
Normal file
BIN
target/debug/.fingerprint/base64-a2c323877c4533ad/dep-lib-base64
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
2c22eceb0fe0b539
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":5347358027863023418,"path":3131682540330081973,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/base64-a2c323877c4533ad/dep-lib-base64","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
target/debug/.fingerprint/bcrypt-3fe0c668338e92e1/dep-lib-bcrypt
Normal file
BIN
target/debug/.fingerprint/bcrypt-3fe0c668338e92e1/dep-lib-bcrypt
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
061c6300d6ffc6cc
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"default\", \"getrandom\", \"std\", \"zeroize\"]","declared_features":"[\"alloc\", \"default\", \"getrandom\", \"js\", \"std\", \"zeroize\"]","target":15699326785376903934,"profile":5347358027863023418,"path":15170100589542242193,"deps":[[11023519408959114924,"getrandom",false,2681344337742186397],[12865141776541797048,"zeroize",false,7666445269755055630],[13077212702700853852,"base64",false,4158476189933773356],[14723042243959528973,"blowfish",false,10466087116963799369],[17003143334332120809,"subtle",false,7164567650919969281]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bcrypt-3fe0c668338e92e1/dep-lib-bcrypt","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
7c5e4de174fe897d
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"std\"]","declared_features":"[\"arbitrary\", \"bytemuck\", \"example_generated\", \"serde\", \"serde_core\", \"std\"]","target":7691312148208718491,"profile":5347358027863023418,"path":1987038683704404053,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bitflags-d51cdd8650beb82a/dep-lib-bitflags","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
4961ebfac7023f91
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"bcrypt\"]","declared_features":"[\"bcrypt\", \"zeroize\"]","target":2484384566325761644,"profile":5347358027863023418,"path":6418748496445189202,"deps":[[3712811570531045576,"byteorder",false,821971137506136315],[7916416211798676886,"cipher",false,5186578246651896791]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/blowfish-3459955acbf548eb/dep-lib-blowfish","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
target/debug/.fingerprint/bstr-44d4a91cc184b786/dep-lib-bstr
Normal file
BIN
target/debug/.fingerprint/bstr-44d4a91cc184b786/dep-lib-bstr
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
1
target/debug/.fingerprint/bstr-44d4a91cc184b786/lib-bstr
Normal file
1
target/debug/.fingerprint/bstr-44d4a91cc184b786/lib-bstr
Normal file
@@ -0,0 +1 @@
|
||||
244df001204922a8
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"alloc\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"serde\", \"std\", \"unicode\"]","target":3845652121355691695,"profile":5347358027863023418,"path":3956625926827952434,"deps":[[1363051979936526615,"memchr",false,109361320982092791]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bstr-44d4a91cc184b786/dep-lib-bstr","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
fb50ad02653a680b
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[]","declared_features":"[\"default\", \"i128\", \"std\"]","target":8344828840634961491,"profile":5347358027863023418,"path":6690685362319802925,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/byteorder-f2762c17ee1cc8bb/dep-lib-byteorder","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
target/debug/.fingerprint/bytes-90e86e4880046216/dep-lib-bytes
Normal file
BIN
target/debug/.fingerprint/bytes-90e86e4880046216/dep-lib-bytes
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
5dc8d6fae30b7027
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"extra-platforms\", \"serde\", \"std\"]","target":11402411492164584411,"profile":7855341030452660939,"path":11883909183712569867,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bytes-90e86e4880046216/dep-lib-bytes","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
target/debug/.fingerprint/cfg-if-a63920a22c723b43/dep-lib-cfg_if
Normal file
BIN
target/debug/.fingerprint/cfg-if-a63920a22c723b43/dep-lib-cfg_if
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
2f2863bbae8fbc9d
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[]","declared_features":"[\"core\", \"rustc-dep-of-std\"]","target":13840298032947503755,"profile":5347358027863023418,"path":4985536019908600227,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/cfg-if-a63920a22c723b43/dep-lib-cfg_if","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
BIN
target/debug/.fingerprint/chrono-6018f8ab58dee75b/dep-lib-chrono
Normal file
BIN
target/debug/.fingerprint/chrono-6018f8ab58dee75b/dep-lib-chrono
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
f6c5a0f79d7ffc39
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[\"alloc\", \"clock\", \"default\", \"iana-time-zone\", \"js-sys\", \"now\", \"oldtime\", \"serde\", \"std\", \"wasm-bindgen\", \"wasmbind\", \"winapi\", \"windows-link\"]","declared_features":"[\"__internal_bench\", \"alloc\", \"arbitrary\", \"clock\", \"core-error\", \"default\", \"defmt\", \"iana-time-zone\", \"js-sys\", \"libc\", \"now\", \"oldtime\", \"pure-rust-locales\", \"rkyv\", \"rkyv-16\", \"rkyv-32\", \"rkyv-64\", \"rkyv-validation\", \"serde\", \"std\", \"unstable-locales\", \"wasm-bindgen\", \"wasmbind\", \"winapi\", \"windows-link\"]","target":15315924755136109342,"profile":5347358027863023418,"path":6233380245842804825,"deps":[[5157631553186200874,"num_traits",false,269881187328155962],[13548984313718623784,"serde",false,4755198247368830486],[16619627449254928351,"iana_time_zone",false,891644702064754823]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/chrono-6018f8ab58dee75b/dep-lib-chrono","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
14a1ade71959c13b
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":17940977064402226622,"features":"[]","declared_features":"[\"case-insensitive\", \"filter-by-regex\", \"regex\", \"uncased\"]","target":16403465266122158524,"profile":3033921117576893,"path":18333616885213520549,"deps":[[1280075590338009456,"phf_codegen",false,5873881603926401952],[12335805432749277816,"parse_zoneinfo",false,7227311656393003800],[17186037756130803222,"phf",false,10581565088822326824]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/chrono-tz-build-1fb292845184b349/dep-lib-chrono_tz_build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user