v5.0.9 — Surveillance timeout session EasyVista (compteur tick 1s, alertes 5min/2min)
This commit is contained in:
@@ -150,9 +150,25 @@ let state = {
|
||||
session: null, // { phpsessid, origin, tabId }
|
||||
currentDate: null, // "YYYY-MM-DD" affiché
|
||||
currentData: null, // résultat parsé (techs, stats, ...)
|
||||
loading: false
|
||||
loading: false,
|
||||
// v5.0.9 : timestamp (ms) auquel la session EV va expirer.
|
||||
// On suppose une durée de 30 min à chaque requête EV réussie.
|
||||
// null = inconnu (pas encore récupéré).
|
||||
sessionExpireAt: null,
|
||||
// v5.0.9 : true pendant une reconnexion en cours (après clic sur "Me
|
||||
// reconnecter" tant que la nouvelle session n'est pas détectée)
|
||||
reconnecting: false,
|
||||
// v5.0.9 : true si la session est expirée (bannière rouge affichée)
|
||||
sessionExpired: false,
|
||||
// v5.0.9 : true si on a déjà fait le ping de confirmation < 5 min
|
||||
sessionPingDone: false
|
||||
};
|
||||
|
||||
// v5.0.9 : constantes session
|
||||
const SESSION_DURATION_MS = 30 * 60 * 1000; // 30 min
|
||||
const SESSION_WARN_THRESHOLD_MS = 5 * 60 * 1000; // 5 min → affichage compteur
|
||||
const SESSION_CRITICAL_THRESHOLD_MS = 2 * 60 * 1000; // 2 min → rouge + modal
|
||||
|
||||
// ─── Annulation coopérative d'un refresh manuel (v3.1) ──────────────────────
|
||||
// Chaque refresh reçoit un "jeton" (entier incrémenté). Les workers lisent
|
||||
// isRefreshAborted() avant chaque fetch : si le jeton a changé ou si
|
||||
@@ -213,6 +229,7 @@ async function init() {
|
||||
initAppFooter(); // v4.2.9 : pied de page discret bas-droite
|
||||
initAppClock(); // v5.0.0 : horloge HH:MM au milieu topbar
|
||||
initAdminMenu(); // v5.0.0 : menu admin caché (5 clics sur titre)
|
||||
initSessionTimer(); // v5.0.9 : compteur de session EV (tick 1s)
|
||||
|
||||
// Initialiser la date = aujourd'hui
|
||||
state.currentDate = todayISO();
|
||||
@@ -250,10 +267,25 @@ async function refreshSessionAndLoad() {
|
||||
hideEvUnreachable();
|
||||
hideSessionExpiredBanner();
|
||||
hideEvUnreachableBanner();
|
||||
state.sessionExpired = false;
|
||||
state.reconnecting = false;
|
||||
fetchAndShowCurrentUser();
|
||||
// v5.0.9 : à chaque démarrage/reconnexion, on suppose que la session vient
|
||||
// d'être rafraîchie à 30 min. updateSessionIndicator va masquer le compteur.
|
||||
markSessionActivity();
|
||||
await loadForDate(state.currentDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* v5.0.9 : doit être appelée à chaque requête EasyVista réussie. Reset le
|
||||
* timer local à 30 min (la session serveur a été renouvelée implicitement).
|
||||
*/
|
||||
function markSessionActivity() {
|
||||
state.sessionExpireAt = Date.now() + SESSION_DURATION_MS;
|
||||
state.sessionPingDone = false; // reset le flag de ping
|
||||
updateSessionIndicator();
|
||||
}
|
||||
|
||||
// v4.2 : fetche l'utilisateur EasyVista connecté (via background.js) et
|
||||
// l'affiche dans la topbar. En cas d'échec ou si aucun nom n'est trouvé,
|
||||
// le badge reste caché.
|
||||
@@ -777,6 +809,215 @@ function initAdminMenu() {
|
||||
title.style.cursor = "default";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// v5.0.9 : Surveillance du timeout de session EasyVista
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Initialise le tick du compteur de session (toutes les secondes).
|
||||
* Pas de requête réseau : décompte purement local depuis state.sessionExpireAt.
|
||||
* En parallèle, un polling 2s actif uniquement en reconnexion, pour détecter
|
||||
* dès que l'user s'est reconnecté dans l'onglet EasyVista ouvert.
|
||||
*/
|
||||
function initSessionTimer() {
|
||||
setInterval(() => {
|
||||
updateSessionIndicator();
|
||||
}, 1000);
|
||||
|
||||
// Polling actif UNIQUEMENT pendant une reconnexion pour détecter le nouveau
|
||||
// PHPSESSID dès qu'il apparaît dans un onglet EV. Rien d'envoyé au serveur
|
||||
// en dehors de ça.
|
||||
setInterval(async () => {
|
||||
if (!state.reconnecting) return;
|
||||
try {
|
||||
const resp = await sendMessage({ type: "getSession" });
|
||||
if (resp && resp.ok && resp.session && resp.session.phpsessid) {
|
||||
const oldPhpsessid = state.session ? state.session.phpsessid : null;
|
||||
if (resp.session.phpsessid !== oldPhpsessid) {
|
||||
console.log("[session] nouvelle session détectée après reconnexion :", resp.session.phpsessid);
|
||||
state.session = resp.session;
|
||||
state.reconnecting = false;
|
||||
state.sessionExpired = false;
|
||||
hideReconnectingBanner();
|
||||
hideSessionExpiredBanner();
|
||||
markSessionActivity();
|
||||
showToast("Reconnecté", "Session EasyVista renouvelée");
|
||||
// Recharger le planning à la date courante sans perdre la position
|
||||
await loadForDate(state.currentDate);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silencieux, on réessayera au prochain tick
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour l'affichage du compteur session dans la topbar.
|
||||
* Règles :
|
||||
* - Session expirée ou reconnexion → compteur caché (bannière gère l'affichage)
|
||||
* - > 5 min restantes → compteur invisible
|
||||
* - 2-5 min → jaune, bouton "Prolonger" visible
|
||||
* - < 2 min → rouge pulse + modal automatique (une seule fois)
|
||||
* - <= 0 → déclenche l'état "expirée"
|
||||
*/
|
||||
function updateSessionIndicator() {
|
||||
const el = document.getElementById("app-session");
|
||||
if (!el) return;
|
||||
|
||||
if (state.sessionExpired || state.reconnecting) {
|
||||
el.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
if (!state.sessionExpireAt) {
|
||||
el.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
const remainingMs = state.sessionExpireAt - Date.now();
|
||||
|
||||
if (remainingMs <= 0) {
|
||||
handleSessionExpired();
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainingMs > SESSION_WARN_THRESHOLD_MS) {
|
||||
el.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
// Zone d'alerte (< 5 min)
|
||||
// v5.0.9 : avant d'afficher l'alerte, on fait UN ping de confirmation
|
||||
// pour vérifier que le serveur est bien d'accord (compteur local parfois
|
||||
// désynchronisé si plusieurs requêtes EV ont rafraîchi sans qu'on update
|
||||
// notre horloge). Une seule fois par cycle.
|
||||
if (!state.sessionPingDone) {
|
||||
state.sessionPingDone = true;
|
||||
sendMessage({ type: "getSessionRemaining" }).then(resp => {
|
||||
if (resp && resp.ok && typeof resp.remainingMs === "number") {
|
||||
state.sessionExpireAt = Date.now() + resp.remainingMs;
|
||||
updateSessionIndicator();
|
||||
}
|
||||
}).catch(() => {});
|
||||
// En attendant, on continue avec l'estimation locale
|
||||
}
|
||||
|
||||
const mm = Math.floor(remainingMs / 60000);
|
||||
const ss = Math.floor((remainingMs % 60000) / 1000);
|
||||
const timeStr = `${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`;
|
||||
el.classList.remove("hidden", "session-warn", "session-critical");
|
||||
|
||||
if (remainingMs <= SESSION_CRITICAL_THRESHOLD_MS) {
|
||||
el.classList.add("session-critical");
|
||||
if (!state._criticalModalShown) {
|
||||
state._criticalModalShown = true;
|
||||
showSessionCriticalModal();
|
||||
}
|
||||
} else {
|
||||
el.classList.add("session-warn");
|
||||
state._criticalModalShown = false;
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<span class="session-icon">⏱</span>
|
||||
<span class="session-time">${timeStr}</span>
|
||||
<button type="button" class="session-extend-btn" title="Prolonger la session de 30 min">🔄 Prolonger</button>
|
||||
`;
|
||||
const extendBtn = el.querySelector(".session-extend-btn");
|
||||
if (extendBtn) {
|
||||
extendBtn.onclick = async () => {
|
||||
extendBtn.disabled = true;
|
||||
extendBtn.textContent = "…";
|
||||
try {
|
||||
const resp = await sendMessage({ type: "extendSession" });
|
||||
if (resp && resp.ok && typeof resp.remainingMs === "number") {
|
||||
state.sessionExpireAt = Date.now() + resp.remainingMs;
|
||||
state.sessionPingDone = false;
|
||||
state._criticalModalShown = false;
|
||||
showToast("Session prolongée", "30 minutes de plus");
|
||||
updateSessionIndicator();
|
||||
} else {
|
||||
throw new Error((resp && resp.error) || "erreur inconnue");
|
||||
}
|
||||
} catch (err) {
|
||||
extendBtn.disabled = false;
|
||||
extendBtn.textContent = "🔄 Prolonger";
|
||||
if (err.message === "session_expired" || err.message === "no_session") {
|
||||
handleSessionExpired();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appelée quand le compteur atteint 0 ou quand une requête EV échoue en
|
||||
* session expirée. Affiche la bannière "Session expirée" avec bouton "Me
|
||||
* reconnecter".
|
||||
*/
|
||||
function handleSessionExpired() {
|
||||
if (state.sessionExpired) return;
|
||||
state.sessionExpired = true;
|
||||
state.sessionExpireAt = null;
|
||||
state._criticalModalShown = false;
|
||||
console.warn("[session] session EV expirée");
|
||||
showSessionExpiredBanner();
|
||||
const el = document.getElementById("app-session");
|
||||
if (el) el.classList.add("hidden");
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal auto quand < 2 min : alerte visuelle forte.
|
||||
*/
|
||||
function showSessionCriticalModal() {
|
||||
showAlertModal({
|
||||
title: "⚠️ Session EasyVista expire bientôt",
|
||||
message: "Votre session EasyVista expire dans moins de 2 minutes. Cliquez sur « Prolonger » pour éviter d'être déconnecté.",
|
||||
buttons: [
|
||||
{ label: "Ignorer", variant: "secondary", action: () => {} },
|
||||
{
|
||||
label: "🔄 Prolonger maintenant",
|
||||
variant: "primary",
|
||||
action: async () => {
|
||||
try {
|
||||
const resp = await sendMessage({ type: "extendSession" });
|
||||
if (resp && resp.ok && typeof resp.remainingMs === "number") {
|
||||
state.sessionExpireAt = Date.now() + resp.remainingMs;
|
||||
state.sessionPingDone = false;
|
||||
state._criticalModalShown = false;
|
||||
showToast("Session prolongée", "30 minutes de plus");
|
||||
updateSessionIndicator();
|
||||
}
|
||||
} catch (e) {
|
||||
handleSessionExpired();
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Appelé au clic "Me reconnecter" dans la bannière. Ouvre EasyVista dans un
|
||||
* nouvel onglet (déclenche Windows SSO Kerberos automatique). Le polling
|
||||
* dans initSessionTimer détectera la nouvelle session et rechargera le viewer.
|
||||
*/
|
||||
async function triggerReconnect() {
|
||||
state.reconnecting = true;
|
||||
hideSessionExpiredBanner();
|
||||
showReconnectingBanner();
|
||||
try {
|
||||
const origin = (state.session && state.session.origin) || "https://itsma.etat-de-vaud.ch";
|
||||
await sendMessage({ type: "openEasyVistaLogin", origin });
|
||||
} catch (err) {
|
||||
console.warn("[session] openEasyVistaLogin failed:", err);
|
||||
state.reconnecting = false;
|
||||
hideReconnectingBanner();
|
||||
showSessionExpiredBanner();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// v5.0.0 : stockage des paramètres admin dans chrome.storage.local.
|
||||
// Clé unique : "admin_config". Contient la config éditable (équipe,
|
||||
// absences récurrentes, statuts etc.). Au 1er lancement : initialisée
|
||||
@@ -6530,15 +6771,59 @@ function hideEvUnreachable() {
|
||||
// que les mises à jour sont arrêtées.
|
||||
function showSessionExpiredBanner() {
|
||||
const b = document.getElementById("session-expired-banner");
|
||||
if (b) b.classList.remove("hidden");
|
||||
// Masquer la bannière EV si présente (on ne montre qu'une bannière à la fois)
|
||||
if (b) {
|
||||
b.classList.remove("hidden");
|
||||
// v5.0.9 : s'assurer que la bannière contient le bouton "Me reconnecter"
|
||||
// et qu'il appelle triggerReconnect (SSO Windows transparent).
|
||||
if (!b.querySelector(".session-expired-reconnect-btn")) {
|
||||
// Chercher le premier .banner-content ou injecter du contenu si vide
|
||||
let content = b.querySelector(".banner-content") || b;
|
||||
// Si déjà du contenu natif, on ajoute juste le bouton à la fin
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "session-expired-reconnect-btn";
|
||||
btn.textContent = "🔄 Me reconnecter";
|
||||
btn.addEventListener("click", () => triggerReconnect());
|
||||
content.appendChild(btn);
|
||||
}
|
||||
}
|
||||
hideEvUnreachableBanner();
|
||||
hideReconnectingBanner();
|
||||
}
|
||||
function hideSessionExpiredBanner() {
|
||||
const b = document.getElementById("session-expired-banner");
|
||||
if (b) b.classList.add("hidden");
|
||||
}
|
||||
|
||||
// v5.0.9 : bannière affichée pendant la reconnexion (remplace la bannière
|
||||
// expirée après clic sur "Me reconnecter")
|
||||
function showReconnectingBanner() {
|
||||
let b = document.getElementById("session-reconnecting-banner");
|
||||
if (!b) {
|
||||
// Créer la bannière si elle n'existe pas (dans le topbar)
|
||||
b = document.createElement("div");
|
||||
b.id = "session-reconnecting-banner";
|
||||
b.className = "banner-reconnecting";
|
||||
b.innerHTML = `
|
||||
<span class="banner-spinner">⏳</span>
|
||||
<span class="banner-text">Reconnexion à EasyVista en cours… Connectez-vous dans l'onglet qui vient de s'ouvrir.</span>
|
||||
`;
|
||||
// L'insérer juste après la topbar
|
||||
const topbar = document.querySelector(".topbar") || document.querySelector("header") || document.body;
|
||||
if (topbar.nextSibling) {
|
||||
topbar.parentNode.insertBefore(b, topbar.nextSibling);
|
||||
} else {
|
||||
document.body.insertBefore(b, document.body.firstChild);
|
||||
}
|
||||
}
|
||||
b.classList.remove("hidden");
|
||||
hideSessionExpiredBanner();
|
||||
}
|
||||
function hideReconnectingBanner() {
|
||||
const b = document.getElementById("session-reconnecting-banner");
|
||||
if (b) b.classList.add("hidden");
|
||||
}
|
||||
|
||||
// v4.2.5 : bannière non bloquante "EasyVista inaccessible"
|
||||
function showEvUnreachableBanner() {
|
||||
const b = document.getElementById("ev-unreachable-banner");
|
||||
|
||||
Reference in New Issue
Block a user