From 6794360887e6edccec85938ae6b37c7af7fa97e6 Mon Sep 17 00:00:00 2001 From: Quentin Rouiller Date: Tue, 21 Apr 2026 15:44:14 +0200 Subject: [PATCH] =?UTF-8?q?v5.0.11=20=E2=80=94=20D=C3=A9tection=20contexte?= =?UTF-8?q?=20r=C3=A9seau=20(interne/externe=20via=20SSO)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 99 +++++++++++++++++++++++++++++-- manifest.json | 2 +- viewer.css | 54 +++++++++++++++++ viewer.js | 159 +++++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 294 insertions(+), 20 deletions(-) diff --git a/background.js b/background.js index 91cf038..9861e07 100644 --- a/background.js +++ b/background.js @@ -292,6 +292,80 @@ async function extendSessionKeepAlive(origin, phpsessid) { return ms; } +// ============================================================================ +// v5.0.11 : détection automatique du contexte réseau (interne / externe) +// ============================================================================ + +/** + * Détecte si on est sur le réseau interne (itsma.etat-de-vaud.ch accessible) + * ou externe (seul itsma.vd.ch accessible). Fait un HEAD test avec timeout + * court sur l'URL interne : si ça répond, on est interne ; sinon externe. + * + * Le résultat est mis en cache dans chrome.storage.local pendant 1h pour + * éviter de refaire le test à chaque démarrage. + * + * @param {boolean} force - si true, ignore le cache et refait le test + * @returns {Promise<"internal"|"external">} + */ +async function detectNetworkContext(force = false) { + const CACHE_KEY = "network_context"; + const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1h + + if (!force) { + try { + const data = await chrome.storage.local.get([CACHE_KEY]); + const cached = data[CACHE_KEY]; + if (cached && cached.detectedAt && (Date.now() - cached.detectedAt) < CACHE_MAX_AGE_MS) { + console.log("[bg] detectNetworkContext : cache hit =", cached.networkContext); + return cached.networkContext; + } + } catch (e) {} + } + + // Test HEAD sur l'URL interne avec timeout 2.5 sec + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 2500); + let context = "external"; + try { + console.log("[bg] detectNetworkContext : test de itsma.etat-de-vaud.ch..."); + const r = await fetch("https://itsma.etat-de-vaud.ch/", { + method: "HEAD", + signal: controller.signal, + credentials: "omit" // pas besoin des cookies pour ce test + }); + clearTimeout(timer); + // Tout statut HTTP (même 302, 404, 403) indique que le serveur est joignable + console.log("[bg] detectNetworkContext : interne accessible (status=" + r.status + ")"); + context = "internal"; + } catch (err) { + clearTimeout(timer); + // Timeout, DNS unreachable, erreur réseau = domaine interne inaccessible + console.log("[bg] detectNetworkContext : interne inaccessible, on est en externe (" + err.name + ")"); + context = "external"; + } + + // Mettre en cache + try { + await chrome.storage.local.set({ + [CACHE_KEY]: { + networkContext: context, + detectedAt: Date.now() + } + }); + } catch (e) {} + + return context; +} + +/** + * Retourne l'origine EV à utiliser selon le contexte réseau détecté. + */ +function originForContext(context) { + return context === "internal" + ? "https://itsma.etat-de-vaud.ch" + : "https://itsma.vd.ch"; +} + // ============================================================================ // v4.2 : récupération de l'utilisateur connecté // ============================================================================ @@ -1004,21 +1078,36 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.type === "openEasyVistaLogin") { // v5.0.9 : ouvre EasyVista dans un nouvel onglet pour provoquer // le SSO Windows automatique (reconnexion transparente). - // v5.0.10 : fallback sur itsma.vd.ch (externe, accessible de partout) - // au lieu de .etat-de-vaud.ch (inaccessible en télétravail). - const origin = msg.origin || "https://itsma.vd.ch"; + // v5.0.11 : URL simplifiée (racine domaine au lieu de eventName=...), + // et utilise le contexte réseau détecté si l'origine n'est pas fournie. + let origin = msg.origin; + if (!origin) { + const context = await detectNetworkContext(); + origin = originForContext(context); + } try { const tab = await chrome.tabs.create({ - url: `${origin}/index.php?eventName=HelpDesk_PlanningItem`, + url: `${origin}/`, // racine → EV redirige vers SSO si besoin active: true }); - sendResponse({ ok: true, tabId: tab.id }); + sendResponse({ ok: true, tabId: tab.id, origin }); } catch (err) { sendResponse({ ok: false, error: err.message || String(err) }); } return; } + if (msg.type === "detectNetwork") { + // v5.0.11 : détecte si on est en interne ou externe. + const context = await detectNetworkContext(!!msg.force); + sendResponse({ + ok: true, + context, // "internal" | "external" + origin: originForContext(context) // URL correspondante + }); + return; + } + if (msg.type === "cleanupOldCaches") { const removed = await cleanupOldCaches(msg.daysToKeep || 7); sendResponse({ ok: true, removed }); diff --git a/manifest.json b/manifest.json index c58d726..7e886b3 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Planification", - "version": "5.0.10", + "version": "5.0.11", "description": "Vue claire et rapide du planning des techniciens EasyVista. Regroupe interventions et réservations par tech, affiche horaires, contact, lieu, catégorie et statut en un coup d'œil.", "permissions": ["activeTab", "scripting", "storage", "tabs", "alarms"], "host_permissions": [ diff --git a/viewer.css b/viewer.css index d7db0bb..564a91f 100644 --- a/viewer.css +++ b/viewer.css @@ -2336,3 +2336,57 @@ header.topbar::before { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } + +/* ========================================================================== + v5.0.11 : bannières reconnect avec boutons Annuler + choix réseau + ========================================================================== */ + +.banner-reconnecting .banner-cancel-btn, +.banner-reconnect-failed .banner-cancel-btn { + margin-left: auto; + padding: 5px 12px; + background: transparent; + color: inherit; + border: 1px solid currentColor; + border-radius: 4px; + font-size: 13px; + cursor: pointer; + font-weight: 500; + transition: background 0.2s; +} +.banner-reconnecting .banner-cancel-btn:hover, +.banner-reconnect-failed .banner-cancel-btn:hover { + background: rgba(255, 255, 255, 0.15); +} + +.banner-reconnect-failed { + background: #e67e22; + color: #fff; + padding: 10px 20px; + display: flex; + align-items: center; + gap: 12px; + font-size: 14px; + font-weight: 500; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} +.banner-reconnect-failed.hidden { + display: none; +} +.banner-reconnect-failed .banner-icon { + font-size: 18px; +} +.banner-reconnect-failed .banner-btn-primary { + padding: 6px 14px; + border-radius: 4px; + background: #fff; + color: #c0392b; + border: none; + font-weight: 600; + cursor: pointer; + font-size: 13px; + transition: background 0.2s; +} +.banner-reconnect-failed .banner-btn-primary:hover { + background: #f8d7da; +} diff --git a/viewer.js b/viewer.js index 0832823..e87648b 100644 --- a/viewer.js +++ b/viewer.js @@ -163,7 +163,12 @@ let state = { // ou itsma.etat-de-vaud.ch selon qu'on est en externe ou interne). // Conservée même quand state.session est null, pour savoir où rediriger // lors de la reconnexion. - lastKnownOrigin: null + lastKnownOrigin: null, + // v5.0.11 : contexte réseau détecté ("internal" ou "external" ou null). + // Détecté automatiquement au démarrage par un HEAD test sur l'URL interne. + networkContext: null, + // v5.0.11 : timer (setTimeout id) pour le timeout de reconnexion 90 sec + reconnectTimeoutId: null }; // v5.0.9 : constantes session @@ -171,6 +176,10 @@ 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 +// v5.0.11 : timeout de la reconnexion. Si l'user n'est pas reconnecté +// dans ce délai, on bascule en état "Reconnexion échouée" avec choix du réseau. +const RECONNECT_TIMEOUT_MS = 90 * 1000; // 90 sec + // ─── 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 @@ -237,14 +246,34 @@ async function init() { state.currentDate = todayISO(); document.getElementById("date-picker").value = state.currentDate; - // v4.2 : l'auto-refresh 12h/15h a été supprimé. Les rafraîchissements sont - // désormais soit manuels (boutons Actualiser / Tout recharger), soit au - // premier chargement si aucun cache n'existe pour la date. + // v5.0.11 : détecter le contexte réseau en arrière-plan (non bloquant) + detectNetworkContextAsync(); // Charger la sesson puis le planning await refreshSessionAndLoad(); } +/** + * v5.0.11 : détecte si on est en interne (bureau VPN) ou externe (télétravail), + * de manière asynchrone au démarrage. Résultat utilisé pour choisir le bon + * domaine lors de la reconnexion. + */ +async function detectNetworkContextAsync(force = false) { + try { + const resp = await sendMessage({ type: "detectNetwork", force }); + if (resp && resp.ok) { + state.networkContext = resp.context; + // Si on n'a pas encore de lastKnownOrigin, on prend celui du contexte détecté + if (!state.lastKnownOrigin) { + state.lastKnownOrigin = resp.origin; + } + console.log("[viewer] réseau détecté :", resp.context, "→", resp.origin); + } + } catch (e) { + console.warn("[viewer] détection réseau échouée", e); + } +} + async function refreshSessionAndLoad() { const resp = await sendMessage({ type: "getSession" }); if (!resp.ok || !resp.session) { @@ -851,11 +880,18 @@ function initSessionTimer() { 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); + // v5.0.11 : annuler le timeout de reconnexion puisque ça a marché + if (state.reconnectTimeoutId) { + clearTimeout(state.reconnectTimeoutId); + state.reconnectTimeoutId = null; + } state.session = resp.session; + if (resp.session.origin) state.lastKnownOrigin = resp.session.origin; state.reconnecting = false; state.sessionExpired = false; hideReconnectingBanner(); hideSessionExpiredBanner(); + hideReconnectFailedBanner(); markSessionActivity(); showToast("Reconnecté", "Session EasyVista renouvelée"); // Recharger le planning à la date courante sans perdre la position @@ -1019,19 +1055,46 @@ function showSessionCriticalModal() { * dans initSessionTimer détectera la nouvelle session et rechargera le viewer. * * v5.0.10 : utilise l'origine dynamique (interne ou externe selon le réseau). - * Priorité : lastKnownOrigin (mémorisée quand ça marchait) > session.origin > - * itsma.vd.ch (externe, accessible de partout) en fallback. + * v5.0.11 : détecte le contexte réseau avant d'ouvrir (si pas déjà connu) + + * timeout 90s : si pas reconnecté après ce délai, propose choix manuel. + * + * @param {string} [forcedOrigin] - origine à forcer (pour le choix manuel + * dans le fallback après timeout). Si absent : détection auto. */ -async function triggerReconnect() { +async function triggerReconnect(forcedOrigin) { state.reconnecting = true; hideSessionExpiredBanner(); + hideReconnectFailedBanner(); showReconnectingBanner(); + + // Annuler tout timeout précédent + if (state.reconnectTimeoutId) { + clearTimeout(state.reconnectTimeoutId); + state.reconnectTimeoutId = null; + } + try { - const origin = state.lastKnownOrigin - || (state.session && state.session.origin) - || "https://itsma.vd.ch"; + let origin = forcedOrigin; + if (!origin) { + // v5.0.11 : re-détecter le réseau à chaque expiration pour gérer le + // cas où on a changé de contexte (bureau → TT) pendant la session. + await detectNetworkContextAsync(true); + origin = state.lastKnownOrigin + || (state.session && state.session.origin) + || "https://itsma.vd.ch"; + } console.log("[session] triggerReconnect → ouverture de", origin); await sendMessage({ type: "openEasyVistaLogin", origin }); + + // Démarrer le timeout 90s : si pas reconnecté, basculer en mode "Échec" + state.reconnectTimeoutId = setTimeout(() => { + if (state.reconnecting && !state.session) { + console.warn("[session] reconnexion timeout 90s → bannière échec"); + state.reconnecting = false; + hideReconnectingBanner(); + showReconnectFailedBanner(); + } + }, RECONNECT_TIMEOUT_MS); } catch (err) { console.warn("[session] openEasyVistaLogin failed:", err); state.reconnecting = false; @@ -1040,6 +1103,21 @@ async function triggerReconnect() { } } +/** + * v5.0.11 : l'user clique "Annuler" pendant la reconnexion. On arrête le + * polling/timeout et on revient à l'état "Session expirée" normal. + */ +function cancelReconnect() { + if (state.reconnectTimeoutId) { + clearTimeout(state.reconnectTimeoutId); + state.reconnectTimeoutId = null; + } + state.reconnecting = false; + hideReconnectingBanner(); + hideReconnectFailedBanner(); + showSessionExpiredBanner(); +} + // v5.0.0 : stockage des paramètres admin dans chrome.storage.local. // Clé unique : "admin_config". Contient la config éditable (équipe, @@ -6821,16 +6899,13 @@ function hideSessionExpiredBanner() { // v5.0.9 : bannière affichée pendant la reconnexion (remplace la bannière // expirée après clic sur "Me reconnecter") +// v5.0.11 : ajoute un bouton "Annuler" pour interrompre le processus. function showReconnectingBanner() { let b = document.getElementById("session-reconnecting-banner"); if (!b) { b = document.createElement("div"); b.id = "session-reconnecting-banner"; b.className = "banner-reconnecting"; - b.innerHTML = ` - - - `; const topbar = document.querySelector(".topbar") || document.querySelector("header") || document.body; if (topbar.nextSibling) { topbar.parentNode.insertBefore(b, topbar.nextSibling); @@ -6838,14 +6913,70 @@ function showReconnectingBanner() { document.body.insertBefore(b, document.body.firstChild); } } + b.innerHTML = ` + + + + `; + const cancelBtn = b.querySelector(".banner-cancel-btn"); + if (cancelBtn) cancelBtn.addEventListener("click", () => cancelReconnect()); b.classList.remove("hidden"); hideSessionExpiredBanner(); + hideReconnectFailedBanner(); } function hideReconnectingBanner() { const b = document.getElementById("session-reconnecting-banner"); if (b) b.classList.add("hidden"); } +// v5.0.11 : bannière "Reconnexion échouée" avec choix manuel du réseau +// (Bureau/Télétravail). Affichée après timeout 90s de reconnexion. +function showReconnectFailedBanner() { + let b = document.getElementById("session-reconnect-failed-banner"); + if (!b) { + b = document.createElement("div"); + b.id = "session-reconnect-failed-banner"; + b.className = "banner-reconnect-failed"; + 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.innerHTML = ` + + + + + + `; + // Boutons de choix de réseau : retry avec origine forcée + b.querySelectorAll(".banner-btn-primary").forEach(btn => { + btn.addEventListener("click", () => { + const origin = btn.dataset.origin; + hideReconnectFailedBanner(); + triggerReconnect(origin); + }); + }); + // Bouton Annuler : retour à la bannière "Session expirée" simple + const cancelBtn = b.querySelector(".banner-cancel-btn"); + if (cancelBtn) cancelBtn.addEventListener("click", () => { + hideReconnectFailedBanner(); + showSessionExpiredBanner(); + }); + b.classList.remove("hidden"); + hideSessionExpiredBanner(); + hideReconnectingBanner(); +} +function hideReconnectFailedBanner() { + const b = document.getElementById("session-reconnect-failed-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");