diff --git a/background.js b/background.js index 0025a54..fa61c7d 100644 --- a/background.js +++ b/background.js @@ -115,6 +115,31 @@ async function fetchFicheHtml(origin, phpsessid, formLink) { 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" // ============================================================================ @@ -183,6 +208,28 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { 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 }); diff --git a/manifest.json b/manifest.json index 3027d2f..7b97333 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, "name": "Planning Techniciens — Vue claire", - "version": "4.1.6", - "description": "Vue claire du planning EasyVista (itsma.etat-de-vaud.ch et itsma.vd.ch). v4.1.6 : parsing fiche corrigé pour trouver l'Intervenant via ng-model=colData.value et ignorer les blocs d'action vides (étapes workflow automatiques).", + "version": "4.1.14", + "description": "Vue claire du planning EasyVista (itsma.etat-de-vaud.ch et itsma.vd.ch). v4.1.14 : bouton ↻ dans la bulle pour recharger une seule intervention (sans que les boutons topbar tournent), Actualiser tourne quand on arrive sur une date avec cache, signature planif vraiment à droite, progress bar lisible avec halo text-shadow (plus de fond noir).", "permissions": [ "activeTab", "scripting", diff --git a/viewer.css b/viewer.css index 30da2ea..41ede86 100644 --- a/viewer.css +++ b/viewer.css @@ -171,6 +171,106 @@ html, body { flex-shrink: 0; } +/* Bannière de session expirée (v4.1.12) — sticky sous la topbar, non bloquante */ +.session-banner { + position: sticky; + top: 56px; + z-index: 8; + display: flex; + align-items: center; + gap: 12px; + padding: 10px 16px; + background: linear-gradient(90deg, #7a1f1f, #8b2a2a); + color: #fff; + border-bottom: 1px solid #5a1515; + font-size: 13px; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); +} +.session-banner.hidden { + display: none; +} +.session-banner-icon { + font-size: 18px; + flex-shrink: 0; +} +.session-banner-text { + flex: 1; + line-height: 1.4; +} +.session-banner .btn-primary { + background: #fff; + color: #7a1f1f; + border: 0; + font-weight: 600; +} +.session-banner .btn-primary:hover { + background: #f0f0f0; +} +.session-banner .btn-sm { + padding: 4px 12px; + font-size: 12px; +} +.session-banner .btn-icon { + background: transparent; + color: #fff; + border: 0; + font-size: 20px; + line-height: 1; + padding: 4px 8px; +} +.session-banner .btn-icon:hover { + background: rgba(255,255,255,0.15); +} + +/* Barre de progression pendant le rafraîchissement — v4.1.12 : texte + toujours lisible, que la zone verte l'ait atteint ou non (utilise + mix-blend-mode:difference pour inverser la couleur du texte là où la + barre verte est dessous). */ +.progress-bar { + position: sticky; + top: 56px; + z-index: 9; + height: 22px; + background: var(--bg-subtle, rgba(128, 128, 128, 0.08)); + border-bottom: 1px solid var(--border, rgba(128, 128, 128, 0.2)); + overflow: hidden; +} +.progress-bar.hidden { + display: none; +} +.progress-bar-fill { + position: absolute; + left: 0; + top: 0; + bottom: 0; + background: linear-gradient(90deg, #2ea043, #3fb950); + width: 0%; + transition: width 240ms ease-out; + box-shadow: 0 0 8px rgba(63, 185, 80, 0.3); +} +.progress-bar-label { + position: relative; + display: block; + text-align: center; + line-height: 22px; + font-size: 12px; + font-weight: 700; + color: #fff; + pointer-events: none; + letter-spacing: 0.3px; + /* v4.1.14 : text-shadow multi-directionnel qui crée un halo sombre autour + du texte. Lisible peu importe ce qui défile derrière (noms, icônes, + fond gris ou barre verte). Aucun fond opaque → la transparence de la + barre est totalement préservée. */ + text-shadow: + 0 0 2px rgba(0, 0, 0, 0.95), + 0 0 3px rgba(0, 0, 0, 0.85), + 0 1px 2px rgba(0, 0, 0, 0.75), + 0 -1px 2px rgba(0, 0, 0, 0.75), + 1px 0 2px rgba(0, 0, 0, 0.75), + -1px 0 2px rgba(0, 0, 0, 0.75); +} + /* Navigation de date */ .date-nav { display: flex; @@ -249,6 +349,41 @@ html, body { opacity: 1; } +/* v4.1.12 : boutons refresh plus clairs visuellement. + - "Vérifier" (partiel) : style discret, icône demi-rotation + - "Tout recharger" (total) : plus affirmé, icône double-flèche circulaire */ +.btn-refresh { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 10px; +} +.btn-refresh-icon { + width: 15px; + height: 15px; + flex-shrink: 0; + color: currentColor; +} +.btn-refresh-label { + font-size: 12px; + line-height: 1; +} +.btn-refresh-strong { + background: var(--bg-subtle, rgba(63, 185, 80, 0.08)); + border-color: var(--border-strong); +} +.btn-refresh-strong:hover { + background: rgba(63, 185, 80, 0.18); + border-color: rgba(63, 185, 80, 0.5); +} +.btn-refresh-icon.spinning { + animation: refresh-spin 0.9s linear infinite; + transform-origin: 50% 50%; +} +@keyframes refresh-spin { + to { transform: rotate(360deg); } +} + .btn-primary { background: var(--accent); color: white; @@ -840,6 +975,8 @@ html, body { align-items: baseline; gap: 8px; font-size: 12px; + /* v4.1.14 : forcer la ligne à occuper 100% de largeur du parent */ + width: 100%; } .iv-category { color: var(--text-muted); @@ -847,7 +984,10 @@ html, body { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - flex: 1; + /* v4.1.14 : flex: 1 pour prendre tout l'espace disponible entre category + et signature — pousse la signature au bord droit. min-width: 0 permet + l'ellipsis sur les longues catégories. */ + flex: 1 1 auto; min-width: 0; } .iv-signature { @@ -856,6 +996,9 @@ html, body { font-family: var(--mono); flex-shrink: 0; letter-spacing: 0.02em; + /* v4.1.14 : collée au bord droit, pas de padding-right parasite */ + text-align: right; + margin-left: auto; } /* Réservation (créneau bloqué par un coordinateur) */ @@ -918,9 +1061,73 @@ html, body { pointer-events: none; opacity: 0; transition: opacity 0.1s; + /* v4.1.10 : empêcher la sélection par défaut (évite sélection accidentelle + pendant qu'on bouge la souris). Ré-activé quand .pinned. */ + user-select: none; } .tooltip.visible { opacity: 1; + /* v4.1.10 : permet à la souris d'entrer dans la bulle pour la garder + visible (persistance au hover) et, en mode pinned, pour sélectionner. */ + pointer-events: auto; +} +.tooltip.pinned { + /* v4.1.10 : bulle épinglée → curseur texte + sélection active */ + user-select: text; + cursor: text; + border-color: var(--c-accent, #3fb950); + box-shadow: 0 0 0 2px rgba(63, 185, 80, 0.15), var(--shadow-hover); +} + +/* v4.1.13/14 : barre d'actions en haut à droite de la bulle + (recharger cette iv + épingler) */ +.tooltip-actions { + position: absolute; + top: 6px; + right: 6px; + display: flex; + gap: 2px; + z-index: 5; +} +.tooltip-actionbtn { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 4px; + font-size: 13px; + opacity: 0.55; + transition: opacity 0.15s, background 0.15s, transform 0.15s; + user-select: none; + color: var(--text-muted); +} +.tooltip-actionbtn svg { + width: 14px; + height: 14px; +} +.tooltip-actionbtn:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.08); + color: var(--text); +} +.tooltip-actionbtn.spinning svg { + animation: refresh-spin 0.8s linear infinite; + transform-origin: 50% 50%; +} +/* L'ancien .tooltip-pinbtn garde ses variantes */ +.tooltip-pinbtn { + filter: grayscale(100%); +} +.tooltip-pinbtn:hover { + filter: grayscale(0%); +} +.tooltip.pinned .tooltip-pinbtn { + opacity: 1; + filter: grayscale(0%); + transform: rotate(-30deg); + background: rgba(63, 185, 80, 0.15); } .tooltip dl { diff --git a/viewer.html b/viewer.html index 5fec1f3..e29ae22 100644 --- a/viewer.html +++ b/viewer.html @@ -19,8 +19,13 @@
- +
+ + + + + +