This commit is contained in:
oussi
2026-04-27 12:03:08 +02:00
parent ca69337afb
commit c7892748dc
2737 changed files with 2376 additions and 861 deletions

BIN
.DS_Store vendored

Binary file not shown.

301
Cargo.lock generated
View File

@@ -267,16 +267,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@@ -405,15 +395,6 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -452,12 +433,6 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
@@ -627,25 +602,6 @@ dependencies = [
"walkdir",
]
[[package]]
name = "h2"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -748,7 +704,6 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
"h2",
"http",
"http-body",
"httparse",
@@ -757,38 +712,6 @@ dependencies = [
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"tokio",
"tokio-rustls",
"tower-service",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
@@ -797,23 +720,13 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@@ -986,22 +899,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "ipnet"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "iri-string"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -1014,8 +911,6 @@ version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
@@ -1549,60 +1444,6 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "reqwest"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
"encoding_rs",
"futures-core",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-tls",
"hyper-util",
"js-sys",
"log",
"mime",
"native-tls",
"percent-encoding",
"pin-project-lite",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-native-tls",
"tower 0.5.3",
"tower-http 0.6.8",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustix"
version = "1.1.4"
@@ -1616,39 +1457,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
dependencies = [
"once_cell",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -1692,7 +1500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags",
"core-foundation 0.10.1",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -1887,7 +1695,6 @@ dependencies = [
"lettre",
"rand",
"regex",
"reqwest",
"serde",
"serde_json",
"sysinfo",
@@ -1895,7 +1702,7 @@ dependencies = [
"tera",
"tokio",
"tower 0.4.13",
"tower-http 0.5.2",
"tower-http",
"tower-sessions",
"tracing",
"tracing-subscriber",
@@ -1918,9 +1725,6 @@ name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
@@ -1947,27 +1751,6 @@ dependencies = [
"windows",
]
[[package]]
name = "system-configuration"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -2111,16 +1894,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
@@ -2203,24 +1976,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-http"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags",
"bytes",
"futures-util",
"http",
"http-body",
"iri-string",
"pin-project-lite",
"tower 0.5.3",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
@@ -2346,12 +2101,6 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.19.0"
@@ -2388,12 +2137,6 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.8"
@@ -2440,15 +2183,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@@ -2486,16 +2220,6 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.118"
@@ -2562,16 +2286,6 @@ dependencies = [
"semver",
]
[[package]]
name = "web-sys"
version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "widestring"
version = "1.2.1"
@@ -2694,17 +2408,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link",
"windows-result 0.4.1",
"windows-strings",
]
[[package]]
name = "windows-result"
version = "0.1.2"

View File

@@ -29,10 +29,14 @@ regex = "1"
glob = "0.3"
flate2 = "1"
form_urlencoded = "1"
reqwest = { version = "0.12", features = ["json", "native-tls"] }
[target.'cfg(windows)'.dependencies]
windows-service = "0.7"
[dev-dependencies]
tempfile = "3"
[profile.release]
opt-level = 3
lto = true
strip = true
codegen-units = 1

View File

@@ -7,7 +7,7 @@ Outil de supervision système avec interface web, écrit en Rust. Surveille CPU,
- **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/déconnecté, graphe d'activité horaire et hebdomadaire
- **Suivi utilisateurs Amadea** — analyse des logs `awevents` et `isoft`, statuts actif/inactif/absent/déconnecté, temps de présence et temps actif, 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
@@ -119,16 +119,36 @@ 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é
### Tableau temps réel (aujourd'hui)
- Colonnes : Utilisateur, Statut, Dernière action, Actions (24h), Depuis
- Tri : statut (actif → inactif → déconnecté), puis dernière action la plus récente en premier au sein de chaque groupe
- Colonnes : Utilisateur, Statut, Dernière action, Actions (24h), Connecté depuis, Présence, Temps actif, Sessions
- Tri : actif → inactif → absent → déconnecté, puis dernière action la plus récente en premier
### Graphique 7 derniers jours
### Graphiques d'activité
- Affiche le pic d'utilisateurs simultanés par jour
- **Cliquer sur une barre** charge le tableau des utilisateurs de ce jour : Utilisateur, Dernière utilisation, Actions (jour), Durée de présence (première → dernière action)
- Tri par nombre d'actions décroissant
- **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, présence, temps actif, nombre de sessions
- **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
@@ -136,8 +156,6 @@ 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é)
Les seuils de statut (actif / inactif / déconnecté) sont configurables en minutes.
## Lancer les tests
```cmd

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, api_users_day},
users::{users_get, api_users, api_users_weekly, api_users_monthly, api_users_day, api_user_history},
};
pub async fn run_server() {
@@ -87,7 +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

@@ -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,13 +1,24 @@
use axum::{
extract::{Path, 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,
@@ -44,6 +55,9 @@ 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,
})
})
.collect();
@@ -58,6 +72,14 @@ pub async fn api_users_weekly(
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>,
@@ -70,3 +92,13 @@ pub async fn api_users_day(
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

@@ -21,6 +21,12 @@ 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>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -37,6 +43,10 @@ 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()?;
@@ -51,10 +61,8 @@ fn read_log_file(path: &std::path::PathBuf) -> Option<String> {
fn log_files_for_date(log_path: &Path, prefix: &str, date_str: &str) -> Vec<std::path::PathBuf> {
let re_seq = Regex::new(r"_(\d+)\.log(\.gz)?$").unwrap();
// Détecte une date YY-MM-DD dans le nom de fichier
let re_has_date = Regex::new(r"_\d{2}-\d{2}-\d{2}[_.]").unwrap();
// Fichiers avec la date du jour dans le nom (ex: awevents_26-04-13_1.log.gz)
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())
@@ -65,8 +73,6 @@ fn log_files_for_date(log_path: &Path, prefix: &str, date_str: &str) -> Vec<std:
})
.collect();
// Pour le jour courant uniquement : inclure aussi les fichiers sans date dans le nom
// (ex: isoft.log, awevents.log) — ce sont les logs actifs du jour
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);
@@ -95,6 +101,85 @@ fn log_files_for_date(log_path: &Path, prefix: &str, date_str: &str) -> Vec<std:
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)
}
}
// ---------------------------------------------------------------------------
// Parsing ligne par ligne
// ---------------------------------------------------------------------------
pub fn parse_awevents_line(
line: &str,
users: &mut HashMap<String, UserEntry>,
@@ -123,22 +208,30 @@ 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,
});
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);
@@ -147,6 +240,7 @@ pub fn parse_awevents_line(
if ts > lt {
entry.explicit_logout = false;
entry.logout_time = None;
entry.session_count += 1;
}
}
}
@@ -158,6 +252,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,
@@ -169,7 +269,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 {
@@ -178,6 +278,10 @@ pub fn compute_statuses(
}
}
// ---------------------------------------------------------------------------
// UserMonitor
// ---------------------------------------------------------------------------
pub struct UserMonitor {
config_manager: Arc<AsyncMutex<ConfigManager>>,
pub data: Arc<Mutex<UserData>>,
@@ -194,12 +298,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,
)
};
@@ -232,7 +337,7 @@ impl UserMonitor {
(0..24).map(|h| (h, HashSet::new())).collect();
for file in &awevents_files {
if let Some(content) = read_log_file(&file) {
if let Some(content) = read_log_file(file) {
for line in content.lines() {
parse_awevents_line(line, &mut users, cutoff_24h, &mut hourly);
}
@@ -262,12 +367,24 @@ impl UserMonitor {
}
}
// 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(|a, b| {
@@ -297,6 +414,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()
@@ -313,7 +439,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);
@@ -324,7 +450,7 @@ impl UserMonitor {
let mut hourly: HashMap<u32, HashSet<String>> =
(0..24u32).map(|h| (h, HashSet::new())).collect();
for file in &files {
if let Some(content) = read_log_file(&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);
@@ -337,17 +463,18 @@ 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 }),
);
result.push(serde_json::json!({ "date": day.to_string(), "count": max_concurrent }));
}
result
}
pub async fn get_users_for_date(&self, date: NaiveDate) -> Vec<serde_json::Value> {
let log_path = {
let (log_path, pause_threshold_minutes) = {
let cm = self.config_manager.lock().await;
cm.config.amadea_log_path.clone()
(
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() {
@@ -376,26 +503,121 @@ impl UserMonitor {
result
.iter()
.map(|u| {
let duration = u.connected_since.map(|since| {
let mins = (u.last_action_time - since).num_minutes().max(0);
if mins >= 60 {
format!("{}h{:02}", mins / 60, mins % 60)
} else {
format!("{}min", mins)
}
});
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": u.connected_since.map(|t| t.format("%H:%M").to_string()),
"duration": duration,
"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,
})
})
.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);
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,
}));
}
result
}
pub async fn start(self: Arc<Self>) {
self.running
.store(true, std::sync::atomic::Ordering::Relaxed);
@@ -416,17 +638,37 @@ 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,
}
}
#[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"));
@@ -446,20 +688,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

Binary file not shown.

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

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"default\", \"link\"]","declared_features":"[\"chrono\", \"default\", \"link\", \"mac_os_10_7_support\", \"mac_os_10_8_features\", \"uuid\", \"with-chrono\", \"with-uuid\"]","target":3908465493571680068,"profile":5347358027863023418,"path":19410967586775136,"deps":[[12111499963430175700,"libc",false,13910580885449283722],[12589608519315293066,"core_foundation_sys",false,8160079812100183975]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/core-foundation-6bbb3be68939ef19/dep-lib-core_foundation","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[7312356825837975969,"build_script_build",false,14065107951882003722]],"local":[{"Precalculated":"1.5.0"}],"rustflags":[],"config":0,"compile_kind":0}

View File

@@ -0,0 +1 @@
0ae5727f294d31c3

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"nightly\", \"std\"]","target":5408242616063297496,"profile":3033921117576893,"path":14861366854762025859,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crc32fast-65b82aa2d109d340/dep-build-script-build-script-build","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 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
c9f6b5c51668131a

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"nightly\", \"std\"]","target":10823605331999153028,"profile":5347358027863023418,"path":14774993016417964939,"deps":[[7312356825837975969,"build_script_build",false,15447005731378063728],[7667230146095136825,"cfg_if",false,11366117540207208495]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crc32fast-a3eb51766abb8d60/dep-lib-crc32fast","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 @@
a41e89eaeb5a5241

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"alloc\", \"default\"]","declared_features":"[\"alloc\", \"any_all_workaround\", \"default\", \"fast-big5-hanzi-encode\", \"fast-gb-hanzi-encode\", \"fast-hangul-encode\", \"fast-hanja-encode\", \"fast-kanji-encode\", \"fast-legacy-encode\", \"less-slow-big5-hanzi-encode\", \"less-slow-gb-hanzi-encode\", \"less-slow-kanji-encode\", \"serde\", \"simd-accel\"]","target":17616512236202378241,"profile":5347358027863023418,"path":1886017213754624091,"deps":[[7667230146095136825,"cfg_if",false,11366117540207208495]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/encoding_rs-562ec32128eb1a5c/dep-lib-encoding_rs","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 @@
29c0e62a5a7e0ac5

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[]","declared_features":"[]","target":1524667692659508025,"profile":5347358027863023418,"path":3153973420102061108,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/equivalent-3142557ee15390fc/dep-lib-equivalent","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 @@
b66b64172818108b

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"any_impl\", \"default\", \"miniz_oxide\", \"rust_backend\"]","declared_features":"[\"any_c_zlib\", \"any_impl\", \"any_zlib\", \"cloudflare-zlib-sys\", \"cloudflare_zlib\", \"default\", \"document-features\", \"libz-ng-sys\", \"libz-sys\", \"miniz-sys\", \"miniz_oxide\", \"rust_backend\", \"zlib\", \"zlib-default\", \"zlib-ng\", \"zlib-ng-compat\", \"zlib-rs\"]","target":6173716359330453699,"profile":5347358027863023418,"path":3428011456485605323,"deps":[[7312356825837975969,"crc32fast",false,1878959916559234761],[7636735136738807108,"miniz_oxide",false,3793601602213432242]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/flate2-401cd3735d8f7c77/dep-lib-flate2","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 @@
dd3950d3584b02d1

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":10248144769085601448,"profile":5347358027863023418,"path":5377756937042145093,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/fnv-4f8347814e7d92ed/dep-lib-fnv","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 @@
ce5cb59aacce9f0d

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"async-await\"]","declared_features":"[\"alloc\", \"async-await\", \"bilock\", \"cfg-target-has-atomic\", \"compat\", \"default\", \"executor\", \"futures-executor\", \"io-compat\", \"spin\", \"std\", \"thread-pool\", \"unstable\", \"write-all-vectored\"]","target":7465627196321967167,"profile":17669703692130904899,"path":13138868418002636703,"deps":[[270634688040536827,"futures_sink",false,9198557697599103430],[302948626015856208,"futures_core",false,8766504836293673446],[5898568623609459682,"futures_util",false,16386763764708151285],[9128867168860799549,"futures_channel",false,2837100198516199358],[12256881686772805731,"futures_task",false,4304492686060886469],[17736352539849991289,"futures_io",false,18423221109826422674]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/futures-80857546caa5d092/dep-lib-futures","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 @@
f50facbdd27a69e3

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"alloc\", \"async-await\", \"async-await-macro\", \"default\", \"futures-io\", \"futures-macro\", \"futures-sink\", \"io\", \"memchr\", \"sink\", \"slab\", \"std\"]","declared_features":"[\"alloc\", \"async-await\", \"async-await-macro\", \"bilock\", \"cfg-target-has-atomic\", \"channel\", \"compat\", \"default\", \"futures-channel\", \"futures-io\", \"futures-macro\", \"futures-sink\", \"futures_01\", \"io\", \"io-compat\", \"libc\", \"memchr\", \"portable-atomic\", \"sink\", \"slab\", \"spin\", \"std\", \"tokio-io\", \"unstable\", \"write-all-vectored\"]","target":1788798584831431502,"profile":17669703692130904899,"path":6425128508853427410,"deps":[[270634688040536827,"futures_sink",false,9198557697599103430],[302948626015856208,"futures_core",false,8766504836293673446],[1363051979936526615,"memchr",false,109361320982092791],[2251399859588827949,"pin_project_lite",false,16008755560344406462],[12256881686772805731,"futures_task",false,4304492686060886469],[14895711841936801505,"slab",false,15554900920696273994],[17736352539849991289,"futures_io",false,18423221109826422674],[18222057389779178848,"futures_macro",false,320363605096533488]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/futures-util-0e967e3a84e6b808/dep-lib-futures_util","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 @@
fab7348b8f9d4eb7

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[]","declared_features":"[\"stream\", \"unstable\"]","target":15216351499943135959,"profile":2908210774301854779,"path":3200000670678218534,"deps":[[270634688040536827,"futures_sink",false,9198557697599103430],[302948626015856208,"futures_core",false,8766504836293673446],[1074848931188612602,"atomic_waker",false,4788697048909975726],[1345404220202658316,"fnv",false,15060682948754815453],[2620434475832828286,"http",false,6071307373105905826],[3163899731817361221,"tokio_util",false,12961583348386118925],[3870702314125662939,"bytes",false,2841784438664644701],[4246786359834650171,"tokio",false,10896739638684196198],[8826707145280285270,"indexmap",false,14508572465398128164],[14757622794040968908,"tracing",false,17213290732951300318],[14895711841936801505,"slab",false,15554900920696273994]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/h2-542f6060d8674bf9/dep-lib-h2","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 @@
4dab32bcf4acb680

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[]","declared_features":"[\"alloc\", \"allocator-api2\", \"core\", \"default\", \"default-hasher\", \"equivalent\", \"inline-more\", \"nightly\", \"raw-entry\", \"rayon\", \"rustc-dep-of-std\", \"rustc-internal-api\", \"serde\"]","target":7848994504142944354,"profile":5486514514725984129,"path":4158037671503567339,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/hashbrown-5aebf1e65ea77cac/dep-lib-hashbrown","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 @@
191ae66837ab0e33

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"client\", \"default\", \"http1\", \"http2\", \"server\"]","declared_features":"[\"capi\", \"client\", \"default\", \"ffi\", \"full\", \"http1\", \"http2\", \"nightly\", \"server\", \"tracing\"]","target":9574292076208557625,"profile":2498339768060210797,"path":1257536371535161214,"deps":[[302948626015856208,"futures_core",false,8766504836293673446],[1074848931188612602,"atomic_waker",false,4788697048909975726],[1569313478171189446,"want",false,10656261974756330172],[2251399859588827949,"pin_project_lite",false,16008755560344406462],[2620434475832828286,"http",false,6071307373105905826],[3158163345960637315,"h2",false,13208667996965615610],[3666196340704888985,"smallvec",false,8955321267744265754],[3870702314125662939,"bytes",false,2841784438664644701],[4246786359834650171,"tokio",false,10896739638684196198],[5532778797167691009,"itoa",false,6451755352924673826],[6163892036024256188,"httparse",false,2339629684298362045],[6304235478050270880,"httpdate",false,9185244413507445080],[9128867168860799549,"futures_channel",false,2837100198516199358],[14084095096285906100,"http_body",false,9737993903290812655]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/hyper-049aba884421f395/dep-lib-hyper","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 @@
93cc2ea9f2990380

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[]","declared_features":"[\"alpn\", \"vendored\"]","target":11005878871305885301,"profile":5347358027863023418,"path":15951291195223936994,"deps":[[784494742817713399,"tower_service",false,7841524536641349667],[3626672138398771397,"hyper",false,3679066200079735321],[3870702314125662939,"bytes",false,2841784438664644701],[4246786359834650171,"tokio",false,10896739638684196198],[9144560277883153344,"native_tls",false,8961787176735113178],[11976082518617474977,"hyper_util",false,17177556051408080709],[12186126227181294540,"tokio_native_tls",false,4947353475659577644],[16900715236047033623,"http_body_util",false,16645273110637047968]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/hyper-tls-b988ee9b25f925af/dep-lib-hyper_tls","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 @@
454f8ad937f062ee

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"client\", \"client-legacy\", \"client-proxy\", \"client-proxy-system\", \"default\", \"http1\", \"http2\", \"server\", \"service\", \"tokio\"]","declared_features":"[\"__internal_happy_eyeballs_tests\", \"client\", \"client-legacy\", \"client-pool\", \"client-proxy\", \"client-proxy-system\", \"default\", \"full\", \"http1\", \"http2\", \"server\", \"server-auto\", \"server-graceful\", \"service\", \"tokio\", \"tracing\"]","target":11100538814903412163,"profile":5347358027863023418,"path":2517185884657718565,"deps":[[784494742817713399,"tower_service",false,7841524536641349667],[2251399859588827949,"pin_project_lite",false,16008755560344406462],[2620434475832828286,"http",false,6071307373105905826],[3626672138398771397,"hyper",false,3679066200079735321],[3870702314125662939,"bytes",false,2841784438664644701],[4246786359834650171,"tokio",false,10896739638684196198],[5898568623609459682,"futures_util",false,16386763764708151285],[6803352382179706244,"percent_encoding",false,10755141293663985388],[7527774033549147775,"ipnet",false,15822097090532353433],[9128867168860799549,"futures_channel",false,2837100198516199358],[10947645248417156337,"socket2",false,9516952010270175178],[12111499963430175700,"libc",false,13910580885449283722],[13077212702700853852,"base64",false,4158476189933773356],[14084095096285906100,"http_body",false,9737993903290812655],[14425619450980614985,"system_configuration",false,16452660330345866229],[14757622794040968908,"tracing",false,17213290732951300318]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/hyper-util-48cd76deeb015d59/dep-lib-hyper_util","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 @@
249e119ec9cd58c9

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"default\", \"std\"]","declared_features":"[\"arbitrary\", \"borsh\", \"default\", \"quickcheck\", \"rayon\", \"serde\", \"std\", \"sval\", \"test_debug\"]","target":15738714612577068147,"profile":5199701822156178865,"path":12222772178346297160,"deps":[[5230392855116717286,"equivalent",false,14198299700970831913],[14713635449132178795,"hashbrown",false,9274790649745550157]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/indexmap-06aca1d2dd93a7ba/dep-lib-indexmap","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 @@
9925ec8a796193db

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"heapless\", \"json\", \"schemars\", \"schemars08\", \"schemars1\", \"ser_as_str\", \"serde\", \"std\"]","target":2684928858108222948,"profile":5347358027863023418,"path":15012101946580197555,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/ipnet-3bd8d41a9a0a02d3/dep-lib-ipnet","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 @@
e544a93012f2285f

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"memchr\", \"serde\", \"std\"]","target":12413245532915438876,"profile":5347358027863023418,"path":13171059925208291686,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/iri-string-508844d193621839/dep-lib-iri_string","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 @@
617f400705ed3965

View File

@@ -0,0 +1 @@
{"rustc":17940977064402226622,"features":"[\"builder\", \"default\", \"hostname\", \"native-tls\", \"pool\", \"smtp-transport\", \"tokio1\", \"tokio1-native-tls\"]","declared_features":"[\"async-std1\", \"async-std1-rustls\", \"async-std1-rustls-tls\", \"aws-lc-rs\", \"boring-tls\", \"builder\", \"default\", \"dkim\", \"file-transport\", \"file-transport-envelope\", \"fips\", \"hostname\", \"mime03\", \"native-tls\", \"pool\", \"ring\", \"rustls\", \"rustls-native-certs\", \"rustls-no-provider\", \"rustls-platform-verifier\", \"rustls-tls\", \"sendmail-transport\", \"serde\", \"smtp-transport\", \"tokio1\", \"tokio1-boring-tls\", \"tokio1-native-tls\", \"tokio1-rustls\", \"tokio1-rustls-tls\", \"tracing\", \"web\", \"webpki-roots\"]","target":8659387820521590376,"profile":8065582915795385062,"path":3694528857547257820,"deps":[[1528297757488249563,"url",false,2725528751739908],[2726707743931605381,"email_address",false,12405729623440351770],[4246786359834650171,"tokio1_crate",false,10896739638684196198],[5898568623609459682,"futures_util",false,16386763764708151285],[6159443412421938570,"idna",false,9032741048420238908],[6304235478050270880,"httpdate",false,9185244413507445080],[6803352382179706244,"percent_encoding",false,10755141293663985388],[9144560277883153344,"native_tls",false,8961787176735113178],[9442026380873301823,"email_encoding",false,13725776858301694366],[10229185211513642314,"mime",false,12540024495573264604],[10947645248417156337,"socket2",false,9516952010270175178],[11183495053016000077,"quoted_printable",false,14000643524247481518],[11927239882567217773,"hostname",false,365225662190597506],[12186126227181294540,"tokio1_native_tls_crate",false,4947353475659577644],[13077212702700853852,"base64",false,4158476189933773356],[14018164067906085395,"fastrand",false,16926642958608193565],[16611674984963787466,"async_trait",false,14792332986729928676],[17736352539849991289,"futures_io",false,18423221109826422674],[18419674550203303546,"nom",false,1744626982655829655]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/lettre-5c2e089ed257d8b4/dep-lib-lettre","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 @@
b2cb48cf8e94a534

Some files were not shown because too many files have changed in this diff Show More