diff --git a/background.js b/background.js index dc2b16a..1796ab7 100644 --- a/background.js +++ b/background.js @@ -157,19 +157,44 @@ 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); - if (html.length < 500) { - console.warn("[bg] ⚠ fiche très courte, contenu =", JSON.stringify(html)); - } - return html; + // Ne devrait pas arriver (la boucle fait return avant) + throw new Error("fetchFicheHtml: max retries reached"); } // v4.1.7 : API timeline EasyVista. Renvoie le JSON des actions d'une fiche, @@ -375,6 +400,67 @@ function originForContext(context) { : "https://itsma.vd.ch"; } +/** + * v2026.5.16 : surveille un onglet ouvert pour détecter si le Windows SSO + * a échoué et rediriger vers la bonne page. + * + * Quand la session portail Canton est expirée, EasyVista redirige vers + * https://portail.etat-de-vaud.ch/iamlogin/?spEntityID=... + * (page de login manuel moche). On préfère rediriger vers + * https://portail.etat-de-vaud.ch/iam/accueil/ + * qui déclenche le Windows Kerberos SSO automatique. + * + * @param {number} tabId - ID de l'onglet à surveiller + */ +function watchReconnectTabForIamLogin(tabId) { + let redirected = false; + const timeoutMs = 60000; // surveille max 60s + + const listener = (updatedTabId, changeInfo, tab) => { + if (updatedTabId !== tabId) return; + if (redirected) return; + const url = changeInfo.url || (tab && tab.url) || ""; + if (!url) return; + + // Détecter la page de login manuel + // Patterns : portail.etat-de-vaud.ch/iamlogin/ ou www.portail.vd.ch/iamlogin/ + if (/\/iamlogin\//i.test(url) && /portail\./i.test(url)) { + redirected = true; + // Choisir le domaine de redirection : + // - si on voit portail.etat-de-vaud.ch → rester sur interne + // - si on voit www.portail.vd.ch → rester sur externe + let targetUrl; + if (/portail\.etat-de-vaud\.ch/i.test(url)) { + targetUrl = "https://portail.etat-de-vaud.ch/iam/accueil/"; + } else { + targetUrl = "https://www.portail.vd.ch/iam/accueil/"; + } + console.log(`[bg] watchReconnectTab : iamlogin détecté, redirection vers ${targetUrl}`); + chrome.tabs.update(tabId, { url: targetUrl }).catch(e => { + console.warn("[bg] watchReconnectTab : update failed", e); + }); + } + }; + + chrome.tabs.onUpdated.addListener(listener); + + // Stop la surveillance après 60s pour ne pas accumuler des listeners morts + setTimeout(() => { + try { + chrome.tabs.onUpdated.removeListener(listener); + } catch (e) {} + }, timeoutMs); + + // Si l'onglet est fermé, stop aussi + const closeListener = (closedTabId) => { + if (closedTabId === tabId) { + try { chrome.tabs.onUpdated.removeListener(listener); } catch (e) {} + try { chrome.tabs.onRemoved.removeListener(closeListener); } catch (e) {} + } + }; + chrome.tabs.onRemoved.addListener(closeListener); +} + // ============================================================================ // v4.2 : récupération de l'utilisateur connecté // ============================================================================ @@ -1098,6 +1184,11 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { url: `${origin}/`, // racine → EV redirige vers SSO si besoin active: true }); + // v2026.5.16 : surveiller cet onglet — si on tombe sur la page de + // login manuel portail.etat-de-vaud.ch/iamlogin/, rediriger vers + // portail.etat-de-vaud.ch/iam/accueil/ qui déclenche le Windows + // SSO Kerberos automatiquement. + watchReconnectTabForIamLogin(tab.id); sendResponse({ ok: true, tabId: tab.id, origin }); } catch (err) { sendResponse({ ok: false, error: err.message || String(err) }); diff --git a/manifest.json b/manifest.json index 1328d98..21663d0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Planification", - "version": "5.0.15", + "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.", "permissions": ["activeTab", "scripting", "storage", "tabs", "alarms"], "host_permissions": [ diff --git a/viewer.css b/viewer.css index bc51b2f..0f2adb2 100644 --- a/viewer.css +++ b/viewer.css @@ -320,6 +320,19 @@ html, body { display: flex; align-items: center; gap: 4px; + flex-wrap: nowrap; +} + +/* v2026.5.16 : nom court du jour (Mardi, Lundi, ...) à gauche du date-picker */ +.date-picker-day { + font-size: 13px; + font-weight: 500; + color: var(--text-muted); + padding: 0 6px 0 2px; + min-width: 58px; + text-align: right; + white-space: nowrap; + user-select: none; } .btn-nav { @@ -1937,18 +1950,36 @@ body.modal-open { /* ───────────────────────────────────────────────────────────────────────── v5.0.0 : horloge au milieu de la topbar (HH:MM, pas de secondes) ───────────────────────────────────────────────────────────────────────── */ +/* v2026.5.16 : app-clock contient maintenant 2 lignes empilées : + - app-clock-date : "Mardi 21 avril 2026" (petit) + - app-clock-time : "12:34" (grand) */ .app-clock { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + line-height: 1.1; + color: var(--text); + pointer-events: none; + user-select: none; + white-space: nowrap; +} +.app-clock-date { + font-size: 12px; + font-weight: 500; + color: var(--text-muted); + letter-spacing: 0.3px; + text-transform: capitalize; +} +.app-clock-time { font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; - color: var(--text); letter-spacing: 1px; - pointer-events: none; - user-select: none; } .topbar { position: sticky; /* déja défini plus haut */ } /* topbar doit être en position: relative parent pour que .app-clock absolute @@ -2407,3 +2438,63 @@ header.topbar::before { .banner-reconnect-failed .banner-btn-primary:hover { background: #f8d7da; } + +/* ========================================================================== + v2026.5.16 : responsive topbar + ========================================================================== */ + +/* Breakpoint medium : entre 1000 et 1300px, on compacte un peu */ +@media (max-width: 1300px) { + .app-clock-date { font-size: 11px; } + .app-clock-time { font-size: 20px; } + .topbar-right .btn-action .btn-action-label, + .topbar-right .btn-refresh .btn-refresh-label { + font-size: 12px; + } +} + +/* Breakpoint small : moins de 1000px, on masque les labels de boutons action + et on réduit encore l'horloge. Les icônes restent, titres restent. */ +@media (max-width: 1000px) { + .topbar { padding: 8px 14px; gap: 8px; } + .topbar h1 { font-size: 16px; } + .app-clock { font-size: smaller; } + .app-clock-date { font-size: 10px; } + .app-clock-time { font-size: 18px; } + .btn-action .btn-action-label, + .btn-refresh .btn-refresh-label { + display: none; + } + .btn-action, .btn-refresh { + padding: 6px 10px; + } + .capture-info { display: none; } +} + +/* Breakpoint très petit : moins de 720px, on cache la date complète (garde + juste l'heure) et on autorise le wrap total */ +@media (max-width: 720px) { + .topbar { + flex-wrap: wrap; + padding: 6px 10px; + } + .app-clock { + position: static; + transform: none; + margin: 0 auto; + } + .app-clock-date { display: none; } + .topbar-left { flex-wrap: wrap; } + .date-nav { margin-top: 4px; } + .date-picker-day { min-width: 46px; font-size: 12px; } + .topbar-right { flex-wrap: wrap; justify-content: flex-end; } +} + +/* Breakpoint minuscule : masque aussi les labels de refresh, boutons deviennent + vraiment iconifiés */ +@media (max-width: 520px) { + .app-clock-time { font-size: 16px; } + .topbar h1 { font-size: 14px; } + .btn-today { padding: 4px 6px; font-size: 11px; } + .btn-nav { min-width: 26px; padding: 4px 6px; } +} diff --git a/viewer.html b/viewer.html index 968f53e..4865569 100644 --- a/viewer.html +++ b/viewer.html @@ -16,6 +16,8 @@