Compare commits
3 Commits
ca69337afb
...
rust
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebd5482070 | ||
|
|
607ff0629c | ||
|
|
c7892748dc |
43
.gitignore
vendored
43
.gitignore
vendored
@@ -1,18 +1,35 @@
|
|||||||
|
# Build Rust
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Données runtime
|
||||||
|
data/config.json
|
||||||
|
data/alerts.json
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Variables d'environnement
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Dossiers non pertinents
|
||||||
|
imput/
|
||||||
|
logTest/
|
||||||
|
log/
|
||||||
|
docs/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# IDE / outils locaux
|
||||||
|
.claude/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Python (héritage)
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
data/config.json
|
|
||||||
data/alerts.json
|
|
||||||
*.log
|
|
||||||
.env
|
|
||||||
imput/
|
|
||||||
logTest/
|
|
||||||
log/
|
|
||||||
CLAUDE.md
|
|
||||||
docs/
|
|
||||||
.claude/
|
|
||||||
*.spec
|
*.spec
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
docs/
|
|
||||||
|
|||||||
60
CLAUDE.md
Normal file
60
CLAUDE.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dev server (http://localhost:5000, credentials admin/admin)
|
||||||
|
cargo run
|
||||||
|
|
||||||
|
# Release build — produces target/release/supervision (Linux) or target\release\supervision.exe (Windows)
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Cross-compile for Windows from Linux
|
||||||
|
rustup target add x86_64-pc-windows-gnu
|
||||||
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
cargo clippy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows service (run as Administrator)
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
supervision.exe install # register as auto-start service named "Supervision"
|
||||||
|
sc start Supervision
|
||||||
|
sc stop Supervision
|
||||||
|
supervision.exe uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Single-binary Axum web server. All shared mutable state is passed through `AppState` (defined in `src/routes/mod.rs`) which holds `Arc`-wrapped components.
|
||||||
|
|
||||||
|
**`src/main.rs`** — entry point: builds `AppState`, registers all routes, starts background threads (`start_monitoring`, `UserMonitor::start`), and handles Windows service scaffolding (feature-gated on `cfg(windows)`).
|
||||||
|
|
||||||
|
**`src/routes/mod.rs`** — defines `AppState`, the `AuthUser` extractor (redirects to `/login` if session is absent), and `build_tera()` which **embeds all templates at compile time** via `include_str!`. Modifying a template requires a recompile. Also provides `flash`/`get_and_clear_flash` session helpers and `render_html`.
|
||||||
|
|
||||||
|
**`src/routes/{auth,dashboard,settings,alerts,users}.rs`** — all HTTP route handlers. Each protected handler receives `AuthUser` as an extractor to enforce authentication.
|
||||||
|
|
||||||
|
**`src/config.rs`** — `Config` struct serialised to/from `data/config.json`. `ConfigManager` wraps `Config` and the `data/` path; callers lock `Arc<AsyncMutex<ConfigManager>>` to read or write config. Password hashing with bcrypt. Alerts ring-buffered to 500 entries in `data/alerts.json`.
|
||||||
|
|
||||||
|
**`src/monitor.rs`** — `SystemMonitor` collects CPU/RAM/disk/process data via `sysinfo`. `eval_status(value, threshold)` returns `"ok"` / `"warning"` / `"critical"` (warning ≥ 80 %, critical ≥ 100 % of threshold). The background thread calls `collect_metrics` → `check_thresholds`, applies per-key cooldown, persists alerts, and optionally sends email. Sleeps in 5-second chunks so `monitoring_active` changes are picked up promptly.
|
||||||
|
|
||||||
|
**`src/user_monitor.rs`** — `UserMonitor` parses Amadea `awevents_*` and `isoft_*` log files (plain or `.gz`) to build a per-user activity snapshot. `parse_awevents_line` extracts login/action/label from `awevents` files; `isoft` files provide `connected_since` (session open time via `OpenUserSession`). `compute_statuses` assigns `actif` / `inactif` / `absent` / `deconnecte` based on configurable minute thresholds — `absent` means inactive beyond `inactive_minutes` without an explicit logout. `compute_active_time` derives presence and active time by subtracting gaps exceeding `pause_threshold_minutes`. Also provides `get_weekly_activity` / `get_monthly_activity` (peak concurrent users per day) and `get_users_for_date` / `get_user_history`. Log file discovery handles both dated files (`awevents_YY-MM-DD_N.log.gz`) and undated active-log files (`awevents.log`). `UserMonitor.data` is guarded by `std::Mutex` (never held across `.await`).
|
||||||
|
|
||||||
|
**`src/alerter.rs`** — SMTP email dispatch via `lettre`. `is_configured` guards all sends. Uses STARTTLS by default; falls back to unencrypted when `use_tls = false`.
|
||||||
|
|
||||||
|
### Data directory
|
||||||
|
|
||||||
|
`data/` is created next to the binary at first launch:
|
||||||
|
- `config.json` — all settings; written after every settings form submission.
|
||||||
|
- `alerts.json` — ring buffer of the last 500 alerts, newest first.
|
||||||
|
|
||||||
|
### Template context conventions
|
||||||
|
|
||||||
|
Every protected page calls `base_context()` which injects `authenticated`, `flash_messages`, `default_pw`, and `username`. `apply_security_headers()` adds `X-Content-Type-Options`, `X-Frame-Options`, etc. to every response.
|
||||||
301
Cargo.lock
generated
301
Cargo.lock
generated
@@ -267,16 +267,6 @@ dependencies = [
|
|||||||
"version_check",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -405,15 +395,6 @@ version = "0.2.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
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]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -452,12 +433,6 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fnv"
|
|
||||||
version = "1.0.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -627,25 +602,6 @@ dependencies = [
|
|||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
@@ -748,7 +704,6 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"h2",
|
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -757,38 +712,6 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tokio",
|
"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]]
|
[[package]]
|
||||||
@@ -797,23 +720,13 @@ version = "0.1.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
"ipnet",
|
|
||||||
"libc",
|
|
||||||
"percent-encoding",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
|
||||||
"system-configuration",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
|
||||||
"windows-registry",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -986,22 +899,6 @@ dependencies = [
|
|||||||
"generic-array",
|
"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]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -1014,8 +911,6 @@ version = "0.3.95"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
|
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
|
||||||
"futures-util",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@@ -1549,60 +1444,6 @@ version = "0.8.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
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]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -1616,39 +1457,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@@ -1692,7 +1500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
@@ -1887,7 +1695,6 @@ dependencies = [
|
|||||||
"lettre",
|
"lettre",
|
||||||
"rand",
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
@@ -1895,7 +1702,7 @@ dependencies = [
|
|||||||
"tera",
|
"tera",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower 0.4.13",
|
"tower 0.4.13",
|
||||||
"tower-http 0.5.2",
|
"tower-http",
|
||||||
"tower-sessions",
|
"tower-sessions",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -1918,9 +1725,6 @@ name = "sync_wrapper"
|
|||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "synstructure"
|
name = "synstructure"
|
||||||
@@ -1947,27 +1751,6 @@ dependencies = [
|
|||||||
"windows",
|
"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]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
@@ -2111,16 +1894,6 @@ dependencies = [
|
|||||||
"tokio",
|
"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]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -2203,24 +1976,6 @@ dependencies = [
|
|||||||
"tracing",
|
"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]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -2346,12 +2101,6 @@ dependencies = [
|
|||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "try-lock"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
@@ -2388,12 +2137,6 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "untrusted"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -2440,15 +2183,6 @@ dependencies = [
|
|||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
@@ -2486,16 +2220,6 @@ dependencies = [
|
|||||||
"wasm-bindgen-shared",
|
"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]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.118"
|
version = "0.2.118"
|
||||||
@@ -2562,16 +2286,6 @@ dependencies = [
|
|||||||
"semver",
|
"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]]
|
[[package]]
|
||||||
name = "widestring"
|
name = "widestring"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -2694,17 +2408,6 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
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]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|||||||
@@ -29,10 +29,14 @@ regex = "1"
|
|||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
form_urlencoded = "1"
|
form_urlencoded = "1"
|
||||||
reqwest = { version = "0.12", features = ["json", "native-tls"] }
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows-service = "0.7"
|
windows-service = "0.7"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = true
|
||||||
|
strip = true
|
||||||
|
codegen-units = 1
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -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)
|
- **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
|
- **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
|
- **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, 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
|
- **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
|
- **Service Windows** — installation en tant que service système avec démarrage automatique
|
||||||
|
|
||||||
@@ -119,16 +119,46 @@ 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é.
|
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)
|
### Tableau temps réel (aujourd'hui)
|
||||||
|
|
||||||
- Colonnes : Utilisateur, Statut, Dernière action, Actions (24h), Depuis
|
- Colonnes : Utilisateur, Statut, Dernière action, Actions (24h), Erreurs, Présence / Actif, Depuis
|
||||||
- Tri : statut (actif → inactif → déconnecté), puis dernière action la plus récente en premier au sein de chaque groupe
|
- 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
|
- **7 jours** et **30 jours** — 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)
|
- **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
|
||||||
- Tri par nombre d'actions décroissant
|
- **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
|
### Détection des fichiers de logs
|
||||||
|
|
||||||
@@ -136,8 +166,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 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é)
|
- 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
|
## Lancer les tests
|
||||||
|
|
||||||
```cmd
|
```cmd
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ pub struct SmtpConfig {
|
|||||||
pub struct UserStatusThresholds {
|
pub struct UserStatusThresholds {
|
||||||
pub active_minutes: u64,
|
pub active_minutes: u64,
|
||||||
pub inactive_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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -101,6 +107,7 @@ impl Default for Config {
|
|||||||
user_status_thresholds: UserStatusThresholds {
|
user_status_thresholds: UserStatusThresholds {
|
||||||
active_minutes: 5,
|
active_minutes: 5,
|
||||||
inactive_minutes: 30,
|
inactive_minutes: 30,
|
||||||
|
pause_threshold_minutes: 20,
|
||||||
},
|
},
|
||||||
admin: AdminConfig {
|
admin: AdminConfig {
|
||||||
username: "admin".into(),
|
username: "admin".into(),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use routes::{
|
|||||||
update_amadea_log_path, update_user_thresholds,
|
update_amadea_log_path, update_user_thresholds,
|
||||||
},
|
},
|
||||||
alerts::{alerts_get, clear_alerts},
|
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() {
|
pub async fn run_server() {
|
||||||
@@ -87,7 +87,9 @@ pub async fn run_server() {
|
|||||||
.route("/users", get(users_get))
|
.route("/users", get(users_get))
|
||||||
.route("/api/users", get(api_users))
|
.route("/api/users", get(api_users))
|
||||||
.route("/api/users/activity/weekly", get(api_users_weekly))
|
.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/day/:date", get(api_users_day))
|
||||||
|
.route("/api/users/:login/history", get(api_user_history))
|
||||||
.nest_service("/static", ServeDir::new("static"))
|
.nest_service("/static", ServeDir::new("static"))
|
||||||
.layer(session_layer)
|
.layer(session_layer)
|
||||||
.with_state(state.clone())
|
.with_state(state.clone())
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ pub async fn update_amadea_log_path(
|
|||||||
pub struct UserThresholdsForm {
|
pub struct UserThresholdsForm {
|
||||||
pub active_minutes: u64,
|
pub active_minutes: u64,
|
||||||
pub inactive_minutes: u64,
|
pub inactive_minutes: u64,
|
||||||
|
pub pause_threshold_minutes: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_user_thresholds(
|
pub async fn update_user_thresholds(
|
||||||
@@ -369,7 +370,7 @@ pub async fn update_user_thresholds(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(form): Form<UserThresholdsForm>,
|
Form(form): Form<UserThresholdsForm>,
|
||||||
) -> impl IntoResponse {
|
) -> 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(
|
flash(
|
||||||
&session,
|
&session,
|
||||||
"danger",
|
"danger",
|
||||||
@@ -391,6 +392,7 @@ pub async fn update_user_thresholds(
|
|||||||
let mut cm = state.config_manager.lock().await;
|
let mut cm = state.config_manager.lock().await;
|
||||||
cm.config.user_status_thresholds.active_minutes = form.active_minutes;
|
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.inactive_minutes = form.inactive_minutes;
|
||||||
|
cm.config.user_status_thresholds.pause_threshold_minutes = form.pause_threshold_minutes;
|
||||||
cm.save();
|
cm.save();
|
||||||
}
|
}
|
||||||
flash(&session, "success", "Seuils utilisateurs mis à jour.").await;
|
flash(&session, "success", "Seuils utilisateurs mis à jour.").await;
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, Query, State},
|
||||||
response::{IntoResponse, Json},
|
response::{IntoResponse, Json},
|
||||||
};
|
};
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tower_sessions::Session;
|
use tower_sessions::Session;
|
||||||
|
|
||||||
use crate::routes::{get_and_clear_flash, render_html, AppState, AuthUser};
|
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(
|
pub async fn users_get(
|
||||||
_auth: AuthUser,
|
_auth: AuthUser,
|
||||||
session: Session,
|
session: Session,
|
||||||
@@ -44,6 +55,10 @@ pub async fn api_users(
|
|||||||
"action_count_24h": u.action_count_24h,
|
"action_count_24h": u.action_count_24h,
|
||||||
"connected_since": u.connected_since.map(|t| t.format("%H:%M").to_string()),
|
"connected_since": u.connected_since.map(|t| t.format("%H:%M").to_string()),
|
||||||
"explicit_logout": u.explicit_logout,
|
"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();
|
.collect();
|
||||||
@@ -58,6 +73,14 @@ pub async fn api_users_weekly(
|
|||||||
Json(json!({ "weekly": 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(
|
pub async fn api_users_day(
|
||||||
_auth: AuthUser,
|
_auth: AuthUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -70,3 +93,13 @@ pub async fn api_users_day(
|
|||||||
let users = state.user_monitor.get_users_for_date(date).await;
|
let users = state.user_monitor.get_users_for_date(date).await;
|
||||||
Json(json!({ "users": users, "date": date_str }))
|
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 }))
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ pub struct UserEntry {
|
|||||||
pub explicit_logout: bool,
|
pub explicit_logout: bool,
|
||||||
pub logout_time: Option<NaiveDateTime>,
|
pub logout_time: Option<NaiveDateTime>,
|
||||||
pub connected_since: 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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -37,6 +44,10 @@ pub struct UserData {
|
|||||||
pub no_files: bool,
|
pub no_files: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers fichiers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fn read_log_file(path: &std::path::PathBuf) -> Option<String> {
|
fn read_log_file(path: &std::path::PathBuf) -> Option<String> {
|
||||||
if path.to_string_lossy().ends_with(".gz") {
|
if path.to_string_lossy().ends_with(".gz") {
|
||||||
let file = fs::File::open(path).ok()?;
|
let file = fs::File::open(path).ok()?;
|
||||||
@@ -51,10 +62,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> {
|
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();
|
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();
|
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 pattern_with_date = format!("{}/{}_{}_*", log_path.to_string_lossy(), prefix, date_str);
|
||||||
let mut files: Vec<_> = glob::glob(&pattern_with_date)
|
let mut files: Vec<_> = glob::glob(&pattern_with_date)
|
||||||
.unwrap_or_else(|_| glob::glob("__nonexistent__").unwrap())
|
.unwrap_or_else(|_| glob::glob("__nonexistent__").unwrap())
|
||||||
@@ -65,8 +74,6 @@ fn log_files_for_date(log_path: &Path, prefix: &str, date_str: &str) -> Vec<std:
|
|||||||
})
|
})
|
||||||
.collect();
|
.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();
|
let today_str = Local::now().format("%y-%m-%d").to_string();
|
||||||
if date_str == today_str {
|
if date_str == today_str {
|
||||||
let pattern_active = format!("{}/{}*", log_path.to_string_lossy(), prefix);
|
let pattern_active = format!("{}/{}*", log_path.to_string_lossy(), prefix);
|
||||||
@@ -95,6 +102,145 @@ fn log_files_for_date(log_path: &Path, prefix: &str, date_str: &str) -> Vec<std:
|
|||||||
files
|
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(
|
pub fn parse_awevents_line(
|
||||||
line: &str,
|
line: &str,
|
||||||
users: &mut HashMap<String, UserEntry>,
|
users: &mut HashMap<String, UserEntry>,
|
||||||
@@ -123,22 +269,31 @@ pub fn parse_awevents_line(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let is_logout = label.to_lowercase().contains("se deconnecter");
|
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 {
|
let entry = users.entry(login.clone()).or_insert_with(|| UserEntry {
|
||||||
login: login.clone(),
|
login: login.clone(),
|
||||||
last_action_time: ts,
|
last_action_time: ts,
|
||||||
last_action_label: label.chars().take(60).collect(),
|
last_action_label: module_label.clone(),
|
||||||
action_count_24h: 0,
|
action_count_24h: 0,
|
||||||
status: "deconnecte".into(),
|
status: "deconnecte".into(),
|
||||||
explicit_logout: is_logout,
|
explicit_logout: is_logout,
|
||||||
logout_time: if is_logout { Some(ts) } else { None },
|
logout_time: if is_logout { Some(ts) } else { None },
|
||||||
connected_since: Some(ts),
|
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 {
|
if ts > entry.last_action_time {
|
||||||
entry.last_action_time = ts;
|
entry.last_action_time = ts;
|
||||||
entry.last_action_label = label.chars().take(60).collect();
|
entry.last_action_label = module_label;
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_logout {
|
if is_logout {
|
||||||
entry.explicit_logout = true;
|
entry.explicit_logout = true;
|
||||||
entry.logout_time = Some(ts);
|
entry.logout_time = Some(ts);
|
||||||
@@ -147,6 +302,7 @@ pub fn parse_awevents_line(
|
|||||||
if ts > lt {
|
if ts > lt {
|
||||||
entry.explicit_logout = false;
|
entry.explicit_logout = false;
|
||||||
entry.logout_time = None;
|
entry.logout_time = None;
|
||||||
|
entry.session_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,6 +314,12 @@ pub fn parse_awevents_line(
|
|||||||
hourly.entry(ts.hour() as u32).or_default().insert(login);
|
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(
|
pub fn compute_statuses(
|
||||||
users: &mut HashMap<String, UserEntry>,
|
users: &mut HashMap<String, UserEntry>,
|
||||||
active_min: u64,
|
active_min: u64,
|
||||||
@@ -169,7 +331,7 @@ pub fn compute_statuses(
|
|||||||
user.status = if user.explicit_logout {
|
user.status = if user.explicit_logout {
|
||||||
"deconnecte".into()
|
"deconnecte".into()
|
||||||
} else if delta > inactive_min {
|
} else if delta > inactive_min {
|
||||||
"deconnecte".into()
|
"absent".into()
|
||||||
} else if delta > active_min {
|
} else if delta > active_min {
|
||||||
"inactif".into()
|
"inactif".into()
|
||||||
} else {
|
} else {
|
||||||
@@ -178,6 +340,10 @@ pub fn compute_statuses(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// UserMonitor
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
pub struct UserMonitor {
|
pub struct UserMonitor {
|
||||||
config_manager: Arc<AsyncMutex<ConfigManager>>,
|
config_manager: Arc<AsyncMutex<ConfigManager>>,
|
||||||
pub data: Arc<Mutex<UserData>>,
|
pub data: Arc<Mutex<UserData>>,
|
||||||
@@ -194,12 +360,13 @@ impl UserMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn parse_logs(&self) {
|
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;
|
let cm = self.config_manager.lock().await;
|
||||||
(
|
(
|
||||||
cm.config.amadea_log_path.clone(),
|
cm.config.amadea_log_path.clone(),
|
||||||
cm.config.user_status_thresholds.active_minutes,
|
cm.config.user_status_thresholds.active_minutes,
|
||||||
cm.config.user_status_thresholds.inactive_minutes,
|
cm.config.user_status_thresholds.inactive_minutes,
|
||||||
|
cm.config.user_status_thresholds.pause_threshold_minutes,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -232,7 +399,7 @@ impl UserMonitor {
|
|||||||
(0..24).map(|h| (h, HashSet::new())).collect();
|
(0..24).map(|h| (h, HashSet::new())).collect();
|
||||||
|
|
||||||
for file in &awevents_files {
|
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() {
|
for line in content.lines() {
|
||||||
parse_awevents_line(line, &mut users, cutoff_24h, &mut hourly);
|
parse_awevents_line(line, &mut users, cutoff_24h, &mut hourly);
|
||||||
}
|
}
|
||||||
@@ -243,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_]+)",
|
r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*method=OpenUserSession.*login=([A-Za-z0-9_]+)",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for file in log_files_for_date(log_dir, "isoft", &date_str) {
|
let isoft_files: Vec<_> = log_files_for_date(log_dir, "isoft", &date_str);
|
||||||
if let Some(content) = read_log_file(&file) {
|
for file in &isoft_files {
|
||||||
|
if let Some(content) = read_log_file(file) {
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
if let Some(m) = re_isoft.captures(line) {
|
if let Some(m) = re_isoft.captures(line) {
|
||||||
let login = m[2].to_string();
|
let login = m[2].to_string();
|
||||||
@@ -261,13 +429,31 @@ 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);
|
compute_statuses(&mut users, active_min, inactive_min, now);
|
||||||
|
|
||||||
let status_order = |s: &str| match s {
|
let status_order = |s: &str| match s {
|
||||||
"actif" => 0,
|
"actif" => 0,
|
||||||
"inactif" => 1,
|
"inactif" => 1,
|
||||||
_ => 2,
|
"absent" => 2,
|
||||||
|
_ => 3,
|
||||||
};
|
};
|
||||||
let mut sorted: Vec<UserEntry> = users.into_values().collect();
|
let mut sorted: Vec<UserEntry> = users.into_values().collect();
|
||||||
sorted.sort_by(|a, b| {
|
sorted.sort_by(|a, b| {
|
||||||
@@ -297,6 +483,15 @@ impl UserMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_weekly_activity(&self) -> Vec<serde_json::Value> {
|
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 log_path = {
|
||||||
let cm = self.config_manager.lock().await;
|
let cm = self.config_manager.lock().await;
|
||||||
cm.config.amadea_log_path.clone()
|
cm.config.amadea_log_path.clone()
|
||||||
@@ -313,7 +508,7 @@ impl UserMonitor {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
for delta in (0..=6i64).rev() {
|
for delta in (0..days).rev() {
|
||||||
let day = today - Duration::days(delta);
|
let day = today - Duration::days(delta);
|
||||||
let date_str = day.format("%y-%m-%d").to_string();
|
let date_str = day.format("%y-%m-%d").to_string();
|
||||||
let files = log_files_for_date(log_dir, "awevents", &date_str);
|
let files = log_files_for_date(log_dir, "awevents", &date_str);
|
||||||
@@ -324,7 +519,7 @@ impl UserMonitor {
|
|||||||
let mut hourly: HashMap<u32, HashSet<String>> =
|
let mut hourly: HashMap<u32, HashSet<String>> =
|
||||||
(0..24u32).map(|h| (h, HashSet::new())).collect();
|
(0..24u32).map(|h| (h, HashSet::new())).collect();
|
||||||
for file in &files {
|
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() {
|
for line in content.lines() {
|
||||||
if let Some(m) = re.captures(line) {
|
if let Some(m) = re.captures(line) {
|
||||||
let hour: u32 = m[2].parse().unwrap_or(0);
|
let hour: u32 = m[2].parse().unwrap_or(0);
|
||||||
@@ -337,17 +532,24 @@ impl UserMonitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let max_concurrent = hourly.values().map(|s| s.len()).max().unwrap_or(0);
|
let max_concurrent = hourly.values().map(|s| s.len()).max().unwrap_or(0);
|
||||||
result.push(
|
let isoft_files = log_files_for_date(log_dir, "isoft", &date_str);
|
||||||
serde_json::json!({ "date": day.to_string(), "count": max_concurrent }),
|
let total_errors = count_daily_errors(&isoft_files);
|
||||||
);
|
result.push(serde_json::json!({
|
||||||
|
"date": day.to_string(),
|
||||||
|
"count": max_concurrent,
|
||||||
|
"errors": total_errors,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_users_for_date(&self, date: NaiveDate) -> Vec<serde_json::Value> {
|
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;
|
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);
|
let log_dir = Path::new(&log_path);
|
||||||
if !log_dir.is_dir() {
|
if !log_dir.is_dir() {
|
||||||
@@ -373,29 +575,133 @@ impl UserMonitor {
|
|||||||
let mut result: Vec<_> = users.into_values().collect();
|
let mut result: Vec<_> = users.into_values().collect();
|
||||||
result.sort_by(|a, b| b.action_count_24h.cmp(&a.action_count_24h));
|
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
|
result
|
||||||
.iter()
|
.iter()
|
||||||
.map(|u| {
|
.map(|u| {
|
||||||
let duration = u.connected_since.map(|since| {
|
let (presence_mins, active_mins) =
|
||||||
let mins = (u.last_action_time - since).num_minutes().max(0);
|
compute_active_time(&u.timestamps, pause_threshold_minutes);
|
||||||
if mins >= 60 {
|
let first_action = u.timestamps.iter().min()
|
||||||
format!("{}h{:02}", mins / 60, mins % 60)
|
.map(|t| t.format("%H:%M").to_string());
|
||||||
} else {
|
|
||||||
format!("{}min", mins)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"login": u.login,
|
"login": u.login,
|
||||||
"last_action_time": u.last_action_time.format("%H:%M:%S").to_string(),
|
"last_action_time": u.last_action_time.format("%H:%M:%S").to_string(),
|
||||||
"last_action_label": u.last_action_label,
|
"last_action_label": u.last_action_label,
|
||||||
"action_count": u.action_count_24h,
|
"action_count": u.action_count_24h,
|
||||||
"first_action_time": u.connected_since.map(|t| t.format("%H:%M").to_string()),
|
"first_action_time": first_action,
|
||||||
"duration": duration,
|
"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()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Activité d'un utilisateur spécifique sur les `days` derniers jours (7 ou 30).
|
||||||
|
pub async fn get_user_history(&self, login: &str, days: i64) -> Vec<serde_json::Value> {
|
||||||
|
let days = days.clamp(1, 30);
|
||||||
|
let (log_path, pause_threshold_minutes) = {
|
||||||
|
let cm = self.config_manager.lock().await;
|
||||||
|
(
|
||||||
|
cm.config.amadea_log_path.clone(),
|
||||||
|
cm.config.user_status_thresholds.pause_threshold_minutes,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let log_dir = Path::new(&log_path);
|
||||||
|
if !log_dir.is_dir() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let re = Regex::new(
|
||||||
|
r#"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d+;[^;]*;;;;"login=([^,]+),action=([^,]+),Label=(.+?)"?\s*$"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for delta in (0..days).rev() {
|
||||||
|
let day = today - Duration::days(delta);
|
||||||
|
let date_str = day.format("%y-%m-%d").to_string();
|
||||||
|
let files = log_files_for_date(log_dir, "awevents", &date_str);
|
||||||
|
|
||||||
|
if files.is_empty() {
|
||||||
|
result.push(serde_json::json!({ "date": day.to_string(), "action_count": null }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut timestamps: Vec<NaiveDateTime> = Vec::new();
|
||||||
|
let mut action_count: u32 = 0;
|
||||||
|
let mut session_count: u32 = 1;
|
||||||
|
let mut explicit_logout = false;
|
||||||
|
let mut logout_time: Option<NaiveDateTime> = None;
|
||||||
|
|
||||||
|
for file in &files {
|
||||||
|
if let Some(content) = read_log_file(file) {
|
||||||
|
for line in content.lines() {
|
||||||
|
let m = match re.captures(line) {
|
||||||
|
Some(m) => m,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
if m[2].trim() != login {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ts = match NaiveDateTime::parse_from_str(&m[1], "%Y-%m-%d %H:%M:%S") {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let is_logout = m[4].to_lowercase().contains("se deconnecter");
|
||||||
|
|
||||||
|
action_count += 1;
|
||||||
|
timestamps.push(ts);
|
||||||
|
|
||||||
|
if is_logout {
|
||||||
|
explicit_logout = true;
|
||||||
|
logout_time = Some(ts);
|
||||||
|
} else if explicit_logout {
|
||||||
|
if let Some(lt) = logout_time {
|
||||||
|
if ts > lt {
|
||||||
|
explicit_logout = false;
|
||||||
|
logout_time = None;
|
||||||
|
session_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if action_count == 0 {
|
||||||
|
result.push(serde_json::json!({ "date": day.to_string(), "action_count": null }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamps.sort_unstable();
|
||||||
|
let first_action = timestamps.first().map(|t| t.format("%H:%M").to_string());
|
||||||
|
let last_action = timestamps.last().map(|t| t.format("%H:%M").to_string());
|
||||||
|
let (presence_mins, active_mins) =
|
||||||
|
compute_active_time(×tamps, pause_threshold_minutes);
|
||||||
|
|
||||||
|
let isoft_files = log_files_for_date(log_dir, "isoft", &date_str);
|
||||||
|
let error_counts = parse_isoft_errors(&isoft_files);
|
||||||
|
let user_errors = error_counts.get(login).copied().unwrap_or(0);
|
||||||
|
|
||||||
|
result.push(serde_json::json!({
|
||||||
|
"date": day.to_string(),
|
||||||
|
"action_count": action_count,
|
||||||
|
"first_action": first_action,
|
||||||
|
"last_action": last_action,
|
||||||
|
"presence": if presence_mins > 0 { Some(format_duration(presence_mins)) } else { None },
|
||||||
|
"active_time": if active_mins > 0 { Some(format_duration(active_mins)) } else { None },
|
||||||
|
"sessions": session_count,
|
||||||
|
"error_count": user_errors,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn start(self: Arc<Self>) {
|
pub async fn start(self: Arc<Self>) {
|
||||||
self.running
|
self.running
|
||||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
@@ -416,17 +722,38 @@ impl UserMonitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn parse_awevents_line_extracts_user_and_action() {
|
fn parse_awevents_line_extracts_user_and_action() {
|
||||||
let line =
|
let line =
|
||||||
r#"2026-04-07 14:23:45.123;server;;;;"login=jdupont,action=consulter,Label=Consulter dossier""#;
|
r#"2026-04-07 14:23:45.123;server;;;;"login=jdupont,action=consulter,Label=Consulter dossier""#;
|
||||||
let mut users = HashMap::new();
|
let mut users = HashMap::new();
|
||||||
let cutoff = chrono::Local::now().naive_local()
|
let cutoff = chrono::Local::now().naive_local() - chrono::Duration::hours(25);
|
||||||
- chrono::Duration::hours(25);
|
|
||||||
let mut hourly = (0..24u32).map(|h| (h, HashSet::new())).collect();
|
let mut hourly = (0..24u32).map(|h| (h, HashSet::new())).collect();
|
||||||
parse_awevents_line(line, &mut users, cutoff, &mut hourly);
|
parse_awevents_line(line, &mut users, cutoff, &mut hourly);
|
||||||
assert!(users.contains_key("jdupont"));
|
assert!(users.contains_key("jdupont"));
|
||||||
@@ -446,20 +773,59 @@ mod tests {
|
|||||||
fn compute_statuses_marks_recent_as_active() {
|
fn compute_statuses_marks_recent_as_active() {
|
||||||
let now = chrono::Local::now().naive_local();
|
let now = chrono::Local::now().naive_local();
|
||||||
let mut users = HashMap::new();
|
let mut users = HashMap::new();
|
||||||
users.insert(
|
users.insert("alice".into(), make_user("alice", now, false));
|
||||||
"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),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
compute_statuses(&mut users, 5, 30, now);
|
compute_statuses(&mut users, 5, 30, now);
|
||||||
assert_eq!(users["alice"].status, "actif");
|
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
BIN
target/.DS_Store
vendored
Binary file not shown.
BIN
target/debug/.fingerprint/adler2-c7e8d4d29fedd94f/dep-lib-adler2
Normal file
BIN
target/debug/.fingerprint/adler2-c7e8d4d29fedd94f/dep-lib-adler2
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
45fa2d90705c3738
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":17940977064402226622,"features":"[]","declared_features":"[\"core\", \"default\", \"rustc-dep-of-std\", \"std\"]","target":6569825234462323107,"profile":5347358027863023418,"path":13865193194691325206,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/adler2-c7e8d4d29fedd94f/dep-lib-adler2","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||||
BIN
target/debug/.fingerprint/axum-03015be23fc8692d/dep-lib-axum
Normal file
BIN
target/debug/.fingerprint/axum-03015be23fc8692d/dep-lib-axum
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
1
target/debug/.fingerprint/axum-03015be23fc8692d/lib-axum
Normal file
1
target/debug/.fingerprint/axum-03015be23fc8692d/lib-axum
Normal file
@@ -0,0 +1 @@
|
|||||||
|
2df9339f4c62aac3
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":17940977064402226622,"features":"[\"default\", \"form\", \"http1\", \"json\", \"macros\", \"matched-path\", \"original-uri\", \"query\", \"tokio\", \"tower-log\", \"tracing\"]","declared_features":"[\"__private_docs\", \"default\", \"form\", \"http1\", \"http2\", \"json\", \"macros\", \"matched-path\", \"multipart\", \"original-uri\", \"query\", \"tokio\", \"tower-log\", \"tracing\", \"ws\"]","target":13920321295547257648,"profile":5347358027863023418,"path":7907914990833577576,"deps":[[784494742817713399,"tower_service",false,7841524536641349667],[1363051979936526615,"memchr",false,109361320982092791],[2251399859588827949,"pin_project_lite",false,16008755560344406462],[2517136641825875337,"sync_wrapper",false,2262787393906636474],[2620434475832828286,"http",false,6071307373105905826],[3626672138398771397,"hyper",false,3679066200079735321],[3632162862999675140,"tower",false,8865620563452385240],[3870702314125662939,"bytes",false,2841784438664644701],[4246786359834650171,"tokio",false,10896739638684196198],[4359148418957042248,"axum_core",false,8729791837833250442],[5532778797167691009,"itoa",false,6451755352924673826],[5898568623609459682,"futures_util",false,16386763764708151285],[6803352382179706244,"percent_encoding",false,10755141293663985388],[7712452662827335977,"tower_layer",false,14491143023105329304],[7940089053034940860,"axum_macros",false,5676342163752640776],[9678799920983747518,"matchit",false,11028109377757724220],[10229185211513642314,"mime",false,12540024495573264604],[11976082518617474977,"hyper_util",false,17177556051408080709],[13548984313718623784,"serde",false,4755198247368830486],[13795362694956882968,"serde_json",false,14317311717721046485],[14084095096285906100,"http_body",false,9737993903290812655],[14156967978702956262,"rustversion",false,16981750216962319693],[14757622794040968908,"tracing",false,17213290732951300318],[14814583949208169760,"serde_path_to_error",false,11248637185294131992],[16542808166767769916,"serde_urlencoded",false,18036353298262058364],[16611674984963787466,"async_trait",false,14792332986729928676],[16900715236047033623,"http_body_util",false,16645273110637047968]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/axum-03015be23fc8692d/dep-lib-axum","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
8ac627916c732679
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":17940977064402226622,"features":"[\"tracing\"]","declared_features":"[\"__private_docs\", \"tracing\"]","target":2565713999752801252,"profile":5347358027863023418,"path":13979972293121727953,"deps":[[784494742817713399,"tower_service",false,7841524536641349667],[2251399859588827949,"pin_project_lite",false,16008755560344406462],[2517136641825875337,"sync_wrapper",false,2262787393906636474],[2620434475832828286,"http",false,6071307373105905826],[3870702314125662939,"bytes",false,2841784438664644701],[5898568623609459682,"futures_util",false,16386763764708151285],[7712452662827335977,"tower_layer",false,14491143023105329304],[10229185211513642314,"mime",false,12540024495573264604],[14084095096285906100,"http_body",false,9737993903290812655],[14156967978702956262,"rustversion",false,16981750216962319693],[14757622794040968908,"tracing",false,17213290732951300318],[16611674984963787466,"async_trait",false,14792332986729928676],[16900715236047033623,"http_body_util",false,16645273110637047968]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/axum-core-0aa5bfffba341c65/dep-lib-axum_core","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
35d38144a9f5e6e5
|
||||||
@@ -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}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
706936f6dec95ed6
|
||||||
@@ -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}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0ae5727f294d31c3
|
||||||
@@ -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}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
c9f6b5c51668131a
|
||||||
@@ -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}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
a41e89eaeb5a5241
|
||||||
@@ -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}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
29c0e62a5a7e0ac5
|
||||||
@@ -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}
|
||||||
BIN
target/debug/.fingerprint/flate2-401cd3735d8f7c77/dep-lib-flate2
Normal file
BIN
target/debug/.fingerprint/flate2-401cd3735d8f7c77/dep-lib-flate2
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
b66b64172818108b
|
||||||
@@ -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}
|
||||||
BIN
target/debug/.fingerprint/fnv-4f8347814e7d92ed/dep-lib-fnv
Normal file
BIN
target/debug/.fingerprint/fnv-4f8347814e7d92ed/dep-lib-fnv
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
1
target/debug/.fingerprint/fnv-4f8347814e7d92ed/lib-fnv
Normal file
1
target/debug/.fingerprint/fnv-4f8347814e7d92ed/lib-fnv
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dd3950d3584b02d1
|
||||||
@@ -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}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ce5cb59aacce9f0d
|
||||||
@@ -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}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
f50facbdd27a69e3
|
||||||
@@ -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}
|
||||||
BIN
target/debug/.fingerprint/h2-542f6060d8674bf9/dep-lib-h2
Normal file
BIN
target/debug/.fingerprint/h2-542f6060d8674bf9/dep-lib-h2
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
1
target/debug/.fingerprint/h2-542f6060d8674bf9/lib-h2
Normal file
1
target/debug/.fingerprint/h2-542f6060d8674bf9/lib-h2
Normal file
@@ -0,0 +1 @@
|
|||||||
|
fab7348b8f9d4eb7
|
||||||
@@ -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}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
4dab32bcf4acb680
|
||||||
@@ -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}
|
||||||
BIN
target/debug/.fingerprint/hyper-049aba884421f395/dep-lib-hyper
Normal file
BIN
target/debug/.fingerprint/hyper-049aba884421f395/dep-lib-hyper
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
191ae66837ab0e33
|
||||||
@@ -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}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
93cc2ea9f2990380
|
||||||
@@ -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}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
454f8ad937f062ee
|
||||||
@@ -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}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
249e119ec9cd58c9
|
||||||
@@ -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}
|
||||||
BIN
target/debug/.fingerprint/ipnet-3bd8d41a9a0a02d3/dep-lib-ipnet
Normal file
BIN
target/debug/.fingerprint/ipnet-3bd8d41a9a0a02d3/dep-lib-ipnet
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
9925ec8a796193db
|
||||||
@@ -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}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
e544a93012f2285f
|
||||||
@@ -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}
|
||||||
BIN
target/debug/.fingerprint/lettre-5c2e089ed257d8b4/dep-lib-lettre
Normal file
BIN
target/debug/.fingerprint/lettre-5c2e089ed257d8b4/dep-lib-lettre
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
617f400705ed3965
|
||||||
@@ -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}
|
||||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user