forked from FroSteel/Planification
Compare commits
1 Commits
c65e943dac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 06c0195130 |
@@ -9,6 +9,55 @@
|
||||
|
||||
---
|
||||
|
||||
## v2026.5.45 — Dock latéral drag&drop, fix verdicts ghost, multi-onglets EZV résolu
|
||||
|
||||
> Refonte de l'expérience drag&drop avec dock latéral pour parquer des interventions entre les jours, fix critique du parser de fiches qui marquait à tort des interventions terminées comme annulées, résolution des 6 issues ouvertes (multi-onglets EZV, absences récurrentes, popups épinglés, pompier absent), et nombreux ajustements d'ergonomie.
|
||||
|
||||
### Issues résolues
|
||||
|
||||
- **#3** — Coches « Absences récurrentes » : merge propre avec l'état stocké au lieu d'écrasement, les coches sont retenues lors d'un changement de groupe ou d'une réouverture des paramètres.
|
||||
- **#4** — Perte de session EZV multi-onglets : permission optionnelle `cookies` + listener `cookieChanged` côté background, toggle dans Paramètres → Diagnostics. La session reste valable même après reconnexion et fermeture d'un onglet EZV.
|
||||
- **#5** — Bouton de copie de référence dans une popup épinglée : handler `copy-ref` ajouté.
|
||||
- **#6** — Popup épinglée au premier plan : clic sur une popup recalcule le z-index pour la passer au-dessus des autres.
|
||||
- **#7** — Notification « +2 min » fantôme : reset des flags d'alerte slide au retour de la prolongation.
|
||||
- **#8** — Compteur pompier : un pompier absent toute la journée est exclu du compteur.
|
||||
|
||||
### Dock latéral drag&drop
|
||||
|
||||
Le dock à droite permet de mettre des interventions de côté pendant qu'on navigue entre les jours, puis de les redéposer plus tard.
|
||||
|
||||
- Apparition graduelle pendant un drag : peep-min (sans contenu), peep (avec cartes), expanded (au survol ou au bord droit).
|
||||
- **Délai 500 ms** pour expand/collapse pour éviter le flicker quand le curseur effleure le bord du dock.
|
||||
- Card du dock : référence + durée prévue (`1h`, `1h30`, `45min`) avec barre verticale 4 px sur la gauche dans la couleur de la catégorie. Fond transparent.
|
||||
- Bouton de retrait `×` : **appui long 2 s** avec animation `conic-gradient`. Un clic simple ne fait rien.
|
||||
- Drag depuis le dock : retrait différé à l'activation effective du drag (5 px). Ghost flottant sans heure à gauche. Drop sur tech → modal de confirmation. Drop hors zone / Échap / sur le même slot d'origine → restauration dans le dock.
|
||||
- Plus de scrollbar horizontale en bas du dock, plus de clic-through dans la zone autour du bouton « Tout annuler ».
|
||||
|
||||
### Verdicts ghost — fix critique
|
||||
|
||||
Une intervention terminée par le tech mais dont la fiche était passée en statut « Redirigé » / « Finalisation » / « Exécution » était à tort marquée comme « annulée » et retirée du planning, parce que le parser de fiche cherchait les dates d'action aux indices `[8]`/`[9]` du tableau `rows` alors que le layout EV récent les place en `[6]`/`[7]` (et la description en `[9]` au lieu de `[11]`).
|
||||
|
||||
- **Détection robuste** : scan des valeurs pour le pattern `DD/MM/YYYY HH:MM:SS`, garde des 2 dernières occurrences comme (création, fin). Survit aux variations de layout EV.
|
||||
- **Décorrélation logs / KEEP-forcé** : nouvelle case dédiée « Garder les disparitions » dans Paramètres → Diagnostics, indépendante des « Logs verbeux ». Mode prod par défaut → verdict `REMOVE` appliqué (statut Annulé/Supprimé, ou statut clos sans commentaire du tech, ou action sans commentaire `login:`).
|
||||
|
||||
### Drag&drop bloqué pour les interventions non-déplaçables
|
||||
|
||||
`_canRescheduleIv` refuse maintenant explicitement le drag pour :
|
||||
|
||||
- iv en verdict `terminated-pending` (gris « fait »).
|
||||
- iv en verdict `terminated-clos` (vert ✓✓).
|
||||
- iv dont le statut EV est dans `CLOSED_STATUS` ou `RESOLVED_STATUS`.
|
||||
- iv en cours d'analyse de disparition (`_disappearChecking`).
|
||||
|
||||
### Popups dépinglés
|
||||
|
||||
- Auto-fermeture du popup dépinglé quand la souris quitte sa zone et celle des cards liées de la même iv (300 ms de grâce). N'interfère pas avec un drag de planning en cours.
|
||||
|
||||
### Tooltip / contact
|
||||
|
||||
- Le contact n'inclut plus les labels « fiche » (`Étage`, `Bureau`, `Service`, `Matériel`, `Problème`, `TFS`, `Date`, `Heure`, `Lieu`, `Bénéficiaire`, `Nom utilisateur`) qui se collent parfois à la valeur du contact à cause de séparateurs perdus dans la source EV. Le tooltip insère un saut de ligne avant chaque label collé pour la lisibilité.
|
||||
|
||||
|
||||
## v2026.5.44 — Refonte topbar, personnalisation Apparence, onboarding équipe, refresh séquentiel
|
||||
|
||||
> Refonte visuelle de la topbar (vue classique + horizontale), nouveau panneau
|
||||
|
||||
@@ -9,7 +9,7 @@ Extension Chrome / Firefox pour visualiser de manière claire et rapide le plann
|
||||
- **Auteur** : Quentin Rouiller (QRO), Technicien DGNSI — Canton de Vaud
|
||||
- **Public cible** : coordinateurs DGNSI qui pilotent dans EasyVista (`itsma.etat-de-vaud.ch` / `itsma.vd.ch`) le planning de l'équipe technicienne
|
||||
- **Démarrage projet** : jeudi 16 avril 2026
|
||||
- **Version actuelle** : [`v2026.5.44`](https://gitea.netaplaid.ch/FroSteel/Planification/releases/tag/v2026.5.44) (latest)
|
||||
- **Version actuelle** : [`v2026.5.45`](https://gitea.netaplaid.ch/FroSteel/Planification/releases/tag/v2026.5.45) (latest)
|
||||
- **Contact** : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact) ou [ouvrir une issue](https://gitea.netaplaid.ch/FroSteel/Planification/issues/new)
|
||||
- **Manifest** : V3 (Chrome/Edge/Firefox 140+)
|
||||
- **Format** : `.zip` (Chromium) + `.xpi` signé Mozilla (Firefox)
|
||||
@@ -90,7 +90,7 @@ L'extension a connu **3 systèmes de versionning successifs** :
|
||||
|---|---|---|
|
||||
| 16-17 avril 2026 | Versions de base | `1.0.0`, `2.0.0`, `3.0.0` |
|
||||
| 18-20 avril 2026 | SemVer classique | `4.1.3`, `4.2.8`, `5.0.12` |
|
||||
| 21 avril 2026 → maintenant | **`ANNÉE.MAJEURE.PATCH`** | `2026.5.16` → `2026.5.44` |
|
||||
| 21 avril 2026 → maintenant | **`ANNÉE.MAJEURE.PATCH`** | `2026.5.16` → `2026.5.45` |
|
||||
|
||||
### Format actuel : `ANNÉE.MAJEURE.PATCH`
|
||||
|
||||
@@ -113,7 +113,20 @@ Le numéro de **majeure** n'est **pas** un mois et **pas** un chiffre lié au ca
|
||||
|
||||
## Versions notables
|
||||
|
||||
### `v2026.5.44` (latest, 1 mai 2026) — Refonte topbar, personnalisation Apparence, onboarding équipe, fix #1
|
||||
### `v2026.5.45` (latest, 8 mai 2026) — Dock latéral drag&drop, fix verdicts ghost, multi-onglets EZV résolu
|
||||
- **Dock latéral drag&drop** : nouvelle zone à droite pour parquer des interventions et les redéposer plus tard, avec apparition graduelle (peep-min/peep/expanded), délai 500 ms pour expand/collapse anti-flicker, card compacte (réf + durée + barre catégorie 4 px à gauche), bouton de retrait par appui long 2 s.
|
||||
- **Verdicts ghost** : fix critique du parser de fiche (les dates d’action ne sont plus cherchées aux indices fixes \`[8]\`/\`[9]\` mais détectées par scan regex pattern \`DD/MM/YYYY HH:MM:SS\`). Décorrélation des « Logs verbeux » et « Garder les disparitions » dans Paramètres → Diagnostics.
|
||||
- **Drag&drop bloqué** sur les interventions « Fait » (gris), « Clos » (vert ✓✓), statut Clôturé/Résolu/Terminé, et pendant l’analyse de disparition d’un ghost.
|
||||
- **Issue #3** : coches « Absences récurrentes » retenues (merge propre au lieu d’écrasement) lors d’un changement de groupe.
|
||||
- **Issue #4** : multi-onglets EZV — permission optionnelle \`cookies\` + listener \`cookieChanged\`, la session reste valable après reconnexion + fermeture d’un onglet EZV.
|
||||
- **Issue #5** : copie de référence dans une popup épinglée fonctionne.
|
||||
- **Issue #6** : popup épinglée passe au premier plan au clic.
|
||||
- **Issue #7** : plus de notification « +2 min » fantôme après prolongation.
|
||||
- **Issue #8** : pompier absent toute la journée n’est plus compté.
|
||||
- **Auto-fermeture** des popups dépinglés quand la souris quitte leur zone et celle des cards liées.
|
||||
- **Tooltip / contact nettoyé** : les labels « fiche » (Étage/Bureau/Service/etc.) collés à la valeur du contact sont retirés.
|
||||
|
||||
### `v2026.5.44` (1 mai 2026) — Refonte topbar, personnalisation Apparence, onboarding équipe, fix #1
|
||||
- **Topbar refondue (vue classique)** : sélecteur de date du planning ancré au centre absolu (ne se décale plus quand le bouton « Arrêter » apparaît), bouton « Aujourd'hui » en toutes lettres, horloge contextuelle dans un cadre encadré.
|
||||
- **Personnalisation Apparence** : couleur de la barre du haut (12 presets + picker custom + champ hex), contraste de texte calculé automatiquement par luminance ; police de l'application avec 28 choix (Arial, Helvetica, Verdana, Tahoma, Trebuchet, Calibri, Segoe UI, Times New Roman, Georgia, Cambria, Garamond, Palatino, Courier, Consolas, Comic Sans, Impact, …) appliquée à toute la page.
|
||||
- **Vue horizontale** : bloc Aujourd'hui + horloge dans le même cadre que la classique, barre verte verticale à droite des mini-cards clos/résolu, sidebar adopte la couleur de topbar custom de manière cohérente.
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
"addons": {
|
||||
"planification-dgnsi@netaplaid.ch": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "2026.5.45",
|
||||
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.45/planification-v2026.5.45-firefox.xpi",
|
||||
"update_hash": "sha256:42ac47aeda23912e80051fc941ba45a0dec74ec4a6c509a25270c27b73dddead"
|
||||
},
|
||||
{
|
||||
"version": "2026.5.44",
|
||||
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.44/planification-v2026.5.44-firefox.xpi",
|
||||
|
||||
+354
-8
@@ -188,6 +188,48 @@ async function getDayBounds() {
|
||||
// Clic sur l'icône → ouvrir le viewer
|
||||
// ============================================================================
|
||||
|
||||
// ============================================================================
|
||||
// badge "!" clignotant sur l'icône de l'extension quand la session
|
||||
// EasyVista va expirer. Permet à l'utilisateur de voir l'alerte même si
|
||||
// l'onglet planning n'est pas au premier plan.
|
||||
// ============================================================================
|
||||
|
||||
let _expirationBlinkInterval = null;
|
||||
let _expirationBlinkTimeout = null;
|
||||
|
||||
function _setExpirationBadge(active, durationMs) {
|
||||
// Stop l'état précédent
|
||||
if (_expirationBlinkInterval) {
|
||||
clearInterval(_expirationBlinkInterval);
|
||||
_expirationBlinkInterval = null;
|
||||
}
|
||||
if (_expirationBlinkTimeout) {
|
||||
clearTimeout(_expirationBlinkTimeout);
|
||||
_expirationBlinkTimeout = null;
|
||||
}
|
||||
try { chrome.action.setBadgeText({ text: "" }); } catch (e) {}
|
||||
|
||||
if (!active) return;
|
||||
|
||||
// Couleur de fond rouge pour le "!"
|
||||
try { chrome.action.setBadgeBackgroundColor({ color: "#d6443d" }); } catch (e) {}
|
||||
let on = true;
|
||||
const tick = () => {
|
||||
try {
|
||||
chrome.action.setBadgeText({ text: on ? "!" : "" });
|
||||
} catch (e) {}
|
||||
on = !on;
|
||||
};
|
||||
tick();
|
||||
_expirationBlinkInterval = setInterval(tick, 700);
|
||||
|
||||
if (durationMs && durationMs > 0) {
|
||||
_expirationBlinkTimeout = setTimeout(() => {
|
||||
_setExpirationBadge(false);
|
||||
}, durationMs);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -207,24 +249,146 @@ chrome.action.onClicked.addListener(async () => {
|
||||
/**
|
||||
* Trouve l'onglet EasyVista ouvert et récupère phpsessid + origin.
|
||||
*
|
||||
* v2026.5.45 (issue #4) : si la permission optionnelle "cookies" est
|
||||
* accordée, on lit le PHPSESSID directement depuis le cookie HttpOnly du
|
||||
* domaine EZV — toujours à jour, immune au PHPSESSID périmé qu'on trouve
|
||||
* dans l'URL des onglets historiques après un relog. Sinon, fallback sur
|
||||
* l'ancienne logique URL (compat sans la permission).
|
||||
*
|
||||
* @author Quentin Rouiller
|
||||
*/
|
||||
async function findEasyVistaSession() {
|
||||
// v2026.5.41 : les origines EV viennent de admin_config (éditables dans
|
||||
// Paramètres → EasyVista), avec fallback sur DEFAULT_EV_ORIGINS.
|
||||
async function _hasCookiesPermission() {
|
||||
return new Promise(resolve => {
|
||||
try {
|
||||
chrome.permissions.contains({ permissions: ["cookies"] }, granted => {
|
||||
resolve(!!granted);
|
||||
});
|
||||
} catch (e) {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function _readPhpsessidFromCookie(origin) {
|
||||
if (!chrome.cookies) return null;
|
||||
return new Promise(resolve => {
|
||||
try {
|
||||
chrome.cookies.get({ url: origin, name: "PHPSESSID" }, c => {
|
||||
if (!c || !c.value) { resolve(null); return; }
|
||||
resolve({
|
||||
value: c.value,
|
||||
expirationDate: c.expirationDate || null
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── findEasyVistaSession ───────────────────────────────────────────────────
|
||||
//
|
||||
// Stratégie unifiée : pour chaque origine EV configurée, on cherche le
|
||||
// PHPSESSID dans cet ordre :
|
||||
// 1. cookie HttpOnly (autoritatif, donne aussi expirationDate)
|
||||
// 2. fallback URL d'un onglet ouvert (?PHPSESSID=…)
|
||||
// On retourne la première origine qui a un onglet ouvert ET un PHPSESSID.
|
||||
//
|
||||
// Anti-rafale : la fonction est appelée par ~16 endroits dans le service
|
||||
// worker, parfois en parallèle. On cache le résultat 200 ms pour éviter
|
||||
// le bombardement de chrome.cookies.get + chrome.tabs.query, et de logs
|
||||
// dupliqués. Le cache est invalidé immédiatement quand un événement
|
||||
// cookies.onChanged tombe (relog, expiration).
|
||||
//
|
||||
// Logs : une seule ligne `LOG.warn` par CHANGEMENT d'état (transition
|
||||
// cookie↔url, nouveau PHPSESSID, ou perte de session). Pas de log
|
||||
// répétitif par appel — utiliser `LOG.info` pour les détails internes.
|
||||
|
||||
let _sessionCache = null;
|
||||
let _sessionCacheTs = 0;
|
||||
// Cache désactivé temporairement (TTL=0) — soupçon que la durée de
|
||||
// fenêtre stale puisse causer une réponse "page de login" (~8 Ko) lors
|
||||
// d'un fetch fiche. À remettre à 200 après confirmation que ce n'était
|
||||
// pas le souci.
|
||||
const _SESSION_CACHE_TTL_MS = 0;
|
||||
let _lastSessionSignature = null;
|
||||
|
||||
function _invalidateSessionCache() {
|
||||
_sessionCache = null;
|
||||
_sessionCacheTs = 0;
|
||||
}
|
||||
|
||||
async function _findEasyVistaSessionRaw() {
|
||||
const origins = await getEvOrigins();
|
||||
const hasCookies = await _hasCookiesPermission();
|
||||
|
||||
for (const origin of 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 };
|
||||
if (!tabs.length) continue;
|
||||
|
||||
let phpsessid = null;
|
||||
let expirationDate = null;
|
||||
let source = null;
|
||||
|
||||
if (hasCookies) {
|
||||
const c = await _readPhpsessidFromCookie(origin);
|
||||
if (c && c.value) {
|
||||
phpsessid = c.value;
|
||||
expirationDate = c.expirationDate;
|
||||
source = "cookie";
|
||||
}
|
||||
}
|
||||
|
||||
if (!phpsessid) {
|
||||
for (const tab of tabs) {
|
||||
const m = (tab.url || "").match(/[?&]PHPSESSID=([a-zA-Z0-9]+)/);
|
||||
if (m) {
|
||||
phpsessid = m[1];
|
||||
source = "url";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (phpsessid) {
|
||||
return { phpsessid, origin, tabId: tabs[0].id, source, expirationDate };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function findEasyVistaSession() {
|
||||
const now = Date.now();
|
||||
if (_sessionCache !== null && now - _sessionCacheTs < _SESSION_CACHE_TTL_MS) {
|
||||
return _sessionCache;
|
||||
}
|
||||
if (_sessionCache === null && _sessionCacheTs > 0 && now - _sessionCacheTs < _SESSION_CACHE_TTL_MS) {
|
||||
// Cache négatif (pas de session trouvée à l'instant) — même TTL.
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await _findEasyVistaSessionRaw();
|
||||
_sessionCache = result;
|
||||
_sessionCacheTs = now;
|
||||
|
||||
// Log uniquement si l'état change (source, origine, ou PHPSESSID).
|
||||
const sig = result
|
||||
? `${result.source}|${result.origin}|${result.phpsessid.slice(0, 8)}`
|
||||
: "none";
|
||||
if (sig !== _lastSessionSignature) {
|
||||
if (result) {
|
||||
LOG.warn("session",
|
||||
`🔑 PHPSESSID via ${result.source.toUpperCase()} (${result.phpsessid.slice(0, 8)}…) sur ${result.origin}`,
|
||||
{ source: result.source, origin: result.origin,
|
||||
expirationDate: result.expirationDate || null });
|
||||
} else {
|
||||
LOG.warn("session", "❌ Aucun PHPSESSID disponible (pas d'onglet EZV ouvert ou cookie absent)");
|
||||
}
|
||||
_lastSessionSignature = sig;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Fetch helpers (s'exécutent dans le contexte du service worker,
|
||||
// les cookies du domaine sont automatiquement inclus via credentials: include)
|
||||
@@ -1263,6 +1427,14 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// badge "!" clignotant sur l'icône de l'extension.
|
||||
// { active: true, durationMs?: number }
|
||||
if (msg.type === "setExpirationBadge") {
|
||||
_setExpirationBadge(!!msg.active, msg.durationMs || 0);
|
||||
sendResponse({ ok: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "fetchPlanning") {
|
||||
const session = await findEasyVistaSession();
|
||||
if (!session) {
|
||||
@@ -1336,6 +1508,88 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
// probe rapide de session — fetch un endpoint léger pour vérifier
|
||||
// que PHPSESSID est toujours valide. Renvoie ok=false/error=session_expired
|
||||
// si la session est morte.
|
||||
// (feature reschedule) : déplace une intervention vers un autre
|
||||
// tech / nouvelle date / nouvel horaire de début. Durée préservée
|
||||
// automatiquement par EZV (l'API ne change que start_date+hour+minute).
|
||||
// Args : actionId, employeeId, date (DD/MM/YYYY), hour (0-23), minute (0-59).
|
||||
if (msg.type === "rescheduleAction") {
|
||||
const session = await findEasyVistaSession();
|
||||
if (!session) { sendResponse({ ok: false, error: "no_session" }); return; }
|
||||
try {
|
||||
const url = `${session.origin}/planning_updator_xhr.php` +
|
||||
`?PHPSESSID=${encodeURIComponent(session.phpsessid)}` +
|
||||
`&function_name=Planning_schedule_action_Employee` +
|
||||
`&action_id=${encodeURIComponent(msg.actionId)}` +
|
||||
`&employee_id=${encodeURIComponent(msg.employeeId)}` +
|
||||
`&date=${encodeURIComponent(msg.date)}` +
|
||||
`&hour=${encodeURIComponent(msg.hour)}` +
|
||||
`&minute=${encodeURIComponent(msg.minute)}` +
|
||||
`&multi_day_mode_act=0`;
|
||||
LOG.warn("reschedule",
|
||||
`📅 reschedule actionId=${msg.actionId} → tech=${msg.employeeId} ${msg.date} ${msg.hour}:${String(msg.minute).padStart(2,"0")}`);
|
||||
const r = await evFetch(url, session.origin);
|
||||
if (!r.ok) {
|
||||
sendResponse({ ok: false, error: classifyHttpStatus(r.status), httpStatus: r.status });
|
||||
return;
|
||||
}
|
||||
const txt = await r.text();
|
||||
if (looksLikeLoginPage(txt)) {
|
||||
sendResponse({ ok: false, error: "session_expired" });
|
||||
return;
|
||||
}
|
||||
sendResponse({ ok: true, response: txt });
|
||||
} catch (err) {
|
||||
LOG.warn("reschedule", "rescheduleAction err", { err: err && err.message });
|
||||
sendResponse({ ok: false, error: err.kind || "fetch_failed", detail: err.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// (feature reschedule) : modifie la durée (heure début/fin) d'une
|
||||
// action sans changer de tech. POST application/x-www-form-urlencoded
|
||||
// sur planning_updator_xhr.php avec function_name=fc_save_inspector.
|
||||
// Args : actionId, suffix (ex "act_<id>_nb_0_date_<DDMMYYYY>"),
|
||||
// startDate, endDate (DD/MM/YYYY), startTime, endTime (HH:MM).
|
||||
if (msg.type === "updateActionTimes") {
|
||||
const session = await findEasyVistaSession();
|
||||
if (!session) { sendResponse({ ok: false, error: "no_session" }); return; }
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.set(`start_date_${msg.suffix}`, msg.startDate);
|
||||
params.set(`start_time_${msg.suffix}`, msg.startTime);
|
||||
params.set(`end_date_${msg.suffix}`, msg.endDate);
|
||||
params.set(`end_time_${msg.suffix}`, msg.endTime);
|
||||
params.set("action_id", msg.actionId);
|
||||
params.set("suffix_act", msg.suffix);
|
||||
params.set(`act_absence_${msg.suffix}`, "");
|
||||
params.set("function_name", "fc_save_inspector");
|
||||
const body = params.toString();
|
||||
const url = `${session.origin}/planning_updator_xhr.php` +
|
||||
`?PHPSESSID=${encodeURIComponent(session.phpsessid)}`;
|
||||
LOG.warn("reschedule",
|
||||
`⏱ updateActionTimes actionId=${msg.actionId} ${msg.startDate} ${msg.startTime}→${msg.endTime}`);
|
||||
const r = await evFetch(url, session.origin, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body
|
||||
});
|
||||
if (!r.ok) {
|
||||
sendResponse({ ok: false, error: classifyHttpStatus(r.status), httpStatus: r.status });
|
||||
return;
|
||||
}
|
||||
const txt = await r.text();
|
||||
if (looksLikeLoginPage(txt)) {
|
||||
sendResponse({ ok: false, error: "session_expired" });
|
||||
return;
|
||||
}
|
||||
sendResponse({ ok: true, response: txt });
|
||||
} catch (err) {
|
||||
LOG.warn("reschedule", "updateActionTimes err", { err: err && err.message });
|
||||
sendResponse({ ok: false, error: err.kind || "fetch_failed", detail: err.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "checkSession") {
|
||||
const session = await findEasyVistaSession();
|
||||
if (!session) {
|
||||
@@ -1655,10 +1909,33 @@ async function _getCacheDays() {
|
||||
}
|
||||
|
||||
// Au démarrage, nettoyer les anciennes alarmes et les anciens caches
|
||||
chrome.runtime.onInstalled.addListener(async () => {
|
||||
// v2026.5.45 (issue #3) : on stocke aussi un marqueur d'install/update
|
||||
// avec previousVersion. Au prochain ouverture du viewer, on affiche un
|
||||
// toast préventif si la version précédente était < 2026.5.44 (où le bug
|
||||
// structurel d'écrasement des absences récurrentes existait encore).
|
||||
chrome.runtime.onInstalled.addListener(async (details) => {
|
||||
clearLegacyRefreshAlarms();
|
||||
const days = await _getCacheDays();
|
||||
cleanupOldCaches(days).catch(err => LOG.warn("cleanup", "échec onInstalled", { err: err && err.message }));
|
||||
try {
|
||||
const reason = details && details.reason;
|
||||
const previousVersion = (details && details.previousVersion) || null;
|
||||
if (reason === "install" || reason === "update") {
|
||||
const currentVersion = (chrome.runtime.getManifest && chrome.runtime.getManifest().version) || null;
|
||||
await chrome.storage.local.set({
|
||||
_lastInstallEvent: {
|
||||
reason,
|
||||
previousVersion,
|
||||
currentVersion,
|
||||
ts: Date.now(),
|
||||
notified: false
|
||||
}
|
||||
});
|
||||
LOG.info("install", "marqueur posé", { reason, previousVersion, currentVersion });
|
||||
}
|
||||
} catch (e) {
|
||||
LOG.warn("install", "écriture _lastInstallEvent échouée", { err: e && e.message });
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onStartup.addListener(async () => {
|
||||
@@ -1666,3 +1943,72 @@ chrome.runtime.onStartup.addListener(async () => {
|
||||
const days = await _getCacheDays();
|
||||
cleanupOldCaches(days).catch(err => LOG.warn("cleanup", "échec onStartup", { err: err && err.message }));
|
||||
});
|
||||
|
||||
// v2026.5.45 (issue #4 — bonus) : listener sur les changements de cookie
|
||||
// EZV. Quand la permission `cookies` est accordée, on s'abonne pour détecter
|
||||
// EN TEMPS RÉEL les changements de PHPSESSID (relog dans un autre onglet,
|
||||
// expiration côté serveur, révocation…). On envoie un message broadcast aux
|
||||
// viewers ouverts pour qu'ils recalibrent leur compteur de session.
|
||||
function _attachCookiesListener() {
|
||||
if (!chrome.cookies || !chrome.cookies.onChanged) return;
|
||||
if (_attachCookiesListener._attached) return;
|
||||
_attachCookiesListener._attached = true;
|
||||
LOG.warn("cookies", "🍪 listener PHPSESSID activé (permission cookies accordée)");
|
||||
chrome.cookies.onChanged.addListener((info) => {
|
||||
try {
|
||||
if (!info || !info.cookie) return;
|
||||
const c = info.cookie;
|
||||
if (c.name !== "PHPSESSID") return;
|
||||
const dom = (c.domain || "").replace(/^\./, "");
|
||||
if (!/itsma\.(etat-de-vaud|vd)\.ch$/.test(dom)) return;
|
||||
const phpsessid8 = c.value ? c.value.slice(0, 8) + "…" : null;
|
||||
// événements importants → LOG.warn (toujours visibles).
|
||||
// Détail (cause, expirationDate, …) → LOG.info (debug only).
|
||||
if (info.removed) {
|
||||
LOG.warn("cookies", `🚫 PHPSESSID supprimé sur ${dom} (cause=${info.cause}) → session morte`);
|
||||
} else {
|
||||
LOG.warn("cookies", `🍪 PHPSESSID ${phpsessid8} sur ${dom} (cause=${info.cause})`);
|
||||
}
|
||||
// Invalide immédiatement le cache findEasyVistaSession() pour que
|
||||
// le prochain appel reflète le nouveau cookie sans attendre la TTL.
|
||||
if (typeof _invalidateSessionCache === "function") _invalidateSessionCache();
|
||||
LOG.info("cookies", " détail onChanged",
|
||||
{ domain: dom, removed: !!info.removed, cause: info.cause,
|
||||
phpsessid8, expirationDate: c.expirationDate || null,
|
||||
exp: c.expirationDate ? new Date(c.expirationDate * 1000).toISOString() : null });
|
||||
try {
|
||||
chrome.runtime.sendMessage({
|
||||
type: "cookieChanged",
|
||||
domain: dom,
|
||||
removed: !!info.removed,
|
||||
cause: info.cause,
|
||||
phpsessid: c.value || null,
|
||||
expirationDate: c.expirationDate || null
|
||||
}).catch(() => { /* viewers fermés → pas grave */ });
|
||||
} catch (e) { /* idem */ }
|
||||
} catch (e) {
|
||||
LOG.warn("cookies", "listener err", { err: e && e.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Au boot du service worker, on log l'état de la permission (toujours visible)
|
||||
// et on attache le listener si déjà accordée.
|
||||
(async () => {
|
||||
try {
|
||||
const has = await _hasCookiesPermission();
|
||||
LOG.warn("cookies", `🔧 permission cookies au boot : ${has ? "ACCORDÉE → lecture cookie active" : "non accordée → fallback URL"}`);
|
||||
if (has) _attachCookiesListener();
|
||||
} catch (e) { /* silent */ }
|
||||
})();
|
||||
chrome.permissions.onAdded && chrome.permissions.onAdded.addListener((p) => {
|
||||
if (p && p.permissions && p.permissions.includes("cookies")) {
|
||||
LOG.warn("cookies", "✅ permission cookies ACCORDÉE par l'utilisateur");
|
||||
_attachCookiesListener();
|
||||
}
|
||||
});
|
||||
chrome.permissions.onRemoved && chrome.permissions.onRemoved.addListener((p) => {
|
||||
if (p && p.permissions && p.permissions.includes("cookies")) {
|
||||
LOG.warn("cookies", "🛑 permission cookies RETIRÉE → fallback URL réactivé");
|
||||
}
|
||||
});
|
||||
|
||||
+4
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Planification",
|
||||
"version": "2026.5.44",
|
||||
"version": "2026.5.45",
|
||||
"description": "Vue claire et rapide du planning des techniciens EasyVista. Développé par Quentin Rouiller — DGNSI, Canton de Vaud.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
@@ -10,6 +10,9 @@
|
||||
"tabs",
|
||||
"alarms"
|
||||
],
|
||||
"optional_permissions": [
|
||||
"cookies"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://itsma.etat-de-vaud.ch/*",
|
||||
"https://itsma.vd.ch/*"
|
||||
|
||||
+857
-55
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -28,7 +28,7 @@
|
||||
type="button" aria-label="Utilisateur connecté"
|
||||
title="Utilisateur — cliquer pour accéder aux paramètres">?</button>
|
||||
<h1 id="app-title">Planification</h1>
|
||||
<!-- R13u : bloc "Aujourd'hui + horloge" encadré, suivi DIRECTEMENT
|
||||
<!-- : bloc "Aujourd'hui + horloge" encadré, suivi DIRECTEMENT
|
||||
du statut d'actualisation (MAJ + ✓), puis le sélecteur de date
|
||||
du planning. -->
|
||||
<div id="today-block" class="today-block">
|
||||
|
||||
+3274
-328
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user