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

View File

@@ -9,13 +9,56 @@
<div id="alert-zone"></div>
<!-- Graphique d'activite horaire -->
<!-- Offcanvas : détail d'un utilisateur sur 7 jours -->
<div class="offcanvas offcanvas-end" tabindex="-1" id="userHistoryCanvas" style="width:420px">
<div class="offcanvas-header border-bottom">
<h6 class="offcanvas-title mb-0">
<i class="bi bi-person-lines-fill"></i>
<span id="canvas-login" class="ms-1"></span>
</h6>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
</div>
<div class="offcanvas-body p-3">
<div class="btn-group btn-group-sm mb-3" role="group" id="canvas-period-btns">
<button type="button" class="btn btn-primary" data-days="7">7 jours</button>
<button type="button" class="btn btn-outline-primary" data-days="30">30 jours</button>
</div>
<div id="canvas-loading" class="text-center py-4">
<div class="spinner-border spinner-border-sm text-primary me-2"></div>
<span class="text-muted small">Chargement...</span>
</div>
<div id="canvas-content" class="d-none">
<p class="text-muted small mb-1" id="canvas-period-label">Actions par jour (7 derniers jours)</p>
<div style="display:flex;gap:4px;height:64px;align-items:flex-end" id="canvas-bars"></div>
<div style="display:flex;gap:4px;margin-top:2px" id="canvas-bar-labels"></div>
<table class="table table-sm mt-3 mb-0">
<thead class="table-light">
<tr>
<th class="small">Date</th>
<th class="small">Actions</th>
<th class="small">Présence</th>
<th class="small">Actif</th>
<th class="small">Sessions</th>
</tr>
</thead>
<tbody id="canvas-tbody"></tbody>
</table>
</div>
<div id="canvas-no-data" class="d-none text-center text-muted py-4">
<i class="bi bi-calendar-x d-block fs-3 mb-2"></i>
Aucune donnée disponible sur 7 jours.
</div>
</div>
</div>
<!-- Graphique -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-bar-chart"></i> Utilisateurs actifs par heure</h6>
<select id="period-select" class="form-select form-select-sm" style="width: auto;">
<option value="today">Aujourd'hui</option>
<option value="7days">7 derniers jours</option>
<option value="30days">30 derniers jours</option>
</select>
</div>
<div class="card-body">
@@ -37,7 +80,7 @@
<div id="chart-unavailable" class="d-none">
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle"></i>
Les donnees historiques des jours precedents ne sont pas disponibles (fichiers archives).
Les données historiques ne sont pas disponibles (fichiers archivés).
</div>
</div>
</div>
@@ -46,18 +89,12 @@
<!-- Tableau utilisateurs -->
<div class="card">
<div class="card-header">
<h6 class="mb-0" id="table-title"><i class="bi bi-person-lines-fill"></i> Utilisateurs connectes aujourd'hui</h6>
<h6 class="mb-0" id="table-title"></h6>
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead>
<tr id="table-headers">
<th>Utilisateur</th>
<th>Statut</th>
<th>Derniere action</th>
<th>Actions (24h)</th>
<th>Depuis</th>
</tr>
<tr id="table-headers"></tr>
</thead>
<tbody id="users-tbody"></tbody>
</table>
@@ -66,34 +103,59 @@
{% endblock %}
{% block scripts %}
<style>
.badge-absent { background-color: #fd7e14; color: #fff; }
tr.row-clickable { cursor: pointer; }
tr.row-clickable:hover td { background-color: rgba(13,110,253,.05); }
</style>
<script>
var STATUS_CONFIG = {
actif: { badge: 'bg-success', label: 'ACTIF' },
inactif: { badge: 'bg-warning text-dark', label: 'INACTIF' },
absent: { badge: 'badge-absent', label: 'ABSENT' },
deconnecte: { badge: 'bg-secondary', label: 'DECONNECTE' },
};
var currentHourly = [];
var currentPeriod = 'today';
var selectedBarEl = null;
var lastTodayUsers = [];
var currentPeriod = 'today';
var selectedBarEl = null;
var lastTodayUsers = [];
var currentHourly = [];
var DAYS_FR = ['Dim','Lun','Mar','Mer','Jeu','Ven','Sam'];
var canvasCurrentLogin = '';
var canvasDays = 7;
/* --- Graphique CSS pur --- */
/* --- Helpers DOM --- */
function el(tag, cls, text) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (text !== undefined) e.textContent = text;
return e;
}
function icon(name) {
var i = document.createElement('i');
i.className = 'bi bi-' + name;
return i;
}
/* --- Y-axis --- */
function renderYAxis(max) {
var yaxis = document.getElementById('chart-yaxis');
yaxis.textContent = '';
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.lineHeight = '1';
lbl.textContent = v;
yaxis.appendChild(lbl);
[max, Math.round(max / 2), 0].forEach(function(v) {
var d = el('div');
d.style.fontSize = '0.6rem';
d.style.color = '#6c757d';
d.style.lineHeight = '1';
d.textContent = v;
yaxis.appendChild(d);
});
}
/* --- Graphique générique --- */
function renderChart(data, options) {
var container = document.getElementById('chart-container');
var labelsEl = document.getElementById('chart-labels');
@@ -108,10 +170,7 @@ function renderChart(data, options) {
if (!data || data.length === 0) {
renderYAxis(0);
var msg = document.createElement('span');
msg.className = 'text-muted small';
msg.textContent = 'Aucune donnee disponible.';
container.appendChild(msg);
container.appendChild(el('span', 'text-muted small', 'Aucune donnée disponible.'));
return;
}
@@ -123,9 +182,9 @@ function renderChart(data, options) {
var count = item.count || 0;
var heightPct = count > 0 ? Math.max((count / max) * 100, 6) : 0;
var bar = document.createElement('div');
var bar = el('div');
bar.style.flex = '1';
bar.style.minWidth = '14px';
bar.style.minWidth = '8px';
bar.style.height = heightPct + '%';
bar.style.background = '#0d6efd';
bar.style.borderRadius = '3px 3px 0 0';
@@ -137,25 +196,24 @@ function renderChart(data, options) {
if (options && options.onBarClick && item.date !== undefined) {
bar.style.cursor = 'pointer';
(function(capturedItem, capturedBar) {
capturedBar.addEventListener('click', function() {
(function(ci, cb) {
cb.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);
cb.style.background = '#0a58ca';
cb.style.outline = '2px solid #0a3fa8';
selectedBarEl = cb;
options.onBarClick(ci);
});
})(item, bar);
}
container.appendChild(bar);
var lbl = document.createElement('div');
var lbl = el('div');
lbl.style.flex = '1';
lbl.style.minWidth = '14px';
lbl.style.minWidth = '8px';
lbl.style.textAlign = 'center';
lbl.style.fontSize = '0.6rem';
lbl.style.color = '#6c757d';
@@ -170,14 +228,13 @@ function renderChart(data, options) {
});
}
function renderWeekly(weekly) {
function renderPeakChart(list) {
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) {
if (list.every(function(d) { return d.count === null; })) {
container.style.display = 'none';
labelsEl.style.display = 'none';
hint.classList.add('d-none');
@@ -188,41 +245,43 @@ function renderWeekly(weekly) {
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);
}
}
list.map(function(d) { return { date: d.date, count: d.count === null ? 0 : d.count }; }),
{ onBarClick: function(item) { loadDayUsers(item.date); } }
);
}
/* --- Tableau --- */
/* --- En-têtes du tableau --- */
function setTableMode(mode, dateLabel) {
var title = document.getElementById('table-title');
var thead = document.getElementById('table-headers');
title.textContent = '';
thead.textContent = '';
title.appendChild(icon('person-lines-fill'));
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>';
title.appendChild(document.createTextNode(' Utilisateurs connectés aujourd\'hui'));
['Utilisateur','Statut','Dernière action','Actions (24h)','Présence / Actif','Depuis'].forEach(function(h) {
thead.appendChild(el('th', null, h));
});
} else {
title.innerHTML = '<i class="bi bi-person-lines-fill"></i> Utilisateurs &mdash; ' + (dateLabel || '');
thead.innerHTML = '<th>Utilisateur</th><th>Derniere utilisation</th><th>Actions (jour)</th><th>Duree de presence</th>';
title.appendChild(document.createTextNode(' Utilisateurs ' + (dateLabel || '')));
['Utilisateur','Dernière utilisation','Actions','Présence / Actif','Sessions'].forEach(function(h) {
thead.appendChild(el('th', null, h));
});
}
}
/* --- Tableau aujourd'hui --- */
function renderTable(users) {
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 = 5;
td.className = 'text-center text-muted py-3';
td.textContent = "Aucun utilisateur detecte aujourd'hui.";
var td = el('td', 'text-center text-muted py-3', "Aucun utilisateur détecté aujourd'hui.");
td.colSpan = 6;
tr.appendChild(td);
tbody.appendChild(tr);
return;
@@ -230,43 +289,54 @@ function renderTable(users) {
users.forEach(function(u) {
var sc = STATUS_CONFIG[u.status] || { badge: 'bg-secondary', label: (u.status || '').toUpperCase() };
var tr = document.createElement('tr');
var tr = el('tr', 'row-clickable');
tr.addEventListener('click', function() { openUserHistory(u.login); });
// Utilisateur
var tdUser = document.createElement('td');
var strong = document.createElement('strong');
strong.textContent = u.login || '';
tdUser.appendChild(strong);
tdUser.appendChild(el('strong', null, u.login || ''));
if ((u.session_count || 1) > 1) {
tdUser.appendChild(el('small', 'text-muted d-block', u.session_count + ' sessions aujourd\'hui'));
}
tr.appendChild(tdUser);
// Statut
var tdStatus = document.createElement('td');
var badge = document.createElement('span');
badge.className = 'badge ' + sc.badge;
badge.textContent = sc.label;
tdStatus.appendChild(badge);
tdStatus.appendChild(el('span', 'badge ' + sc.badge, sc.label));
tr.appendChild(tdStatus);
// Dernière action
var tdAction = document.createElement('td');
tdAction.textContent = u.last_action_time || '\u2014';
tdAction.textContent = u.last_action_time || '';
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);
tdAction.appendChild(el('small', 'text-muted d-block', u.last_action_label));
}
tr.appendChild(tdAction);
var tdCount = document.createElement('td');
tdCount.textContent = String(u.action_count_24h || 0);
tr.appendChild(tdCount);
// Actions (24h)
tr.appendChild(el('td', null, String(u.action_count_24h || 0)));
var tdSince = document.createElement('td');
tdSince.textContent = u.connected_since || '\u2014';
tr.appendChild(tdSince);
// Présence / Actif
var tdPres = document.createElement('td');
if (u.presence_str) {
tdPres.textContent = u.presence_str;
if (u.active_time_str) {
tdPres.appendChild(el('small', 'text-muted d-block', u.active_time_str + ' actif'));
}
} else {
tdPres.textContent = '—';
}
tr.appendChild(tdPres);
// Depuis
tr.appendChild(el('td', null, u.connected_since || '—'));
tbody.appendChild(tr);
});
}
/* --- Tableau jour historique --- */
function renderDayTable(users, dateStr) {
setTableMode('day', dateStr);
var tbody = document.getElementById('users-tbody');
@@ -274,42 +344,45 @@ function renderDayTable(users, dateStr) {
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.';
var td = el('td', 'text-center text-muted py-3', 'Aucun utilisateur détecté ce jour.');
td.colSpan = 5;
tr.appendChild(td);
tbody.appendChild(tr);
return;
}
users.forEach(function(u) {
var tr = document.createElement('tr');
var tr = el('tr', 'row-clickable');
tr.addEventListener('click', function() { openUserHistory(u.login); });
var tdUser = document.createElement('td');
var strong = document.createElement('strong');
strong.textContent = u.login || '';
tdUser.appendChild(strong);
tdUser.appendChild(el('strong', null, u.login || ''));
if (u.first_action_time) {
tdUser.appendChild(el('small', 'text-muted d-block', 'depuis ' + u.first_action_time));
}
tr.appendChild(tdUser);
var tdAction = document.createElement('td');
tdAction.textContent = u.last_action_time || '\u2014';
var tdAct = document.createElement('td');
tdAct.textContent = u.last_action_time || '';
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);
tdAct.appendChild(el('small', 'text-muted d-block', u.last_action_label));
}
tr.appendChild(tdAction);
tr.appendChild(tdAct);
var tdCount = document.createElement('td');
tdCount.textContent = String(u.action_count || 0);
tr.appendChild(tdCount);
tr.appendChild(el('td', null, String(u.action_count || 0)));
var tdDuration = document.createElement('td');
tdDuration.textContent = u.duration || '\u2014';
tr.appendChild(tdDuration);
var tdPres = document.createElement('td');
if (u.presence) {
tdPres.textContent = u.presence;
if (u.active_time) {
tdPres.appendChild(el('small', 'text-muted d-block', u.active_time + ' actif'));
}
} else {
tdPres.textContent = '—';
}
tr.appendChild(tdPres);
tr.appendChild(el('td', null, String(u.sessions || 1)));
tbody.appendChild(tr);
});
}
@@ -325,17 +398,118 @@ function loadDayUsers(date) {
.catch(function() {});
}
/* --- Offcanvas historique utilisateur --- */
function setCanvasDays(days) {
canvasDays = days;
var btns = document.querySelectorAll('#canvas-period-btns button');
btns.forEach(function(b) {
var active = parseInt(b.getAttribute('data-days')) === days;
b.className = active ? 'btn btn-sm btn-primary' : 'btn btn-sm btn-outline-primary';
});
document.getElementById('canvas-period-label').textContent =
'Actions par jour (' + days + ' derniers jours)';
}
function loadCanvasHistory(login, days) {
document.getElementById('canvas-loading').classList.remove('d-none');
document.getElementById('canvas-content').classList.add('d-none');
document.getElementById('canvas-no-data').classList.add('d-none');
fetch('/api/users/' + encodeURIComponent(login) + '/history?days=' + days)
.then(function(r) { return r.json(); })
.then(function(data) {
document.getElementById('canvas-loading').classList.add('d-none');
var history = data.history || [];
if (!history.some(function(d) { return d.action_count !== null; })) {
document.getElementById('canvas-no-data').classList.remove('d-none');
return;
}
renderUserHistory(history);
document.getElementById('canvas-content').classList.remove('d-none');
})
.catch(function() {
document.getElementById('canvas-loading').classList.add('d-none');
document.getElementById('canvas-no-data').classList.remove('d-none');
});
}
function openUserHistory(login) {
canvasCurrentLogin = login;
document.getElementById('canvas-login').textContent = login;
setCanvasDays(canvasDays);
bootstrap.Offcanvas.getOrCreateInstance(
document.getElementById('userHistoryCanvas')
).show();
loadCanvasHistory(login, canvasDays);
}
document.getElementById('canvas-period-btns').addEventListener('click', function(e) {
var btn = e.target.closest('button[data-days]');
if (!btn || !canvasCurrentLogin) return;
var days = parseInt(btn.getAttribute('data-days'));
setCanvasDays(days);
loadCanvasHistory(canvasCurrentLogin, days);
});
function renderUserHistory(history) {
var barsEl = document.getElementById('canvas-bars');
var lblsEl = document.getElementById('canvas-bar-labels');
barsEl.textContent = '';
lblsEl.textContent = '';
var max = 1;
history.forEach(function(d) { if ((d.action_count || 0) > max) max = d.action_count; });
history.forEach(function(d) {
var count = d.action_count || 0;
var h = count > 0 ? Math.max(Math.round((count / max) * 64), 6) : 4;
var bar = el('div');
bar.style.flex = '1';
bar.style.height = h + 'px';
bar.style.background = count > 0 ? '#0d6efd' : '#e9ecef';
bar.style.borderRadius = '3px 3px 0 0';
bar.title = d.date + ' : ' + count + ' actions';
barsEl.appendChild(bar);
var dt = new Date(d.date);
var lbl = el('div', null, DAYS_FR[dt.getUTCDay()] + ' ' + dt.getUTCDate());
lbl.style.flex = '1';
lbl.style.textAlign = 'center';
lbl.style.fontSize = '0.6rem';
lbl.style.color = '#6c757d';
lblsEl.appendChild(lbl);
});
var tbody = document.getElementById('canvas-tbody');
tbody.textContent = '';
history.forEach(function(d) {
var tr = document.createElement('tr');
var dt = new Date(d.date);
var dLbl = DAYS_FR[dt.getUTCDay()] + ' ' + dt.getUTCDate() + '/' + (dt.getUTCMonth() + 1);
if (!d.action_count) {
var td = el('td', 'text-muted small', dLbl + ' — aucun log');
td.colSpan = 5;
tr.appendChild(td);
} else {
[dLbl, String(d.action_count), d.presence || '—', d.active_time || '—', String(d.sessions || 1)]
.forEach(function(txt) { tr.appendChild(el('td', 'small', txt)); });
}
tbody.appendChild(tr);
});
}
/* --- Alertes --- */
function showAlert(type, message) {
var zone = document.getElementById('alert-zone');
zone.textContent = '';
var div = document.createElement('div');
div.className = 'alert alert-' + type;
var icon = document.createElement('i');
icon.className = 'bi bi-exclamation-triangle me-1';
div.appendChild(icon);
div.appendChild(document.createTextNode(message));
var div = el('div', 'alert alert-' + type);
div.appendChild(icon('exclamation-triangle'));
div.appendChild(document.createTextNode(' ' + message));
zone.appendChild(div);
}
@@ -368,7 +542,7 @@ function refreshUsers() {
renderChart(currentHourly);
}
document.getElementById('last-update').textContent =
'Mis a jour : ' + new Date().toLocaleTimeString('fr-FR');
'Mis à jour : ' + new Date().toLocaleTimeString('fr-FR');
})
.catch(function() {});
}
@@ -384,14 +558,20 @@ document.getElementById('period-select').addEventListener('change', function() {
setTableMode('today');
renderTable(lastTodayUsers);
renderChart(currentHourly);
} else {
} else if (currentPeriod === '7days') {
fetch('/api/users/activity/weekly')
.then(function(r) { return r.json(); })
.then(function(data) { renderWeekly(data.weekly || []); })
.then(function(data) { renderPeakChart(data.weekly || []); })
.catch(function() {});
} else if (currentPeriod === '30days') {
fetch('/api/users/activity/monthly')
.then(function(r) { return r.json(); })
.then(function(data) { renderPeakChart(data.monthly || []); })
.catch(function() {});
}
});
setTableMode('today');
refreshUsers();
setInterval(refreshUsers, 30000);
</script>