diff --git a/background.js b/background.js index 141d970..5079a86 100644 --- a/background.js +++ b/background.js @@ -157,12 +157,41 @@ async function fetchXhr2(origin, phpsessid, actionId) { async function fetchFicheHtml(origin, phpsessid, formLink) { const url = `${origin}/index.php?${formLink}&PHPSESSID=${encodeURIComponent(phpsessid)}`; console.log("[bg] fetchFicheHtml →", url.substring(0, 120)); - const r = await evFetch(url, origin); - if (!r.ok) { - const err = new Error("HTTP " + r.status); - err.kind = classifyHttpStatus(r.status); - err.status = r.status; - throw err; + + // v2026.5.16 : juste après une reconnexion SSO, EasyVista retourne parfois + // une page intermédiaire tronquée (~8 Ko au lieu de ~250 Ko), le temps que + // les cookies SSO/Kerberos se propagent. On fait jusqu'à 3 tentatives avec + // 1.5s entre chaque si on détecte une taille suspecte. + const MAX_RETRIES = 3; + const RETRY_DELAY_MS = 1500; + const MIN_VALID_SIZE = 20000; // < 20 Ko = probablement page intermédiaire + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + const r = await evFetch(url, origin); + if (!r.ok) { + const err = new Error("HTTP " + r.status); + err.kind = classifyHttpStatus(r.status); + err.status = r.status; + throw err; + } + const html = await r.text(); + console.log(`[bg] fiche status = ${r.status} | taille = ${html.length}${attempt > 1 ? ` (tentative ${attempt}/${MAX_RETRIES})` : ""}`); + + // Si réponse clairement une redirection courte → login expiré, inutile de retry + if (html.length < 500) { + console.warn("[bg] ⚠ fiche très courte, contenu =", JSON.stringify(html)); + return html; + } + + // Si taille suspecte (< 20 Ko), probable page intermédiaire SSO : retry + if (html.length < MIN_VALID_SIZE && attempt < MAX_RETRIES) { + console.warn(`[bg] ⚠ fiche anormalement petite (${html.length} octets), retry dans ${RETRY_DELAY_MS} ms...`); + await new Promise(res => setTimeout(res, RETRY_DELAY_MS)); + continue; + } + + // Sinon : on retourne ce qu'on a + return html; } const html = await r.text(); console.log("[bg] fiche status =", r.status, "| taille =", html.length); diff --git a/manifest.json b/manifest.json index eb9b52b..8667fb4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Planification", - "version": "5.0.12", + "version": "2026.5.16", "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.", "browser_specific_settings": { "gecko": { diff --git a/viewer.css b/viewer.css index 564a91f..cdb7dc4 100644 --- a/viewer.css +++ b/viewer.css @@ -320,6 +320,7 @@ html, body { display: flex; align-items: center; gap: 4px; + flex-wrap: nowrap; } .btn-nav { diff --git a/viewer.js b/viewer.js index e87648b..550b49d 100644 --- a/viewer.js +++ b/viewer.js @@ -131,6 +131,7 @@ function deriveShortTitle(iv) { function deriveColorKey(iv) { if (iv.type === "AL-Reservation") return "reservation"; + if (iv.type === "AL-Absence") return "absence"; // v5.0.15 : couleur noire/gris foncé if (iv.ref && /^I\d/.test(iv.ref)) return "incident"; if (isRollOut(iv)) return "rollout"; if (isRecupAction(iv)) return "recup"; @@ -241,10 +242,12 @@ async function init() { 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) + initDateCustomPicker(); // v2026.5.17 : faux input date avec jour // Initialiser la date = aujourd'hui state.currentDate = todayISO(); document.getElementById("date-picker").value = state.currentDate; + updateDatePickerDayLabel(state.currentDate); // v2026.5.16 : label "Mardi" // v5.0.11 : détecter le contexte réseau en arrière-plan (non bloquant) detectNetworkContextAsync(); @@ -398,7 +401,21 @@ function toggleUserNamePopup() { return; } if (!state.currentUser || !state.currentUser.name) return; - popup.textContent = state.currentUser.name; + + // v2026.5.17 : afficher aussi le temps restant de la session (MM:SS) avec + // une couleur qui dépend du seuil (vert/jaune/rouge). + popup.innerHTML = ""; + const nameEl = document.createElement("div"); + nameEl.className = "user-name-popup-name"; + nameEl.textContent = state.currentUser.name; + popup.appendChild(nameEl); + + const sessEl = document.createElement("div"); + sessEl.className = "user-name-popup-session"; + sessEl.id = "user-name-popup-session"; + _renderUserPopupSessionLine(sessEl); + popup.appendChild(sessEl); + popup.classList.remove("hidden"); badge.classList.add("open"); // Positionne juste en dessous de la pastille @@ -413,6 +430,38 @@ function hideUserNamePopup() { if (badge) badge.classList.remove("open"); } +// v2026.5.17 : remplit la ligne "Session : MM:SS" avec couleur selon seuil. +// Recalcule à chaque appel — appelée aussi par le tick session pour rafraîchir. +function _renderUserPopupSessionLine(el) { + if (!el) return; + const remainingMs = _getSessionRemainingMs(); + if (remainingMs == null) { + el.textContent = "Session : —"; + el.className = "user-name-popup-session"; + return; + } + const mins = Math.floor(remainingMs / 60000); + const secs = Math.floor((remainingMs % 60000) / 1000); + const txt = `Session : ${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; + el.textContent = txt; + el.className = "user-name-popup-session"; + if (remainingMs <= SESSION_CRITICAL_THRESHOLD_MS) { + el.classList.add("session-critical"); + } else if (remainingMs <= SESSION_WARN_THRESHOLD_MS) { + el.classList.add("session-warn"); + } else { + el.classList.add("session-ok"); + } +} + +// v2026.5.17 : récupère en ms le temps restant avant expiration de la session. +// Retourne null si on ne connaît pas encore (pas de session ouverte). +function _getSessionRemainingMs() { + if (!state.sessionExpireAt) return null; + const remaining = state.sessionExpireAt - Date.now(); + return remaining > 0 ? remaining : 0; +} + // ============================================================================ // Thème clair/sombre // ============================================================================ @@ -516,18 +565,65 @@ function bindTopbar() { } } }); + // v2026.5.20 : nouveau comportement de la touche Échap + // - Appui court : ferme uniquement le popup SOUS la souris (normal ou + // minimisé). Si la souris n'est sur aucun popup, ne fait rien. + // Ferme aussi le popup user-badge et la grande bulle anchored. + // - Maintenu ≥ 3 secondes : ferme TOUS les popups flottants, mais garde + // les pastilles dock (popups "réduits" en bas). + let _escHoldTimer = null; + let _escHoldTriggered = false; + const ESC_HOLD_MS = 3000; + document.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - hideUserNamePopup(); - // v4.2.4 : Échap ferme aussi la grande bulle anchored - const tip = tooltipEl(); - if (tip && tip.dataset.mode === "anchored" && tip.classList.contains("visible")) { - hideTooltip({ force: true }); - } - // v4.3.0 : Échap ferme TOUS les popups épinglés (le user veut tout fermer) - if (typeof closeAllPinnedPopups === "function") { - closeAllPinnedPopups(); + if (e.key !== "Escape") return; + // keydown peut se répéter si la touche est maintenue ; on ignore les répétitions. + if (e.repeat) return; + // Armer le timer "maintenu 3s" + _escHoldTriggered = false; + if (_escHoldTimer) clearTimeout(_escHoldTimer); + _escHoldTimer = setTimeout(() => { + _escHoldTriggered = true; + _escHoldTimer = null; + // Fermer TOUS les popups flottants (normaux + minimisés) mais pas les dockés + document.querySelectorAll(".pinned-popup:not(.pinned-popup-reduced)").forEach(p => { + try { p.remove(); } catch (err) {} + }); + // Nettoyer la liste + for (let i = pinnedPopups.length - 1; i >= 0; i--) { + if (!document.body.contains(pinnedPopups[i].el)) { + pinnedPopups.splice(i, 1); + } } + _ensureDockCloseAllBtn(); + }, ESC_HOLD_MS); + }); + + document.addEventListener("keyup", (e) => { + if (e.key !== "Escape") return; + if (_escHoldTimer) { + clearTimeout(_escHoldTimer); + _escHoldTimer = null; + } + if (_escHoldTriggered) { + // On a déjà fait l'action "maintenu", ne rien faire de plus + _escHoldTriggered = false; + return; + } + // Appui court : fermer le popup sous la souris si applicable + hideUserNamePopup(); + const tip = tooltipEl(); + if (tip && tip.dataset.mode === "anchored" && tip.classList.contains("visible")) { + hideTooltip({ force: true }); + } + // Quel popup est sous la souris ? Utiliser :hover pour détecter + const hovered = document.querySelector(".pinned-popup:hover"); + if (hovered && !hovered.classList.contains("pinned-popup-reduced")) { + // Retirer aussi de pinnedPopups + const idx = pinnedPopups.findIndex(p => p.el === hovered); + if (idx >= 0) pinnedPopups.splice(idx, 1); + hovered.remove(); + _ensureDockCloseAllBtn(); } }); @@ -798,11 +894,37 @@ function initAppFooter() { function initAppClock() { const el = document.getElementById("app-clock"); if (!el) return; + const dateEl = document.getElementById("app-clock-date"); + const timeEl = document.getElementById("app-clock-time"); + + // v2026.5.16 : format "Mardi 21 avril 2026" + const JOURS = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"]; + const MOIS = [ + "janvier", "février", "mars", "avril", "mai", "juin", + "juillet", "août", "septembre", "octobre", "novembre", "décembre" + ]; + + let lastDateStr = ""; const tick = () => { const d = new Date(); const h = String(d.getHours()).padStart(2, "0"); const m = String(d.getMinutes()).padStart(2, "0"); - el.textContent = `${h}:${m}`; + const timeStr = `${h}:${m}`; + if (timeEl) timeEl.textContent = timeStr; + else el.textContent = timeStr; // fallback si ancien markup + + // Date complète : actualisée seulement si elle a changé (évite reflow inutile) + if (dateEl) { + const jour = JOURS[d.getDay()]; + const num = d.getDate(); + const mois = MOIS[d.getMonth()]; + const annee = d.getFullYear(); + const dateStr = `${jour} ${num} ${mois} ${annee}`; + if (dateStr !== lastDateStr) { + dateEl.textContent = dateStr; + lastDateStr = dateStr; + } + } // v5.0.0 : profite du tick pour mettre à jour la ligne rouge "now" updateNowLine(); };