khalid #1
20
README.md
20
README.md
@@ -197,10 +197,22 @@ L'onglet **Utilisateurs** affiche en temps reel les utilisateurs connectes a Ama
|
||||
|
||||
| Fichier | Role |
|
||||
|---------|------|
|
||||
| `awevents_JJ-MM-AA_N.txt` | Source principale : actions utilisateurs, deconnexions explicites |
|
||||
| `isoft_JJ-MM-AA_N.txt` | Complement : evenements de session |
|
||||
| `awevents_JJ-MM-AA_N.log(.gz)` | Source principale : actions utilisateurs, deconnexions explicites |
|
||||
| `awevents.log` | Log actif du jour (sans date dans le nom, serveur HDS) |
|
||||
| `isoft_JJ-MM-AA_N.log(.gz)` | Complement : evenements de session |
|
||||
|
||||
Seuls les fichiers du jour courant sont lus (les fichiers des jours precedents zipes sont ignores pour le tableau temps reel). Le graphique "7 derniers jours" utilise les fichiers `.txt` non zipes s'ils sont disponibles.
|
||||
La detection gere les deux cas du serveur HDS : log actif sans date dans le nom (`awevents.log`) et log zip en cours de journee avec date dans le nom (`awevents_26-04-13_1.log.gz`).
|
||||
|
||||
### Tableau temps reel (aujourd'hui)
|
||||
|
||||
- Colonnes : Utilisateur, Statut, Derniere action, Actions (24h), Depuis
|
||||
- Tri : statut (actif → inactif → deconnecte), puis derniere action la plus recente en premier au sein de chaque groupe
|
||||
|
||||
### Graphique 7 derniers jours
|
||||
|
||||
- Affiche le pic d'utilisateurs simultanes par jour
|
||||
- **Cliquer sur une barre** charge le tableau des utilisateurs de ce jour : Utilisateur, Derniere utilisation, Actions (jour), Duree de presence (premiere → derniere action)
|
||||
- Tri par nombre d'actions decroissant
|
||||
|
||||
### Regles de statut
|
||||
|
||||
@@ -212,7 +224,7 @@ Seuls les fichiers du jour courant sont lus (les fichiers des jours precedents z
|
||||
|
||||
### Configuration du chemin des logs
|
||||
|
||||
Dans **Configuration > Chemin des logs Amadea**, renseignez le chemin complet du dossier contenant les fichiers `awevents_*.txt` et `isoft_*.txt`.
|
||||
Dans **Configuration > Chemin des logs Amadea**, renseignez le chemin complet du dossier contenant les fichiers de logs.
|
||||
|
||||
Valeur par defaut : `C:\ProgramData\ISoft\Amadea Web 8 x64\data\logs`
|
||||
|
||||
|
||||
55
alerter.py
55
alerter.py
@@ -1,8 +1,11 @@
|
||||
"""Envoi d'alertes par email via SMTP."""
|
||||
"""Envoi d'alertes par email via Brevo API ou SMTP."""
|
||||
|
||||
import json
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
|
||||
class EmailAlerter:
|
||||
@@ -14,28 +17,64 @@ class EmailAlerter:
|
||||
|
||||
def is_configured(self):
|
||||
smtp = self._get_smtp_config()
|
||||
return bool(smtp.get("server") and smtp.get("from_email") and smtp.get("to_emails"))
|
||||
has_recipients = bool(smtp.get("from_email") and smtp.get("to_emails"))
|
||||
if smtp.get("brevo_api_key"):
|
||||
return has_recipients
|
||||
return has_recipients and bool(smtp.get("server"))
|
||||
|
||||
def send_alert(self, subject, body):
|
||||
"""Envoie un email d'alerte. Silencieux si SMTP non configure."""
|
||||
"""Envoie un email d'alerte. Silencieux si non configure."""
|
||||
if not self.is_configured():
|
||||
return False, "SMTP non configure"
|
||||
return False, "Email non configure"
|
||||
return self._send_email(subject, body)
|
||||
|
||||
def send_test(self):
|
||||
"""Envoie un email de test pour valider la configuration SMTP."""
|
||||
"""Envoie un email de test pour valider la configuration."""
|
||||
if not self.is_configured():
|
||||
return False, "Configuration SMTP incomplete"
|
||||
return False, "Configuration email incomplete"
|
||||
subject = "[TEST] Supervision - Test de configuration email"
|
||||
body = (
|
||||
"Ceci est un email de test.\n\n"
|
||||
"Si vous recevez ce message, la configuration SMTP est correcte.\n\n"
|
||||
"Si vous recevez ce message, la configuration est correcte.\n\n"
|
||||
"-- Supervision"
|
||||
)
|
||||
return self._send_email(subject, body)
|
||||
|
||||
def _send_email(self, subject, body):
|
||||
smtp_cfg = self._get_smtp_config()
|
||||
if smtp_cfg.get("brevo_api_key"):
|
||||
return self._send_via_brevo(subject, body, smtp_cfg)
|
||||
return self._send_via_smtp(subject, body, smtp_cfg)
|
||||
|
||||
def _send_via_brevo(self, subject, body, smtp_cfg):
|
||||
"""Envoi via l'API REST Brevo."""
|
||||
data = {
|
||||
"sender": {"email": smtp_cfg["from_email"]},
|
||||
"to": [{"email": e} for e in smtp_cfg["to_emails"]],
|
||||
"subject": subject,
|
||||
"textContent": body,
|
||||
}
|
||||
req = urllib.request.Request(
|
||||
"https://api.brevo.com/v3/smtp/email",
|
||||
data=json.dumps(data).encode("utf-8"),
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"api-key": smtp_cfg["brevo_api_key"],
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15):
|
||||
return True, "Email envoye via Brevo"
|
||||
except urllib.error.HTTPError as e:
|
||||
return False, f"Erreur Brevo API: {e.code} {e.reason}"
|
||||
except urllib.error.URLError as e:
|
||||
return False, f"Impossible de joindre Brevo: {e.reason}"
|
||||
except Exception as e:
|
||||
return False, f"Erreur Brevo: {str(e)}"
|
||||
|
||||
def _send_via_smtp(self, subject, body, smtp_cfg):
|
||||
"""Envoi via SMTP classique."""
|
||||
try:
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = smtp_cfg["from_email"]
|
||||
|
||||
31
app.py
31
app.py
@@ -126,10 +126,8 @@ def settings():
|
||||
smtp = cfg.get("smtp", {})
|
||||
# Masquer le mot de passe SMTP dans l'affichage
|
||||
smtp_display = dict(smtp)
|
||||
if smtp_display.get("password"):
|
||||
smtp_display["password_masked"] = "*" * 8
|
||||
else:
|
||||
smtp_display["password_masked"] = ""
|
||||
smtp_display["password_masked"] = "*" * 8 if smtp_display.get("password") else ""
|
||||
smtp_display["brevo_api_key_masked"] = "*" * 8 if smtp_display.get("brevo_api_key") else ""
|
||||
return render_template(
|
||||
"settings.html",
|
||||
config=cfg,
|
||||
@@ -183,6 +181,7 @@ def update_monitoring():
|
||||
@login_required
|
||||
def update_smtp():
|
||||
try:
|
||||
old_smtp = config.get("smtp", {})
|
||||
smtp = {
|
||||
"server": request.form["smtp_server"].strip(),
|
||||
"port": int(request.form["smtp_port"]),
|
||||
@@ -193,14 +192,12 @@ def update_smtp():
|
||||
e.strip() for e in request.form["smtp_to"].split(",") if e.strip()
|
||||
],
|
||||
}
|
||||
# Ne mettre a jour le mot de passe que s'il est fourni
|
||||
# Conserver le mot de passe si non fourni
|
||||
new_password = request.form.get("smtp_password", "")
|
||||
if new_password:
|
||||
smtp["password"] = new_password
|
||||
else:
|
||||
# Garder l'ancien mot de passe
|
||||
old_smtp = config.get("smtp", {})
|
||||
smtp["password"] = old_smtp.get("password", "")
|
||||
smtp["password"] = new_password if new_password else old_smtp.get("password", "")
|
||||
# Conserver la cle Brevo si non fournie
|
||||
new_brevo_key = request.form.get("brevo_api_key", "").strip()
|
||||
smtp["brevo_api_key"] = new_brevo_key if new_brevo_key else old_smtp.get("brevo_api_key", "")
|
||||
|
||||
config.set("smtp", smtp)
|
||||
flash("Configuration SMTP mise a jour.", "success")
|
||||
@@ -349,6 +346,18 @@ def api_users_weekly():
|
||||
return jsonify({"weekly": user_monitor.get_weekly_activity()})
|
||||
|
||||
|
||||
@app.route("/api/users/day/<date_str>")
|
||||
@login_required
|
||||
def api_users_day(date_str):
|
||||
from datetime import datetime as dt
|
||||
try:
|
||||
date = dt.strptime(date_str, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
return jsonify({"error": "Date invalide"}), 400
|
||||
users = user_monitor.get_users_for_date(date)
|
||||
return jsonify({"users": users, "date": date_str})
|
||||
|
||||
|
||||
@app.route("/settings/amadea-log-path", methods=["POST"])
|
||||
@login_required
|
||||
def update_amadea_log_path():
|
||||
|
||||
@@ -54,6 +54,7 @@ def get_default_config():
|
||||
"password": "",
|
||||
"from_email": "",
|
||||
"to_emails": [],
|
||||
"brevo_api_key": "",
|
||||
},
|
||||
"amadea_log_path": r"C:\ProgramData\ISoft\Amadea Web 8 x64\data\logs",
|
||||
"user_status_thresholds": {
|
||||
|
||||
@@ -132,6 +132,17 @@
|
||||
placeholder="admin@example.com, tech@example.com">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 mb-3">
|
||||
<label for="brevo_api_key" class="form-label">
|
||||
<i class="bi bi-key"></i> Cle API Brevo
|
||||
</label>
|
||||
<input type="password" class="form-control" id="brevo_api_key" name="brevo_api_key"
|
||||
placeholder="{% if smtp.brevo_api_key_masked %}{{ smtp.brevo_api_key_masked }}{% else %}Non definie{% endif %}"
|
||||
autocomplete="new-password">
|
||||
<div class="form-text">Si renseignee, la cle API Brevo est utilisee a la place du SMTP. Laissez vide pour conserver la cle actuelle.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> Enregistrer
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
<div id="chart-labels" style="display: flex; gap: 3px; margin-top: 4px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chart-hint" class="d-none mt-2">
|
||||
<small class="text-muted"><i class="bi bi-hand-index"></i> Cliquez sur une barre pour voir les utilisateurs de ce jour.</small>
|
||||
</div>
|
||||
<div id="chart-unavailable" class="d-none">
|
||||
<div class="alert alert-info mb-0">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
@@ -43,12 +46,12 @@
|
||||
<!-- Tableau utilisateurs -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0"><i class="bi bi-person-lines-fill"></i> Utilisateurs connectes aujourd'hui</h6>
|
||||
<h6 class="mb-0" id="table-title"><i class="bi bi-person-lines-fill"></i> Utilisateurs connectes aujourd'hui</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<tr id="table-headers">
|
||||
<th>Utilisateur</th>
|
||||
<th>Statut</th>
|
||||
<th>Derniere action</th>
|
||||
@@ -70,8 +73,10 @@ var STATUS_CONFIG = {
|
||||
deconnecte: { badge: 'bg-secondary', label: 'DECONNECTE' },
|
||||
};
|
||||
|
||||
var currentHourly = [];
|
||||
var currentPeriod = 'today';
|
||||
var currentHourly = [];
|
||||
var currentPeriod = 'today';
|
||||
var selectedBarEl = null;
|
||||
var lastTodayUsers = [];
|
||||
|
||||
/* --- Graphique CSS pur --- */
|
||||
|
||||
@@ -81,15 +86,15 @@ function renderYAxis(max) {
|
||||
var values = [max, Math.round(max / 2), 0];
|
||||
values.forEach(function(v) {
|
||||
var lbl = document.createElement('div');
|
||||
lbl.style.fontSize = '0.6rem';
|
||||
lbl.style.color = '#6c757d';
|
||||
lbl.style.fontSize = '0.6rem';
|
||||
lbl.style.color = '#6c757d';
|
||||
lbl.style.lineHeight = '1';
|
||||
lbl.textContent = v;
|
||||
yaxis.appendChild(lbl);
|
||||
});
|
||||
}
|
||||
|
||||
function renderChart(data) {
|
||||
function renderChart(data, options) {
|
||||
var container = document.getElementById('chart-container');
|
||||
var labelsEl = document.getElementById('chart-labels');
|
||||
var unavail = document.getElementById('chart-unavailable');
|
||||
@@ -99,11 +104,12 @@ function renderChart(data) {
|
||||
unavail.classList.add('d-none');
|
||||
container.textContent = '';
|
||||
labelsEl.textContent = '';
|
||||
selectedBarEl = null;
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
renderYAxis(0);
|
||||
var msg = document.createElement('span');
|
||||
msg.className = 'text-muted small';
|
||||
msg.className = 'text-muted small';
|
||||
msg.textContent = 'Aucune donnee disponible.';
|
||||
container.appendChild(msg);
|
||||
return;
|
||||
@@ -114,7 +120,7 @@ function renderChart(data) {
|
||||
renderYAxis(max);
|
||||
|
||||
data.forEach(function(item) {
|
||||
var count = item.count || 0;
|
||||
var count = item.count || 0;
|
||||
var heightPct = count > 0 ? Math.max((count / max) * 100, 6) : 0;
|
||||
|
||||
var bar = document.createElement('div');
|
||||
@@ -128,6 +134,23 @@ function renderChart(data) {
|
||||
bar.title = item.hour !== undefined
|
||||
? item.hour + 'h : ' + count + ' utilisateur(s)'
|
||||
: item.date + ' : ' + count + ' utilisateur(s)';
|
||||
|
||||
if (options && options.onBarClick && item.date !== undefined) {
|
||||
bar.style.cursor = 'pointer';
|
||||
(function(capturedItem, capturedBar) {
|
||||
capturedBar.addEventListener('click', function() {
|
||||
if (selectedBarEl) {
|
||||
selectedBarEl.style.background = '#0d6efd';
|
||||
selectedBarEl.style.outline = '';
|
||||
}
|
||||
capturedBar.style.background = '#0a58ca';
|
||||
capturedBar.style.outline = '2px solid #0a3fa8';
|
||||
selectedBarEl = capturedBar;
|
||||
options.onBarClick(capturedItem);
|
||||
});
|
||||
})(item, bar);
|
||||
}
|
||||
|
||||
container.appendChild(bar);
|
||||
|
||||
var lbl = document.createElement('div');
|
||||
@@ -151,25 +174,45 @@ function renderWeekly(weekly) {
|
||||
var unavail = document.getElementById('chart-unavailable');
|
||||
var container = document.getElementById('chart-container');
|
||||
var labelsEl = document.getElementById('chart-labels');
|
||||
var hint = document.getElementById('chart-hint');
|
||||
|
||||
var allNull = weekly.every(function(d) { return d.count === null; });
|
||||
if (allNull) {
|
||||
container.style.display = 'none';
|
||||
labelsEl.style.display = 'none';
|
||||
hint.classList.add('d-none');
|
||||
document.getElementById('chart-yaxis').textContent = '';
|
||||
unavail.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
container.style.display = 'flex';
|
||||
labelsEl.style.display = 'flex';
|
||||
unavail.classList.add('d-none');
|
||||
renderChart(weekly.map(function(d) {
|
||||
return { date: d.date, count: d.count === null ? 0 : d.count };
|
||||
}));
|
||||
|
||||
hint.classList.remove('d-none');
|
||||
renderChart(
|
||||
weekly.map(function(d) {
|
||||
return { date: d.date, count: d.count === null ? 0 : d.count };
|
||||
}),
|
||||
{
|
||||
onBarClick: function(item) {
|
||||
loadDayUsers(item.date);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* --- Tableau --- */
|
||||
|
||||
function setTableMode(mode, dateLabel) {
|
||||
var title = document.getElementById('table-title');
|
||||
var thead = document.getElementById('table-headers');
|
||||
if (mode === 'today') {
|
||||
title.innerHTML = '<i class="bi bi-person-lines-fill"></i> Utilisateurs connectes aujourd\'hui';
|
||||
thead.innerHTML = '<th>Utilisateur</th><th>Statut</th><th>Derniere action</th><th>Actions (24h)</th><th>Depuis</th>';
|
||||
} else {
|
||||
title.innerHTML = '<i class="bi bi-person-lines-fill"></i> Utilisateurs — ' + (dateLabel || '');
|
||||
thead.innerHTML = '<th>Utilisateur</th><th>Derniere utilisation</th><th>Actions (jour)</th><th>Duree de presence</th>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(users) {
|
||||
var tbody = document.getElementById('users-tbody');
|
||||
tbody.textContent = '';
|
||||
@@ -189,14 +232,12 @@ function renderTable(users) {
|
||||
var sc = STATUS_CONFIG[u.status] || { badge: 'bg-secondary', label: (u.status || '').toUpperCase() };
|
||||
var tr = document.createElement('tr');
|
||||
|
||||
/* Utilisateur */
|
||||
var tdUser = document.createElement('td');
|
||||
var strong = document.createElement('strong');
|
||||
strong.textContent = u.login || '';
|
||||
tdUser.appendChild(strong);
|
||||
tr.appendChild(tdUser);
|
||||
|
||||
/* Statut */
|
||||
var tdStatus = document.createElement('td');
|
||||
var badge = document.createElement('span');
|
||||
badge.className = 'badge ' + sc.badge;
|
||||
@@ -204,25 +245,20 @@ function renderTable(users) {
|
||||
tdStatus.appendChild(badge);
|
||||
tr.appendChild(tdStatus);
|
||||
|
||||
/* Derniere action */
|
||||
var tdAction = document.createElement('td');
|
||||
tdAction.textContent = u.last_action_time || '\u2014';
|
||||
if (u.last_action_label) {
|
||||
var br = document.createElement('br');
|
||||
var small = document.createElement('small');
|
||||
small.className = 'text-muted';
|
||||
small.className = 'text-muted d-block';
|
||||
small.textContent = u.last_action_label;
|
||||
tdAction.appendChild(br);
|
||||
tdAction.appendChild(small);
|
||||
}
|
||||
tr.appendChild(tdAction);
|
||||
|
||||
/* Actions 24h */
|
||||
var tdCount = document.createElement('td');
|
||||
tdCount.textContent = String(u.action_count_24h || 0);
|
||||
tr.appendChild(tdCount);
|
||||
|
||||
/* Depuis */
|
||||
var tdSince = document.createElement('td');
|
||||
tdSince.textContent = u.connected_since || '\u2014';
|
||||
tr.appendChild(tdSince);
|
||||
@@ -231,6 +267,64 @@ function renderTable(users) {
|
||||
});
|
||||
}
|
||||
|
||||
function renderDayTable(users, dateStr) {
|
||||
setTableMode('day', dateStr);
|
||||
var tbody = document.getElementById('users-tbody');
|
||||
tbody.textContent = '';
|
||||
|
||||
if (!users || users.length === 0) {
|
||||
var tr = document.createElement('tr');
|
||||
var td = document.createElement('td');
|
||||
td.colSpan = 4;
|
||||
td.className = 'text-center text-muted py-3';
|
||||
td.textContent = 'Aucun utilisateur detecte ce jour.';
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
return;
|
||||
}
|
||||
|
||||
users.forEach(function(u) {
|
||||
var tr = document.createElement('tr');
|
||||
|
||||
var tdUser = document.createElement('td');
|
||||
var strong = document.createElement('strong');
|
||||
strong.textContent = u.login || '';
|
||||
tdUser.appendChild(strong);
|
||||
tr.appendChild(tdUser);
|
||||
|
||||
var tdAction = document.createElement('td');
|
||||
tdAction.textContent = u.last_action_time || '\u2014';
|
||||
if (u.last_action_label) {
|
||||
var small = document.createElement('small');
|
||||
small.className = 'text-muted d-block';
|
||||
small.textContent = u.last_action_label;
|
||||
tdAction.appendChild(small);
|
||||
}
|
||||
tr.appendChild(tdAction);
|
||||
|
||||
var tdCount = document.createElement('td');
|
||||
tdCount.textContent = String(u.action_count || 0);
|
||||
tr.appendChild(tdCount);
|
||||
|
||||
var tdDuration = document.createElement('td');
|
||||
tdDuration.textContent = u.duration || '\u2014';
|
||||
tr.appendChild(tdDuration);
|
||||
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function loadDayUsers(date) {
|
||||
fetch('/api/users/day/' + date)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.error) { showAlert('warning', data.error); return; }
|
||||
clearAlert();
|
||||
renderDayTable(data.users || [], date);
|
||||
})
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
/* --- Alertes --- */
|
||||
|
||||
function showAlert(type, message) {
|
||||
@@ -257,20 +351,20 @@ function refreshUsers() {
|
||||
.then(function(data) {
|
||||
if (data.error) {
|
||||
showAlert('warning', data.error);
|
||||
renderTable([]);
|
||||
renderChart([]);
|
||||
if (currentPeriod === 'today') { renderTable([]); renderChart([]); }
|
||||
return;
|
||||
}
|
||||
if (data.no_files) {
|
||||
showAlert('info', "Aucun log disponible pour aujourd'hui.");
|
||||
renderTable([]);
|
||||
renderChart([]);
|
||||
if (currentPeriod === 'today') { renderTable([]); renderChart([]); }
|
||||
return;
|
||||
}
|
||||
clearAlert();
|
||||
renderTable(data.users || []);
|
||||
currentHourly = data.hourly || [];
|
||||
lastTodayUsers = data.users || [];
|
||||
currentHourly = data.hourly || [];
|
||||
if (currentPeriod === 'today') {
|
||||
setTableMode('today');
|
||||
renderTable(lastTodayUsers);
|
||||
renderChart(currentHourly);
|
||||
}
|
||||
document.getElementById('last-update').textContent =
|
||||
@@ -281,10 +375,14 @@ function refreshUsers() {
|
||||
|
||||
document.getElementById('period-select').addEventListener('change', function() {
|
||||
currentPeriod = this.value;
|
||||
var hint = document.getElementById('chart-hint');
|
||||
if (currentPeriod === 'today') {
|
||||
hint.classList.add('d-none');
|
||||
document.getElementById('chart-container').style.display = 'flex';
|
||||
document.getElementById('chart-labels').style.display = 'flex';
|
||||
document.getElementById('chart-unavailable').classList.add('d-none');
|
||||
setTableMode('today');
|
||||
renderTable(lastTodayUsers);
|
||||
renderChart(currentHourly);
|
||||
} else {
|
||||
fetch('/api/users/activity/weekly')
|
||||
|
||||
@@ -30,12 +30,15 @@ def _read_log_file(filepath):
|
||||
return None
|
||||
|
||||
|
||||
_DATE_IN_NAME_RE = re.compile(r'_\d{2}-\d{2}-\d{2}[_.]')
|
||||
|
||||
|
||||
def _log_files_for_date(log_path, prefix, date_str):
|
||||
"""Retourne les fichiers de logs pour un prefixe et une date donnes, tries par index.
|
||||
|
||||
Essai 1 : fichiers avec la date dans le nom (ex: awevents_26-04-13_1.log).
|
||||
Essai 2 : si aucun fichier trouve, cherche sans date dans le nom et filtre
|
||||
par date de modification du fichier (ex: serveur HDS).
|
||||
Cherche les fichiers avec la date dans le nom (ex: awevents_26-04-13_1.log.gz).
|
||||
Pour le jour courant uniquement, inclut aussi les fichiers sans date dans le nom
|
||||
(ex: awevents.log, isoft.log) qui sont les logs actifs du jour.
|
||||
"""
|
||||
def is_valid(f):
|
||||
return f.endswith('.log') or f.endswith('.log.gz')
|
||||
@@ -44,29 +47,19 @@ def _log_files_for_date(log_path, prefix, date_str):
|
||||
m = re.search(r'_(\d+)\.log(\.gz)?$', f)
|
||||
return int(m.group(1)) if m else 0
|
||||
|
||||
# Essai 1 : date dans le nom
|
||||
# Fichiers avec la date dans le nom
|
||||
pattern = os.path.join(log_path, f"{prefix}_{date_str}_*")
|
||||
files = [f for f in glob.glob(pattern) if is_valid(f)]
|
||||
if files:
|
||||
return sorted(files, key=sort_key)
|
||||
|
||||
# Essai 2 : sans date dans le nom, filtrer par date de modification
|
||||
try:
|
||||
target_date = datetime.strptime(date_str, "%y-%m-%d").date()
|
||||
except ValueError:
|
||||
return []
|
||||
|
||||
fallback_pattern = os.path.join(log_path, f"{prefix}_*")
|
||||
files = []
|
||||
for f in glob.glob(fallback_pattern):
|
||||
if not is_valid(f):
|
||||
continue
|
||||
try:
|
||||
mtime = os.path.getmtime(f)
|
||||
if datetime.fromtimestamp(mtime).date() == target_date:
|
||||
# Pour le jour courant uniquement : inclure aussi les fichiers sans date dans le nom
|
||||
today_str = datetime.now().strftime("%y-%m-%d")
|
||||
if date_str == today_str:
|
||||
active_pattern = os.path.join(log_path, f"{prefix}*")
|
||||
for f in glob.glob(active_pattern):
|
||||
fname = os.path.basename(f)
|
||||
if is_valid(f) and not _DATE_IN_NAME_RE.search(fname) and f not in files:
|
||||
files.append(f)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
return sorted(files, key=sort_key)
|
||||
|
||||
|
||||
@@ -153,7 +146,10 @@ class UserMonitor:
|
||||
|
||||
status_order = {"actif": 0, "inactif": 1, "deconnecte": 2}
|
||||
sorted_users = dict(
|
||||
sorted(users.items(), key=lambda x: status_order.get(x[1]["status"], 3))
|
||||
sorted(users.items(), key=lambda x: (
|
||||
status_order.get(x[1]["status"], 3),
|
||||
-(x[1]["last_action_time"].timestamp() if x[1].get("last_action_time") else 0),
|
||||
))
|
||||
)
|
||||
hourly_data = [{"hour": h, "count": len(logins)} for h, logins in sorted(hourly.items())]
|
||||
|
||||
@@ -236,6 +232,49 @@ class UserMonitor:
|
||||
else:
|
||||
user["status"] = "actif"
|
||||
|
||||
def get_users_for_date(self, date):
|
||||
"""Retourne la liste des utilisateurs ayant agi a une date donnee, tries par nb d'actions."""
|
||||
log_path = self.config.get(
|
||||
"amadea_log_path",
|
||||
r"C:\ProgramData\ISoft\Amadea Web 8 x64\data\logs"
|
||||
)
|
||||
if not os.path.isdir(log_path):
|
||||
return []
|
||||
|
||||
date_str = date.strftime("%y-%m-%d")
|
||||
cutoff = datetime(date.year, date.month, date.day)
|
||||
files = _log_files_for_date(log_path, "awevents", date_str)
|
||||
if not files:
|
||||
return []
|
||||
|
||||
users = {}
|
||||
hourly = {h: set() for h in range(24)}
|
||||
for filepath in files:
|
||||
content = _read_log_file(filepath)
|
||||
if content:
|
||||
for line in content.splitlines():
|
||||
self._parse_awevents_line(line, users, cutoff, hourly)
|
||||
|
||||
result = sorted(users.values(), key=lambda u: -u.get("action_count_24h", 0))
|
||||
output = []
|
||||
for u in result:
|
||||
duration = None
|
||||
if u.get("connected_since") and u.get("last_action_time"):
|
||||
mins = int((u["last_action_time"] - u["connected_since"]).total_seconds() / 60)
|
||||
if mins >= 60:
|
||||
duration = f"{mins // 60}h{mins % 60:02d}"
|
||||
else:
|
||||
duration = f"{mins}min"
|
||||
output.append({
|
||||
"login": u["login"],
|
||||
"last_action_time": u["last_action_time"].strftime("%H:%M:%S") if u.get("last_action_time") else None,
|
||||
"last_action_label": u.get("last_action_label", ""),
|
||||
"action_count": u.get("action_count_24h", 0),
|
||||
"first_action_time": u["connected_since"].strftime("%H:%M") if u.get("connected_since") else None,
|
||||
"duration": duration,
|
||||
})
|
||||
return output
|
||||
|
||||
def get_weekly_activity(self):
|
||||
"""Retourne le nombre max d'utilisateurs actifs simultanes par jour (7 derniers jours)."""
|
||||
log_path = self.config.get(
|
||||
|
||||
Reference in New Issue
Block a user