6 Commits

Author SHA1 Message Date
oussi
ebd5482070 V1.0.3 2026-04-27 16:41:16 +02:00
oussi
607ff0629c V1.0.2 2026-04-27 14:15:30 +02:00
oussi
c7892748dc V1.0.2 2026-04-27 12:03:08 +02:00
oussi
ca69337afb Améliorations 2026-04-20 16:47:15 +02:00
oussi
ddfa84cfea nouvelle version 2026-04-13 16:03:08 +02:00
oussi
eb62e74f08 Ajout du Readme 2026-04-07 15:48:56 +02:00
7231 changed files with 16565 additions and 182 deletions

BIN
.DS_Store vendored

Binary file not shown.

43
.gitignore vendored
View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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

View File

@@ -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));
}
}

View File

@@ -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(),

View File

@@ -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())

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 }))
}

View File

@@ -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(&timestamps, 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

Binary file not shown.

1
target/.rustc_info.json Normal file
View 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
View 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

Binary file not shown.

0
target/debug/.cargo-lock Normal file
View File

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
45fa2d90705c3738

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
813f18dedd477879

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
e49754717aec48cd

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
ae903d2998de7442

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
dc498293fcd160e6

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
2df9339f4c62aac3

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
3958e49c55d4e2f2

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
8ac627916c732679

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
31621826d0ab138a

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
08610c961f6bc64e

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
2c22eceb0fe0b539

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
061c6300d6ffc6cc

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
7c5e4de174fe897d

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
4961ebfac7023f91

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
244df001204922a8

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
fb50ad02653a680b

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
5dc8d6fae30b7027

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
2f2863bbae8fbc9d

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
f6c5a0f79d7ffc39

View File

@@ -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}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
14a1ade71959c13b

View File

@@ -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}

View File

@@ -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