// background.js — Service worker (Manifest V3) — v4 // // Rôles : // 1. Au clic sur l'icône : ouvrir le viewer // 2. Répondre aux messages du viewer : // - getSession : trouve l'onglet EasyVista ouvert, renvoie {phpsessid, origin} // - fetchPlanning : fetch le XML du planning pour une date (1 requête = tout) // - fetchXhr2 : fetch un texte d'action détaillé (utilisé en lazy-load au survol) // - fetchFiche : fetch une fiche individuelle (HTML) pour statut + commentaire tech // 3. Programmer les alarmes de refresh auto (12h, 15h) // 4. Nettoyer les vieux caches (>7 jours) // // v4 : suppression de fetchTimeline (pu utilisé). Le calendar_block contient // directement ref/contact/lieu/catégorie dans ses attributs attr1/attr2/attr3, // donc on n'a plus besoin ni de xhr2 en masse, ni de l'API timeline. // Domaines EasyVista reconnus (interne d'abord, externe en fallback) const EV_ORIGINS = [ "https://itsma.etat-de-vaud.ch", "https://itsma.vd.ch" ]; // ============================================================================ // Clic sur l'icône → ouvrir le viewer // ============================================================================ chrome.action.onClicked.addListener(async () => { const viewerUrl = chrome.runtime.getURL("viewer.html"); // Si le viewer est déjà ouvert, on focus cet onglet plutôt que d'en ouvrir un autre const existing = await chrome.tabs.query({ url: viewerUrl + "*" }); if (existing.length > 0) { await chrome.tabs.update(existing[0].id, { active: true }); await chrome.windows.update(existing[0].windowId, { focused: true }); } else { await chrome.tabs.create({ url: viewerUrl }); } }); // ============================================================================ // Trouver l'onglet EasyVista actif et en extraire le PHPSESSID // ============================================================================ async function findEasyVistaSession() { // Chercher tous les onglets sur un domaine EasyVista for (const origin of EV_ORIGINS) { const tabs = await chrome.tabs.query({ url: origin + "/*" }); for (const tab of tabs) { const m = (tab.url || "").match(/[?&]PHPSESSID=([a-zA-Z0-9]+)/); if (m) { return { phpsessid: m[1], origin: origin, tabId: tab.id }; } } } return null; } // ============================================================================ // Fetch helpers (s'exécutent dans le contexte du service worker, // les cookies du domaine sont automatiquement inclus via credentials: include) // ============================================================================ /** * Fetch du XML retourné par planning_xhr.php?div=calendar_block. * Contient les interventions de nos 8 techs pour la date donnée (~40 ko). * * Ce n'est PAS le HTML de la page Planning — le serveur ne rend pas les données * dans le HTML, elles arrivent via cet endpoint AJAX. */ async function fetchPlanningXml(origin, phpsessid, unixDate) { const techIds = "76272,83725,66635,92235,90070,40944,72485,86874"; const groupId = "191"; const url = `${origin}/planning_xhr.php` + `?PHPSESSID=${encodeURIComponent(phpsessid)}` + `&div=calendar_block` + `&mode=day` + `&group_id=${groupId}` + `&event_name=HelpDesk_PlanningItem` + `&sql_param=${techIds}` + `&unix_date=${unixDate}` + `&start_date_label=Date` + `&end_date_label=Date` + `&click_here_label=Ici` + `&mail_title=mail` + `&day_start_hour=8` + `&day_end_hour=19`; console.log("[bg] fetchPlanningXml →", url.substring(0, 140)); const r = await fetch(url, { credentials: "include" }); console.log("[bg] status =", r.status); if (!r.ok) throw new Error("HTTP " + r.status); const xml = await r.text(); console.log("[bg] taille XML =", xml.length); return xml; } /** * Fetch planning_xhr_2.php?id=ACTIONID pour UNE intervention. * Retourne ~400 octets au format custom : * @@DESCRIPTION_S@@...@@DESCRIPTION_E@@@@LABEL_S@@... */ async function fetchXhr2(origin, phpsessid, actionId) { const url = `${origin}/planning_xhr_2.php?PHPSESSID=${encodeURIComponent(phpsessid)}&id=${encodeURIComponent(actionId)}`; const r = await fetch(url, { credentials: "include" }); if (!r.ok) throw new Error("HTTP " + r.status); return await r.text(); } 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 fetch(url, { credentials: "include" }); if (!r.ok) throw new Error("HTTP " + r.status); const html = await r.text(); console.log("[bg] fiche status =", r.status, "| taille =", html.length); return html; } // v4.1.7 : API timeline EasyVista. Renvoie le JSON des actions d'une fiche, // avec pour chaque action : intervenant, ACTION_ID, AM_DONE_BY_ID, description // complète (bien plus riche que le xhr2 tronqué). // Utilisé pour afficher le texte complet de l'action dans le tooltip. // v4.1.9 : le GUID du form est passé en paramètre (extrait dynamiquement du // HTML de la fiche par le viewer). Il est différent pour une demande S... // ({C99ECD05}) vs un incident I... ({07ED9C68}). async function fetchTimelineApi(origin, phpsessid, guid, formId, formChecksum) { // Sécurité : GUID doit être de la forme %7B...%7D ou {...} if (!/^(%7B|\{)[A-F0-9\-]{36}(%7D|\})$/i.test(guid)) { throw new Error("Invalid GUID: " + guid); } // S'assurer qu'on a la forme encodée %7B...%7D const encodedGuid = guid.startsWith("%7B") ? guid : `%7B${guid.replace(/[{}]/g, "")}%7D`; const url = `${origin}/api/v1/internal/forms/${encodedGuid}/timeline` + `?target=${encodeURIComponent(formId)}` + `&checksum=${encodeURIComponent(formChecksum)}` + `&type=todo§ionId=1&navigator=&nbRecord=0` + `&PHPSESSID=${encodeURIComponent(phpsessid)}`; const r = await fetch(url, { credentials: "include" }); if (!r.ok) throw new Error("HTTP " + r.status); return await r.text(); } // ============================================================================ // Détection "session invalide" // ============================================================================ function looksLikeLoginPage(text) { // La page de login EasyVista contient cette chaîne return /customer_login|my\.policy/i.test((text || "").substring(0, 3000)); } // ============================================================================ // Messages du viewer // ============================================================================ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { (async () => { try { if (msg.type === "getSession") { const session = await findEasyVistaSession(); sendResponse({ ok: true, session }); return; } if (msg.type === "fetchPlanning") { const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } // Fetch XML calendar_block du planning (rapide ~40 ko) const xml = await fetchPlanningXml(session.origin, session.phpsessid, msg.unixDate); if (looksLikeLoginPage(xml)) { sendResponse({ ok: false, error: "session_expired" }); return; } sendResponse({ ok: true, xml, session }); return; } if (msg.type === "fetchXhr2") { const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } try { const body = await fetchXhr2(session.origin, session.phpsessid, msg.actionId); sendResponse({ ok: true, body }); } catch (err) { sendResponse({ ok: false, error: String(err) }); } return; } if (msg.type === "fetchFiche") { const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } const html = await fetchFicheHtml(session.origin, session.phpsessid, msg.formLink); if (looksLikeLoginPage(html)) { sendResponse({ ok: false, error: "session_expired" }); return; } sendResponse({ ok: true, html, session }); return; } if (msg.type === "fetchTimelineApi") { const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } try { const body = await fetchTimelineApi( session.origin, session.phpsessid, msg.guid, msg.formId, msg.formChecksum ); if (looksLikeLoginPage(body)) { sendResponse({ ok: false, error: "session_expired" }); return; } sendResponse({ ok: true, body }); } catch (err) { sendResponse({ ok: false, error: String(err) }); } return; } if (msg.type === "scheduleAutoRefresh") { scheduleAutoRefreshAlarms(); sendResponse({ ok: true }); return; } if (msg.type === "cleanupOldCaches") { const removed = await cleanupOldCaches(msg.daysToKeep || 7); sendResponse({ ok: true, removed }); return; } sendResponse({ ok: false, error: "unknown_message" }); } catch (err) { console.error("background error:", err); sendResponse({ ok: false, error: err.message || String(err) }); } })(); // Retourner true pour garder sendResponse asynchrone return true; }); // ============================================================================ // Alarmes : refresh auto 12h / 15h // ============================================================================ function scheduleAutoRefreshAlarms() { // Calculer le prochain 12h et 15h à partir de maintenant const now = new Date(); function nextAt(hour, minute) { const d = new Date(); d.setHours(hour, minute, 0, 0); if (d <= now) d.setDate(d.getDate() + 1); return d.getTime(); } chrome.alarms.create("refresh_12h", { when: nextAt(12, 0), periodInMinutes: 24 * 60 // tous les jours }); chrome.alarms.create("refresh_15h", { when: nextAt(15, 0), periodInMinutes: 24 * 60 }); } chrome.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name === "refresh_12h" || alarm.name === "refresh_15h") { // Envoyer un message à tous les viewers ouverts pour qu'ils se rafraîchissent const viewerUrl = chrome.runtime.getURL("viewer.html"); const tabs = await chrome.tabs.query({ url: viewerUrl + "*" }); for (const tab of tabs) { try { await chrome.tabs.sendMessage(tab.id, { type: "autoRefresh" }); } catch { // Onglet fermé ou pas réactif, on ignore } } } }); // ============================================================================ // Nettoyage caches > 7 jours // ============================================================================ async function cleanupOldCaches(daysToKeep) { const all = await chrome.storage.local.get(null); const threshold = new Date(); threshold.setDate(threshold.getDate() - daysToKeep); const thresholdStr = threshold.toISOString().substring(0, 10); // YYYY-MM-DD const toRemove = []; for (const key of Object.keys(all)) { // Nos clés de cache sont planning_cache_YYYY-MM-DD const m = key.match(/^planning_cache_(\d{4}-\d{2}-\d{2})$/); if (m && m[1] < thresholdStr) { toRemove.push(key); } } if (toRemove.length > 0) { await chrome.storage.local.remove(toRemove); } return toRemove.length; } // Au démarrage, programmer les alarmes et nettoyer chrome.runtime.onInstalled.addListener(() => { scheduleAutoRefreshAlarms(); cleanupOldCaches(7).catch(err => console.warn("cleanup:", err)); }); chrome.runtime.onStartup.addListener(() => { scheduleAutoRefreshAlarms(); cleanupOldCaches(7).catch(err => console.warn("cleanup:", err)); });