Compare commits

..
54 Commits
Author SHA1 Message Date
FroSteel 06c0195130 v2026.5.45 — Dock latéral drag&drop, fix verdicts ghost, multi-onglets EZV
Refonte de l'expérience drag&drop avec dock latéral à droite pour parquer
des interventions entre les jours. Fix critique du parser de fiche EV qui
marquait à tort des interventions terminées comme annulées (les dates
d'action sont détectées par scan regex au lieu d'indices fixes [8]/[9]).
Décorrélation des « Logs verbeux » et de la case « Garder les disparitions »
dans Paramètres → Diagnostics.

Issues résolues : #3 #4 #5 #6 #7 #8.

Closes #3 #4 #5 #6 #7 #8
2026-05-08 16:30:47 +02:00
FroSteel c65e943dac chore: retire .gitignore du repo 2026-05-01 18:17:27 +02:00
FroSteel d23824359f docs(README): mise à jour pour v2026.5.44 (topbar, Apparence, onboarding, fix #1)
- Version courante v2026.5.43 → v2026.5.44 dans le bandeau Aperçu et le tableau de versionning.
- Section Admin et configuration : ajout des entrées personnalisation Apparence (couleur topbar 12 presets + 28 polices), onboarding équipe centré, statuts EV configurables.
- Section Versions notables : nouvelle entrée v2026.5.44 (topbar refonte, Apparence, vue horizontale enrichie, stats X faits / Y clos, onboarding, refresh séquentiel, fix #1, bugfix divers).
2026-05-01 18:16:26 +02:00
FroSteel 2d242d26ec v2026.5.44 — Refonte topbar, personnalisation Apparence, onboarding équipe, fix #1
Refresh / cache / verdicts ghost :
- Rafraîchissement séquentiel (1 fiche à la fois) avec arrêt instantané
  via AbortController.
- Re-fetch checksum frais (basicAutoComplete + redirectHeader).
- Cache merge robuste avec fallback cachedByRef ; cache écrit toutes les
  5 fiches (incrémental).
- Verdicts ghost unifiés : ✓✓ clos/résolu, ✓ Fait (pending), ✓ jaune
  Suspendu, retrait silencieux pour cancelled.
- Statuts EV configurables depuis Paramètres → EasyVista (matching
  insensible à la casse, accents, conjugaisons).
- Mode diagnostic optionnel (Diagnostics) qui logge tout sans rien retirer.

Topbar (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 réduite à côté.

Personnalisation (Paramètres → Apparence) :
- Couleur de la topbar : 12 presets cliquables + picker custom + champ
  hex. Texte topbar adapté automatiquement (luminance) pour rester lisible.
- Police de l'application : 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 (cards, popups, panel admin) avec preview live.
- Export / import du cache et de admin_config.

Vue horizontale :
- Bloc Aujourd'hui + horloge empilé verticalement dans la sidebar.
- Date sélectionnée mise en avant (taille augmentée, gras), date du jour
  + heure réduites à la même petite taille.
- Barre verticale verte à droite des mini-cards clos/résolu (✓✓), avec
  décalage du ✓✓ pour ne pas chevaucher.
- Sidebar adopte la couleur de topbar custom (titre, horloge, today-block,
  date sélectionnée, boutons, theme-toggle, séparateurs translucides
  cohérents via color-mix).

Stats globales :
- Nouveau compteur 'X faits / Y clos' entre (matin · après-midi) et
  tech. dispo.
- Vue classique : séparateur '//' après clos.
- Vue horizontale (sidebar) : barre horizontale 1px de séparation.

Onboarding équipe :
- Carte centrée propre (icône, titre, description, bouton 'Ouvrir
  paramètres') quand aucun technicien n'est sélectionné. Bouton ouvre
  directement la section Équipe du panel admin.

Bugfix :
- Issue #1 (Pompier + Absence) : les deux badges s'affichent désormais
  avec '/' au lieu de masquer l'absence.
- Absences récurrentes restaurées au switch de groupe (étaient invisibles
  alors qu'en storage).
- Barre de progression / bannière session expirée suivent la hauteur
  dynamique de la topbar (--topbar-height via ResizeObserver).
- STATUS_FR regex limite 30 → 200 chars.
- Description action décodée proprement (\u0022, <br>, HTML strippé) ;
  préfixe 'login:' retiré du commentaire technicien.
- Flèche '↗' retirée des références cliquables.
2026-05-01 18:08:11 +02:00
Quentin Rouiller 54b8f826df docs: clarification audience — coordinateurs DGNSI (pas techniciens)
L'extension est utilisée par les coordinateurs DGNSI qui pilotent dans
EasyVista le planning de l'équipe technicienne. Les techniciens
eux-mêmes consultent leur planning sur leur douchette terrain et
n'utilisent pas cet outil.

Mises à jour cohérentes : 'public cible' dans README et CHANGELOG.
Les mentions 'technicien' qui désignent le sujet du planning (cartes
des techniciens, liste des techniciens cochés, etc.) sont conservées
puisque ce sont les personnes DONT on regarde le planning.
2026-04-27 06:02:12 +02:00
FroSteel 8ecf2d3df4 Actualiser README.md 2026-04-27 05:52:42 +02:00
Quentin Rouiller 9d8d8102d7 chore: retire CLAUDE.md et toutes mentions externes du repo
- Suppression du fichier CLAUDE.md (workflow de développement interne)
- Retrait des références correspondantes dans README.md et CHANGELOG.md
- .gitignore : retire la section dédiée (les règles secrets génériques
  .env / *.token / secrets.json couvrent l'essentiel)

Le repo ne contient plus que les sources, la doc utilisateur et les
métadonnées du projet.
2026-04-27 05:39:53 +02:00
Quentin Rouiller 48b00a8585 docs(README): refonte pour clarté + audience + installation directe
- Section Installation rapide ajoutée en tête (Firefox + Chromium avec
  liens directs vers la release courante).
- Audience clarifiée : 'coordinateurs et techniciens DGNSI' (pas juste
  techniciens — les deux rôles utilisent l'extension).
- Doublon de phrase v2026.5.40 retiré.
- Versions notables limitées aux 5 dernières + lien vers CHANGELOG / wiki
  Versions pour l'historique complet.
- Lien direct cliquable vers chaque version notable.
- Bandeau de liens vers toutes les pages wiki en haut du README.
- Lignes de code mises à jour : ~10 700 (viewer.js) + ~1 600 (background.js)
  + ~4 800 (viewer.css).
- Structure du repo réécrite pour refléter le layout flat sur Gitea
  (build.sh à la racine, pas dans Autres/).
- Section Développement simplifiée + référence à CLAUDE.md pour le
  workflow détaillé.
2026-04-27 05:31:29 +02:00
Quentin Rouiller 6bb97addd6 docs(CLAUDE.md): clarifie Phase 3 signature (addon AMO déjà enregistré)
L'addon [email protected] est désormais enregistré sur AMO.
Mise à jour du workflow Phase 3 : pour les versions futures, utiliser
'Téléverser une nouvelle version' (pas 'Submit a New Add-on'). Précision
aussi sur le fait que Chrome/Edge ne sont pas concernés par AMO et n'ont
pas d'auto-update natif.
2026-04-27 05:18:30 +02:00
Quentin Rouiller 05275a3be5 firefox: update_hash v2026.5.43 → sha256 du .xpi signé AMO 2026-04-27 05:02:27 +02:00
Quentin Rouiller d7b680fb3f v2026.5.43 — Fix Firefox menu dock position + stabilité popup pin/unpin
Bug Firefox uniquement : positionnement du menu hover des pastilles du
dock (popup réduit) corrigé. La cause était que getBoundingClientRect()
était appelé immédiatement après appendChild sans que Firefox n'ait fini
de calculer la mise en page, combiné à un transform: translateY dans
l'animation d'apparition du menu. Fix : positionnement hors écran initial,
force-layout via offsetHeight, puis pose finale. Animation CSS simplifiée
en opacité-only.

Stabilité popup au pin/unpin (tous navigateurs) : la popup épinglée
bougeait de 16px et changeait légèrement de taille quand on la
dé-épinglait via le bouton 📌. Cause : .pinned-popup avait padding-top
28px + border 2px alors que .soft-unpinned avait padding-top 12px + border
1px. Fix : .soft-unpinned conserve désormais les mêmes dimensions, juste
la couleur de bordure change (--border-strong gris au lieu de --accent
bleu) pour signaler le mode détaché.
2026-04-27 04:57:03 +02:00
Quentin Rouiller 3c7e7c0c25 firefox: update_hash v2026.5.42 → sha256 du .xpi signé par Mozilla AMO
Le .xpi distribué sur la release v2026.5.42 a été remplacé par sa version
signée AMO (signature META-INF/mozilla.rsa + COSE). Le sha256 dans
firefox-updates.json reflète maintenant le .xpi signé, ce qui permet
l'auto-update Firefox vers la version signée et installable.
2026-04-27 03:44:11 +02:00
Quentin Rouiller b3677d661a chore: change addon ID en [email protected]
L'ID précédent ([email protected]) était déjà enregistré sur AMO.
Nouvel ID : [email protected] — nom plus explicite (mention
DGNSI), domaine inchangé.

build.sh + firefox-updates.json mis à jour avec le nouvel ID. Sha256 du
.xpi v2026.5.42 régénéré.
2026-04-27 03:32:44 +02:00
Quentin Rouiller 8390db9937 v2026.5.42 — Nettoyage de commentaires + exemples génériques
Passage en revue des commentaires de viewer.js : les exemples qui
illustraient le parsing des contacts/lieux/références/codes-barres ont
été uniformisés en placeholders abstraits (Nom1 Prénom1 +41XXXXXXXXX,
SYYMMDD_NNNNN, XXXX_NNNNNNNN, etc.) plutôt que des chaînes spécifiques.

Comportement runtime strictement inchangé — uniquement de la documentation
et des commentaires. README, CHANGELOG et pages wiki Versions/Utilisation
mis à jour de manière cohérente.
2026-04-27 03:28:13 +02:00
Quentin Rouiller 2cc9552fbf security: anonymisation de toutes les données nominatives résiduelles
Suite à un audit de sécurité, retrait de TOUTES les données réelles dans
le code et la documentation :

- src/viewer.js : commentaires-exemples qui contenaient de vrais noms +
  numéros de téléphone (Seda Kaya, Hélène Dongiovanni, Krkic Admir et leurs
  numéros) → remplacés par 'Nom1 Prénom1 +41XXXXXXXXX', etc.
- src/viewer.js : refs tickets EV avec dates concrètes (SYYMMDD_NNNNN avec
  vraies dates) → remplacées par 'SYYMMDD_NNNNN' génériques.
- src/viewer.js : codes-barres / numéros de série (TPCQ_NNN, MNNN, DNNN,
  TNNN avec vrais chiffres) → remplacés par 'XXXX_NNNNNNNN', 'XNNNNNN'.
- README.md, CHANGELOG.md, wiki Utilisation/Versions : exemples de référence
  ticket S260424_00042 → SYYMMDD_NNNNN.

Aucune donnée nominative ni identifiant réel ne subsiste dans le code,
les commentaires, ni la documentation publique. Sha256 du .xpi mis à jour
dans firefox-updates.json.
2026-04-27 03:23:20 +02:00
Quentin Rouiller 0327a55c74 fix(firefox): ajoute background.scripts fallback (compat MV3 Firefox/AMO)
Mozilla AMO rejetait le .xpi avec :
  Unsupported "/background/service_worker" manifest property used without
  "/background/scripts" property as Firefox-compatible fallback.

build.sh ajoute maintenant 'scripts: [background.js]' à background.* dans
le manifest Firefox uniquement (Chrome ignore 'scripts' quand
'service_worker' est présent ; Firefox ignore 'service_worker' et utilise
'scripts'). Les deux navigateurs chargent le même background.js.

Sha256 du .xpi v2026.5.41 mis à jour dans firefox-updates.json.
2026-04-27 03:02:38 +02:00
Quentin Rouiller 67708d1ad3 chore: simplifie firefox-updates.json (repo public, URL raw fixe)
- update_url remis sur .../raw/branch/main/firefox-updates.json maintenant
  que le repo est public (raw URL accessible sans auth).
- firefox-updates.json toujours à la racine, contient toutes les versions ;
  Firefox lit la liste et choisit la plus haute compatible.
- Sha256 du .xpi v2026.5.41 mis à jour suite au rebuild.
- CLAUDE.md : note sur le channel d'update simplifiée.
2026-04-27 03:00:02 +02:00
Quentin Rouiller 1730758cb4 Distribution: firefox-updates.json + CLAUDE.md (workflow Claude) + nettoyage secrets
- firefox-updates.json à la racine : manifest auto-update Firefox avec entrées
  v2026.5.40 et v2026.5.41 (sha256 NON SIGNÉ pour le moment, à remplacer par
  celui des .xpi signés AMO).
- build.sh : maintient firefox-updates.json automatiquement à chaque build
  (ajoute ou met à jour l'entrée de la version courante avec son sha256
  calculé sur le .xpi produit).
- CLAUDE.md : workflow complet pour Claude Code (build → test → push → wiki →
  signature AMO). Token Gitea jamais dans le fichier (stocké hors repo en
  mémoire Claude .claude/projects/.../memory/gitea_token.md).
- .gitignore : ajout _archives/, .claude/, .env, *.token, secrets.json.
- README.md / CHANGELOG.md : retrait email auteur en clair (renvoi vers
  page wiki Contact, email obfusqué en entités HTML).
2026-04-27 02:55:18 +02:00
Quentin Rouiller 7c0742594c v2026.5.41 — Suppression des hardcodes runtime + UX admin + thème unifié 2026-04-27 03:00:00 +02:00
Quentin Rouiller af85473837 v2026.5.40 — Sélection groupe EV + édition domaines + tri équipe + vue horizontale enrichie 2026-04-27 00:43:00 +02:00
Quentin Rouiller 47a0bca998 refactor: ranger le code source dans src/ + script build.sh 2026-04-27 00:00:00 +02:00
Quentin Rouiller e92b0c4444 v2026.5.39 — Séparation Matin / Après-midi + Apparence (thème, taille du texte, durée du cache, heures de la journée) 2026-04-26 18:10:00 +02:00
Quentin Rouiller 957b754bdc v2026.5.38 — Attribution auteur + nettoyage + observabilité (LOG unifié, handlers globaux d'erreur, toggle logs verbeux dans admin) 2026-04-25 22:55:00 +02:00
Quentin Rouiller aabda3ba7e v2026.5.37 — Refonte vue horizontale (sidebar complète) : topbar supprimée, user-badge + titre + bouton Aujourd'hui + date/heure + stats en sidebar 2026-04-24 13:45:50 +02:00
Quentin Rouiller 6a0324b252 v2026.5.36 — Sidebar verticale en vue horizontale (#horizontal-wrapper [sidebar 200px] + [main]) [code interpolé entre v2026.5.35 et v2026.5.37] 2026-04-24 13:22:08 +02:00
Quentin Rouiller fd466504c2 v2026.5.35 — Fix popup épinglé position vue horizontale + stats gauche 2026-04-24 13:11:16 +02:00
Quentin Rouiller 02524e78b2 v2026.5.34 — Bouton 📌 restauré + badge user cliquable + positionTooltipAnchored unifiée [code interpolé] 2026-04-24 12:56:34 +02:00
Quentin Rouiller 193b3252d4 v2026.5.33 — Vue horizontale : interactions différenciées (hover/clic) [code interpolé] 2026-04-24 12:12:32 +02:00
Quentin Rouiller 3a28e1bd0a v2026.5.26 — Badge user-badge cliquable + auto-détection EV à l'ouverture admin 2026-04-23 16:21:48 +02:00
Quentin Rouiller 10a1aef4c7 v2026.5.25 — Bouton ⚙ Paramètres dans popup user-badge (remplace 5 clics secrets sur le titre) 2026-04-23 16:00:38 +02:00
Quentin Rouiller b77f0a9caa v2026.5.24 — Améliorations diverses 2026-04-23 15:40:00 +02:00
Quentin Rouiller f7f81f7d9d v2026.5.23 — Polish UX 2026-04-23 15:31:44 +02:00
Quentin Rouiller ddb075d563 v2026.5.22 — Stabilité popups 2026-04-23 15:13:04 +02:00
Quentin Rouiller f6dc9eaf7b v2026.5.21 — Polish positionnement popups 2026-04-23 15:03:54 +02:00
Quentin Rouiller 3d5bdbab3d v2026.5.20 — Safe area : popups jamais cachés sous topbar/dock 2026-04-23 14:48:16 +02:00
Quentin Rouiller ad952ebc55 v2026.5.19 — Drag-and-drop des popups épinglés 2026-04-23 14:34:08 +02:00
Quentin Rouiller 1a7393c297 v2026.5.18 — Polish date custom 2026-04-23 14:20:52 +02:00
Quentin Rouiller d589447533 v2026.5.17 — Date custom : label localisé (jour de la semaine en français) 2026-04-23 14:00:10 +02:00
Quentin Rouiller ea5a42c5e1 v2026.5.16 — Passage au schéma de versionning ANNÉE.MAJEURE.PATCH + faux input date custom (Mardi 24.04.2026) 2026-04-23 13:03:58 +02:00
Quentin Rouiller 763e63d9c6 v5.0.15 — Absences partielles affichées comme rows (gris foncé) 2026-04-21 16:24:24 +02:00
Quentin Rouiller bea236ca88 v5.0.14 — Affichage timeline pour absences partielles seules 2026-04-21 16:13:04 +02:00
Quentin Rouiller d6ab8d59e0 v5.0.13 — Cache + retry 2026-04-21 16:04:00 +02:00
Quentin Rouiller 909ddb8301 v5.0.12 — Stabilité 2026-04-21 15:49:08 +02:00
Quentin Rouiller 6794360887 v5.0.11 — Détection contexte réseau (interne/externe via SSO) 2026-04-21 15:44:14 +02:00
Quentin Rouiller 7ba28d3bac v5.0.10 — Stabilité session EV 2026-04-21 15:32:44 +02:00
Quentin Rouiller e17f604d9e v5.0.9 — Surveillance timeout session EasyVista (compteur tick 1s, alertes 5min/2min) 2026-04-21 15:19:06 +02:00
Quentin Rouiller 9d701701e6 v5.0.8 — Correctifs 2026-04-21 12:53:22 +02:00
Quentin Rouiller 77c68dbe83 v5.0.7 — Correctifs 2026-04-21 12:50:36 +02:00
Quentin Rouiller d4fc8ff250 v5.0.6 — Correctifs 2026-04-21 12:46:58 +02:00
Quentin Rouiller 3996e3fb4f v5.0.5 — Correctifs admin/UX 2026-04-21 12:42:50 +02:00
Quentin Rouiller 86f52029f5 v5.0.4 — Améliorations admin/UX 2026-04-21 12:40:08 +02:00
Quentin Rouiller 984f326b39 v5.0.3 — Ajustements admin et stabilité 2026-04-20 14:03:34 +02:00
Quentin Rouiller 6d3058028f v5.0.1 — Refonte topbar : horloge HH:MM + compteur session EV + admin caché (5 clics titre) 2026-04-20 13:21:16 +02:00
Quentin Rouiller c59abbed23 v4.3.3 — Soft unpin popup + nettoyage tooltip persistance 2026-04-20 09:13:20 +02:00
16 changed files with 2650 additions and 12330 deletions
+6 -201
View File
@@ -1,7 +1,7 @@
# CHANGELOG — Extension Planification EasyVista Canton de Vaud # CHANGELOG — Extension Planification EasyVista Canton de Vaud
> Ce changelog documente l'évolution de l'extension Chrome/Firefox "Planification" > Ce changelog documente l'évolution de l'extension Chrome/Firefox "Planification"
> développée par Quentin pour les coordinateurs DGNSI (Canton de Vaud). > développée par Quentin Rouiller pour les coordinateurs DGNSI (Canton de Vaud).
> >
> Les versions documentées ci-dessous sont celles dont les détails sont connus. > Les versions documentées ci-dessous sont celles dont les détails sont connus.
> Pour les versions plus anciennes, l'analyse du code source permet de > Pour les versions plus anciennes, l'analyse du code source permet de
@@ -9,201 +9,6 @@
--- ---
## v2026.5.50 — Texte des actions rétabli (format EasyVista du 29.09.2026), catégories Roll Out et Déménagement
> Correctif urgent : depuis le 29 septembre 2026, le texte complet des actions ne s'affichait plus. EasyVista renvoie désormais les actions avec une cellule de moins, et l'extension les lisait par position. Elle les retrouve maintenant par leur contenu. Cette version ajoute aussi deux catégories de réservation.
### Texte des actions
- **Texte complet des actions rétabli** dans les bulles et les fiches, avec l'ancien comme avec le nouveau format d'EasyVista.
- **Plus robuste aux prochains changements** : la description est repérée par le nom de sa colonne, et l'identifiant de l'action par son contenu, plus par leur position.
- **Changement de format signalé** : si EasyVista modifie encore la structure au point qu'aucune action ne soit lisible, un avertissement apparaît dans la console (une seule fois, sans aucune donnée personnelle). Les fiches ou historiques qui ne se chargent pas sont aussi signalés dans la console.
### Réservations
- **Deux nouvelles catégories : Roll Out et Déménagement.** Elles sont propres à la Planification : dans EasyVista, la réservation reste « Evènements spéciaux ». Elles sont partagées entre tous les postes par le dossier partagé, comme le n°, la ville et la rue.
- **Catégorie proposée à la saisie du n°** : le n° d'une intervention Roll Out propose Roll Out.
- **Catégorie inconnue conservée** : une catégorie posée par une version plus récente n'est plus effacée quand on modifie la réservation.
### Déploiement
- **Mettre à jour tous les postes le même jour.** Un poste resté en 2026.5.49 qui modifie une réservation Roll Out ou Déménagement effacerait cette catégorie sur tous les postes.
---
## v2026.5.49 — Sécurité hors session, onglet Email clarifié, lecture au défilement, affichage et synchronisation fiabilisés
> Cette version renforce la **sécurité** quand aucune session EasyVista n'est active, clarifie l'**onglet Email** (déplacement et annulation bien séparés, aperçu de la copie), améliore la **lecture du planning au défilement** (le nom du technicien, son compteur et la frise horaire restent visibles), et corrige en profondeur l'**affichage de la reconnexion** ainsi que plusieurs comportements de la synchronisation et des réservations.
### Sécurité et confidentialité (sans session EasyVista)
- **Aucune écriture dans le dossier partagé sans session active** : tant que vous n'êtes pas identifié dans EasyVista, l'extension travaille sur le **cache local seul** et ne touche plus au dossier partagé. Un ancien cache reste consultable, mais il n'est plus alimenté ni relu depuis le partage.
- **Aucune modification sans session active** : déplacement d'intervention, suppression, création ou édition d'absence, réservation — tout est bloqué proprement tant que la session n'est pas rétablie, avec un message clair. Cela évite d'écrire des données incohérentes qui seraient ensuite propagées aux autres postes.
### Onglet Email clarifié
- **Déplacement et annulation séparés** : l'onglet Email présente désormais deux sections distinctes (déplacement d'une intervention / annulation-suppression), chacune avec sa case d'activation et son propre modèle de message.
- **Copie (CC) commune avec aperçu en direct** : un bloc « Copie (CC) » en tête montre, sous forme de puces, les adresses effectivement retenues (les adresses invalides sont barrées) — vous voyez immédiatement à qui la copie sera envoyée.
- **Variables cliquables en colonnes** pour les deux modèles (déplacement et annulation), rangées par thème pour une insertion rapide.
### Lecture du planning au défilement (vue classique)
- **En-tête de carte collant** : quand vous faites défiler une carte, le **nom du technicien**, son **nombre d'interventions** et la **frise horaire** restent visibles en haut — plus besoin de remonter pour savoir de qui il s'agit.
- **Survol d'une pause** : passer la souris sur une pause met la **frise horaire en évidence** (sans faner les interventions), pour bien distinguer le créneau libre.
### Affichage et reconnexion fiabilisés
- La bannière **« Session EasyVista expirée »** s'affiche au bon endroit (dans le corps, sous la barre de recherche) et **uniquement quand de vraies cartes sont affichées** ; sinon un message centré prend la place du planning. Plus de fausse bannière au-dessus d'un écran vide.
- **Plus de bulles de techniciens vides** : lors d'un rafraîchissement automatique alors que la session est morte, l'extension détecte la page de connexion EasyVista et évite d'afficher une équipe vide.
- La **barre de progression** ne recouvre plus le nom du technicien pendant un rafraîchissement lorsqu'on a fait défiler.
### Corrections
- **Noms de contact nettoyés** : les caractères parasites (« ? » et caractères de remplacement) parfois collés aux noms venant d'EasyVista sont retirés.
- **Deux interventions du même technicien le même jour** (deux actions distinctes) s'affichent désormais chacune sur sa propre carte, sans en perdre.
- **Caractères parasités depuis EasyVista** (dans les libellés d'intervention, contacts, lieux) corrigés à la source.
- **Indicateur d'écart d'adresse** remis à jour après correction : actualiser une intervention dont l'adresse a été corrigée retire bien l'alerte.
---
## v2026.5.48 — Notification par email des déplacements, onglet Email, fiches réservation fiables entre postes, recherche et reconnexion durcies
> Cette version apporte une vraie nouveauté : après le déplacement d'une intervention (autre jour ou autre technicien), l'extension peut ouvrir un **brouillon d'email pré-rempli** dans votre application mail pour prévenir les techniciens concernés — l'envoi reste un geste manuel. Elle corrige aussi en profondeur la circulation des informations de réservation entre postes, le bouton de reconnexion quand il n'y a aucun cache, et plusieurs comportements de la recherche.
### Email après un déplacement (nouveau)
- **Brouillon automatique** : après un déplacement vers un **autre jour** ou un **autre technicien** (fenêtre de modification ou glisser-déposer), un brouillon pré-adressé aux deux techniciens s'ouvre dans l'application mail (ex. Outlook). Un simple ajustement d'heures n'ouvre rien. **Rien n'est envoyé automatiquement.**
- **Case « Prévenir les techniciens par email »** dans les fenêtres de déplacement : quand l'option est active, la décocher ne vaut que pour **ce déplacement-là** — le prochain déplacement la retrouve cochée. L'option ne se désactive durablement que dans les Paramètres.
- **Destinataires** séparés par « ; » (compatibilité Outlook) ; si une adresse manque, le brouillon s'ouvre quand même, à compléter directement dans l'email (une notification indique qui manque).
- **Nouvel onglet « Email »** dans les Paramètres : option, et **modèle du message** entièrement personnalisable (sujet + texte), avec **variables cliquables** rangées par thème (Intervention / Avant / Après) et **aperçu en direct** sur un exemple. Le modèle de base présente le déplacement en deux blocs AVANT / APRÈS bien lisibles.
- **Adresses des techniciens** dans l'onglet Équipe (colonne Email, boutons au-dessus de la liste) : bouton « **Récupérer les emails** » qui remplit les adresses manquantes depuis l'annuaire EasyVista — et **complète au passage un nom affiché tronqué** (ex. « Martins » → « De Almeida Martins »), avec un garde-fou contre les homonymes. Une adresse saisie à la main n'est jamais écrasée. Champ vide = adresse proposée `prenom.nom@domaine` (domaine réglable au même endroit, appliqué à tous).
### Informations de réservation fiables entre postes
- Les informations saisies sur une réservation (n° d'intervention, ville, rue, catégorie) **voyagent désormais avec la réservation** dans le dossier partagé, horodatées : la modification la plus récente gagne, sur tous les postes.
- Fin des « fuites » d'informations : une réservation posée par un autre coordinateur ne récupère plus les infos d'une réservation précédente au même créneau, un identifiant recyclé par EasyVista ne fait plus hériter les infos d'un autre technicien, et un champ volontairement vidé ne « ressuscite » plus.
- Une réservation **re-planifiée directement dans EasyVista** conserve ses informations quand le rapprochement est sans ambiguïté.
- Divers : le pompier du jour n'est plus compté en double dans les statistiques ; « réservé toute la journée » se calcule sur les heures affichées ; les champs pré-remplis du formulaire ne sont plus écrasés par le remplissage automatique.
### Reconnexion et recherche
- **Bouton « Ouvrir EasyVista »** (écran « Session expirée » sans aucun cache) : après la reconnexion, l'écran se ferme réellement et le planning s'affiche ; re-cliquer relance le chargement au lieu d'être ignoré.
- **Recherche** : la recherche part de façon fiable quand on arrête de taper (les relances au survol ne l'interrompent plus), **Entrée** lance la recherche immédiatement, la roue de chargement est visible dès le début, déplier « Planifiée (passé) » ne ferme plus la zone de résultats, et seuls les statuts **Clôturé / Fait** sont affichés sur les résultats.
---
## v2026.5.47 — Vue « échelle de temps exacte » : aide à la saisie, erreurs mises en évidence, corrections de fiabilité
> Cette version peaufine la vue horizontale « échelle de temps exacte » : un vrai retour visuel accompagne la pose d'un rendez-vous ou d'une absence, et les erreurs de saisie (rendez-vous dont le début est égal à la fin) sont désormais bien visibles et signalées. Elle corrige aussi plusieurs problèmes de navigation entre dates et de synchronisation du dossier partagé, réorganise le panneau de synchronisation, et ajoute un bouton de test de la communication avec le script du poste.
### Vue « échelle de temps exacte » — aide à la saisie
- **Retour visuel au survol d'un créneau libre** : une zone surlignée suit la souris et affiche **l'heure exacte** (début → fin) du rendez-vous ou de l'absence qu'un clic droit poserait. Une bulle fixe, en haut de la ligne, rappelle la **plage libre et la durée disponible** du créneau (« Libre 12:00–18:00 / 6 h disponible »). Elle se place toujours sur un espace libre — jamais par-dessus un rendez-vous — et ne masque pas l'heure choisie.
- **Erreurs de saisie mises en évidence** : un rendez-vous dont le **début est égal à la fin** (ex. 10:00 → 10:00) est désormais affiché en **rouge** et reste **visible** (avant, il disparaissait sur l'échelle exacte). Survoler une telle zone pour en poser un autre affiche un **avertissement** et **empêche** la création tant que l'erreur n'est pas corrigée.
- **Interventions courtes visibles** : une intervention de 1 à 5 minutes garde une largeur minimale et reste visible.
- **Zone du nom** : le fond du nom du technicien remplit toute la hauteur de la ligne (nom centré).
- **Barre des heures** : reste collée en haut au défilement et passe sous la banderole de reconnexion (au lieu de disparaître derrière).
### Corrections de fiabilité
- **Navigation rapide entre dates** : le jour que l'on quitte ne peut plus écraser le jour qui vient de s'afficher.
- **Technicien retiré de l'équipe** : n'apparaît plus sur les jours relus depuis le dossier partagé.
- **Synchronisation du dossier partagé** : correction d'un blocage où l'écriture n'aboutissait jamais (conflit permanent entre l'index et le fichier relu) ; l'index se répare de lui-même.
- **Jour présent au dossier mais pas en local** : lu depuis le dossier d'emblée — plus de re-remplissage visible inutile.
- **Absences** : plus de doublon « absence récurrente » + « vraie absence » le même jour ; le fond des cartes d'absence remplit toute la carte en échelle exacte.
- **Réservations multiples le même jour** : les deux créneaux coexistent ; saisir le n° d'intervention dans le formulaire remplit automatiquement ville, rue et catégorie.
- **Barre de progression** et **message de reconnexion** : bien positionnés en vue horizontale (barre en haut de la sidebar, message centré dans le contenu).
### Synchronisation multi-postes
- **Nouveau bouton « Tester la communication »** (onglet Synchro) : diagnostic de bout en bout extension → script du poste → dossier partagé.
- **Panneau Synchro réorganisé** en sections claires : état de la synchro, dossier partagé, script sur le poste, journal d'activité.
- **Script du poste (native messaging host) 2026.1.8** : verrou dédié sur l'index du dossier partagé (protège l'écriture simultanée par plusieurs postes ou onglets) et icône de la zone de notification ré-affichée au redémarrage de l'explorateur Windows. Le bouton « Télécharger le script » récupère cette version.
---
## v2026.5.46 — Refonte de la couche données, vue « échelle de temps exacte », session fiabilisée, réservations enrichies
> Version majeure par son contenu : la couche données est entièrement refondue (store unifié par jour, un seul verdict par intervention, synchronisation du dossier partagé en sous-dossier `v2`), une nouvelle disposition « échelle de temps exacte » s'ajoute à la vue horizontale, la détection de session EasyVista est fiabilisée de bout en bout, et les réservations portent enfin leurs informations (n°, lieu, catégorie) sur tous les postes. Le script PowerShell du dossier partagé est désormais publié avec l'extension et téléchargeable depuis les Paramètres.
### Couche données v2 (refonte complète)
- **Un store par jour** (`planv2_<date>`) : liste plate d'interventions dédoublonnées par identité stable (requestId > actionId > réf) — la seule source de vérité ; le rendu, le cache local, le dossier partagé et EasyVista n'en sont que des projections ou des sources.
- **Un seul verdict par intervention** (`live / done / suspended / closed / gone / moved / unknown`) qui remplace les anciens marqueurs dispersés (ghost, statuts de disparition, diagnostics).
- **Une seule fusion** (modèle vie/mort) utilisée partout : EV↔store, local↔dossier, import. Une vie strictement postérieure à une mort lève les tombstones (réapparition EV autoritaire).
- **Dossier partagé** : transport natif inchangé (verrous + écriture atomique + compare-and-swap), fichiers v2 dans le sous-dossier `v2` du partage — les postes en ancienne version n'interfèrent plus. Ordre des sources refondu : EasyVista s'affiche immédiatement, le dossier habille les cartes sans re-télécharger, le fetch ne traite que ce qui manque, l'enrichissement est repoussé au dossier en fin de chargement.
- Les interventions déplacées par l'extension portent un verdict « déplacée » propagé (plus d'analyse inutile), avec fenêtre anti cache-serveur EV.
- Les disparues en attente de verdict restent affichées (grisées, spinner) au lieu de disparaître puis réapparaître.
- Outil réservé à l'auteur (onglet Test) : conversion de l'ancien cache dossier v1 vers le modèle v2, sans modifier les anciens fichiers.
- Les anciens caches (`planning_cache_*`) sont purgés au démarrage.
### Vue horizontale : nouvelle disposition « échelle de temps exacte »
- **Nouvelle option** dans Paramètres → Apparence (exclusive avec « Afficher les pauses » : cocher l'une décoche l'autre). La vue par défaut reste inchangée.
- Le bandeau timeline disparaît : chaque intervention/réservation/absence partielle est posée **en position réelle** sur une bande continue de la plage affichée (position et largeur proportionnelles à l'heure et à la durée).
- **Règle des heures fixe** en haut (reste collée au défilement), **lignes verticales continues** sur toute la hauteur du tableau : trait net à chaque heure, pointillé discret aux demi-heures, césure rayée à midi ; les traits passent derrière les bulles et sautent les techniciens absents.
- **Ligne rouge « maintenant »** continue sur toute la hauteur (jour courant), reposée instantanément à chaque rendu ; elle saute aussi les absents.
- **Chevauchements** : les éléments qui se recouvrent s'empilent en sous-lignes de même hauteur — la zone du technicien s'agrandit seulement dans ce cas ; tolérance de 10 minutes pour ne pas empiler de simples enchaînements.
- **Cartes** : fond neutre, le bord gauche porte la couleur de catégorie et sert de **poignée** (cliquer = fenêtre de modification, glisser = déplacer ; présente sur toutes les cartes, déplacement bloqué pour les faites/clôturées, réservations et absences). Numéro d'intervention en tête, ville + adresse groupées ; cartes étroites : texte compact (ville + fin d'adresse, « Rue de l'Exemple 5 » → « Exemple 5 »), jamais vides tant qu'il reste de la place.
- **Réservations** : affichent sous l'intitulé le lieu renseigné à la création, sinon le nom de la personne qui a réservé.
- **Zones libres** : survol = « Libre HH:MM–HH:MM » (y compris sur les journées totalement vides), clic droit = Réservation/Absence à l'heure du curseur, dépôt du drag précis (pas de 30 min) avec marqueur vert sur toute la bande.
- Alignement règle/cartes mesuré au pixel réel (indépendant du zoom et de la taille de texte).
### Session EasyVista fiabilisée (fin des faux « déconnecté »)
- **Cookie réécrit ≠ supprimé** : le navigateur émet toujours une « suppression » technique juste avant de reposer un cookie mis à jour — l'extension basculait à tort en « session expirée » à chaque rafraîchissement du PHPSESSID (permission cookies active). Désormais ignoré ; une vraie suppression attend 3 s (relog possible) puis vérifie au serveur avant d'alarmer.
- **Expiration sur preuve uniquement** : une erreur transitoire (réseau, HTTP 5xx) ne déclenche plus la bannière — seule une preuve le fait (page de login à la sonde, temps restant nul, plus aucun PHPSESSID) ; sinon nouvel essai dans 30 s.
- **Onglet EasyVista facultatif** avec la permission cookies : le cookie suffit aux requêtes — fermer l'onglet EV ne « perd » plus la session.
- **Contre-vérification HTTP** : avant de classer un 401/403/404 en « session expirée », l'endpoint d'autorité `session_time` est sondé ; si la session vit, l'erreur est reclassée « EV inaccessible » (transitoire).
- **Compteur calé sur le serveur** : recalage périodique sur la vraie valeur EasyVista (au démarrage, toutes les 90 s, au retour de visibilité), plus de recalage erroné sur l'expiration du cookie ; badge session en direct (MM:SS) avec « Prolonger la session » dans le popup.
### Réservations
- **Modal enrichie** : préréglages Matin/Après-midi/Journée puis heures ; champs n° d'intervention, ville, rue, catégorie ; recherche automatique des informations dans EasyVista à partir du n° (catégorie et localisation pré-remplies).
- **Meta persistante et partagée** : ces informations survivent aux rafraîchissements (EV ne les stocke pas) et remontent désormais au dossier partagé — tous les postes voient le n°, le lieu et la catégorie d'une réservation, quel que soit le poste qui l'a créée ou modifiée.
- Édition d'une réservation (heures ou infos seules), suppression, tooltip détaillé avec ouverture/copie du n° lié.
### Pompier
- **Assignations « pompier du mois »** : définition par période (bouton Ajouter → Pompier), affichage dans la barre de stats, liste des pompiers actuels/à venir, édition/déplacement de période.
- La bandelette « En pompier du … au … » reste fixée en haut de la carte ; un pompier absent toute la journée est exclu du compteur.
### Coordinateur (en développement)
- Badge « Mes actions » dans la barre de stats + onglet dédié listant les interventions où le coordinateur a une action ; outil d'impersonation (réservé à l'auteur) pour tester.
### Douchette
- **Vraie confirmation EasyVista** : l'envoi vérifie le marqueur de prise en compte renvoyé par EV — toast « confirmé par EasyVista » fiable.
- **Resynchronisation automatique** après un déplacement d'intervention (jour source + jour cible, regroupés si même jour), avec compte-rendu clair et relance possible.
### Recherche
- **Opérateurs** : espace = tous les mots, `%` joker, `"expression exacte"`, `-mot` exclusion, `tech:/lieu:/ref:/cat:/contact:` ciblage de champ.
- **Pagination** 15 par 15 au défilement ; passé replié par défaut ; badge « Jour affiché » ; aide sur la syntaxe.
- **Mise en avant stabilisée** : « Jour ↗ » navigue et surligne la carte pendant 15 s, survit aux re-rendus, un seul défilement animé.
- La croix d'effacement native du navigateur (en doublon avec celle de l'extension) est masquée.
### Interventions & affichage
- **Multi-jours** : badge pilule (« 📅 ⇢ 16/06 » / « ⇠ 12/06 »), heures d'affichage par jour, période complète au survol, déplacement bloqué (à modifier dans EasyVista).
- **Conflit au déplacement** : le modal de confirmation signale toute occupation du technicien cible (détail en rouge) et **propose le créneau libre le plus proche** (même durée) en un bouton.
- **Vérification d'adresse** : la localisation officielle de la fiche devient l'adresse affichée ; si elle diverge de celle écrite dans l'action, le lieu passe en rouge avec explication au survol (comparaison tolérante ; exception Roll Out).
- **Contacts** : deux numéros de téléphone dans une action ne sont plus collés à l'affichage (désormais « 021 000 00 84 / 079 000 00 21 ») ; labels de fiche collés au contact séparés proprement.
- **Ouverture de fiche** : les interventions faites/clôturées se rouvrent à nouveau dans EasyVista (le lien interne est régénéré via la référence).
- Sections Matin / Après-midi / « Journée » harmonisées sur les deux vues ; éléments chevauchant midi non découpés ; catégorie « Autre » en violet foncé (fini le gris confondu avec les cartes faites).
- Interventions « faites » plus ré-analysées à chaque actualisation (économie réseau).
### Dossier partagé : script PowerShell publié
- Le script hôte (native messaging) est publié avec cette version et **téléchargeable depuis Paramètres → Dossier partagé**.
### Qualité
- Nettoyage complet : fonctions mortes retirées, aucun doublon de code, tous les journaux passés au logger maison (console propre en production), plus aucun emoji dans les logs, marqueurs de révision internes retirés.
---
## v2026.5.45 — Dock latéral drag&drop, fix verdicts ghost, multi-onglets EZV résolu ## 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. > 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.
@@ -562,7 +367,7 @@ Une intervention terminée par le tech mais dont la fiche était passée en stat
### Attribution auteur ### Attribution auteur
- Ajout en-têtes copyright dans tous les fichiers source - Ajout en-têtes copyright dans tous les fichiers source
(viewer.js, viewer.html, viewer.css, background.js) (viewer.js, viewer.html, viewer.css, background.js)
- Ajout `@author Quentin` sur les fonctions principales - Ajout `@author Quentin Rouiller` sur les fonctions principales
(loadForDate, buildCard, buildTooltipHTML, pinTooltip, _softUnpinPopup, (loadForDate, buildCard, buildTooltipHTML, pinTooltip, _softUnpinPopup,
positionTooltipAnchored, _applyViewMode, _moveElementsToSidebar, positionTooltipAnchored, _applyViewMode, _moveElementsToSidebar,
_restoreElementsToTopbar, fetchAndShowCurrentUser, _maybeRetryFetchUser, _restoreElementsToTopbar, fetchAndShowCurrentUser, _maybeRetryFetchUser,
@@ -570,7 +375,7 @@ Une intervention terminée par le tech mais dont la fiche était passée en stat
openPersistentTimelinePopup, showTooltip, _findFreePopupPosition, openPersistentTimelinePopup, showTooltip, _findFreePopupPosition,
_clampPopupInSafeArea, findEasyVistaSession, fetchPlanningXml, _clampPopupInSafeArea, findEasyVistaSession, fetchPlanningXml,
fetchCurrentUser, detectNetworkContext) fetchCurrentUser, detectNetworkContext)
- Ajout signature "Développé par Quentin" en bas du popup - Ajout signature "Développé par Quentin Rouiller" en bas du popup
user-badge (style cohérent avec footer version : 11px, italique, user-badge (style cohérent avec footer version : 11px, italique,
gris atténué, séparateur fin) gris atténué, séparateur fin)
- Mise à jour `description` du manifest pour mentionner DGNSI - Mise à jour `description` du manifest pour mentionner DGNSI
@@ -668,7 +473,7 @@ Une intervention terminée par le tech mais dont la fiche était passée en stat
## v2026.5.29 — Contraste++ + footer ## v2026.5.29 — Contraste++ + footer
- Contrastes encore plus forts (text-muted #d0d5de dark, #2e3642 light) - Contrastes encore plus forts (text-muted #d0d5de dark, #2e3642 light)
- Footer FRS/version : 13px badge avec fond bg-muted + bordure - Footer QRO/version : 13px badge avec fond bg-muted + bordure
- Fix highlight row : selector .intervention-v2[data-iv-idx] - Fix highlight row : selector .intervention-v2[data-iv-idx]
- Scroll-into-view automatique au hover segment timeline - Scroll-into-view automatique au hover segment timeline
@@ -736,7 +541,7 @@ Une intervention terminée par le tech mais dont la fiche était passée en stat
- v4.2.3 : grande popup timeline persistante (clic), suit-souris (hover) - v4.2.3 : grande popup timeline persistante (clic), suit-souris (hover)
- v4.2.3 : bindTimelinePopover, showTimelinePopover, moveTimelineTooltip - v4.2.3 : bindTimelinePopover, showTimelinePopover, moveTimelineTooltip
- v4.2.4 : setTooltipViewportPosition (détection auto fixed/abs) - v4.2.4 : setTooltipViewportPosition (détection auto fixed/abs)
- v4.2.9 : pied de page discret FRS/version - v4.2.9 : pied de page discret QRO/version
- v4.2.9 : initModalScrollLock (bloquer scroll arrière modal) - v4.2.9 : initModalScrollLock (bloquer scroll arrière modal)
- v4.3.0 : tooltip live libéré après épinglage (réutilisable autres survols) - v4.3.0 : tooltip live libéré après épinglage (réutilisable autres survols)
- v4.3.3 : _softUnpinPopup (désépinglage mou) - v4.3.3 : _softUnpinPopup (désépinglage mou)
@@ -762,6 +567,6 @@ Une intervention terminée par le tech mais dont la fiche était passée en stat
## Auteur ## Auteur
**Quentin** (FRS) **Quentin Rouiller** (QRO)
Technicien DGNSI — Canton de Vaud Technicien DGNSI — Canton de Vaud
Contact : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact) Contact : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact)
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2026 Quentin Copyright (c) 2026 Quentin Rouiller
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+6 -52
View File
@@ -6,10 +6,10 @@ Extension Chrome / Firefox pour visualiser de manière claire et rapide le plann
## Aperçu rapide ## Aperçu rapide
- **Auteur** : Quentin (FRS), Technicien DGNSI — Canton de Vaud - **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 - **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 - **Démarrage projet** : jeudi 16 avril 2026
- **Version actuelle** : [`v2026.5.50`](https://gitea.netaplaid.ch/FroSteel/Planification/releases/tag/v2026.5.50) (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) - **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+) - **Manifest** : V3 (Chrome/Edge/Firefox 140+)
- **Format** : `.zip` (Chromium) + `.xpi` signé Mozilla (Firefox) - **Format** : `.zip` (Chromium) + `.xpi` signé Mozilla (Firefox)
@@ -47,7 +47,6 @@ Les mises à jour sont **manuelles** : à chaque nouvelle release, retélécharg
- **Vue classique** (depuis v1.0.0) : cards en grille, mode compact écran 24" (depuis v2026.5.30) - **Vue classique** (depuis v1.0.0) : cards en grille, mode compact écran 24" (depuis v2026.5.30)
- **Vue horizontale** (depuis v2026.5.32) : timeline par tech, sidebar verticale (depuis v2026.5.36) - **Vue horizontale** (depuis v2026.5.32) : timeline par tech, sidebar verticale (depuis v2026.5.36)
- Depuis v2026.5.40 : barre couleur catégorie + référence + ville sur chaque segment timeline - Depuis v2026.5.40 : barre couleur catégorie + référence + ville sur chaque segment timeline
- **Vue horizontale « échelle de temps exacte » (depuis v2026.5.46)** : option Paramètres → Apparence — chaque intervention est posée à son heure réelle sur une bande continue (règle des heures fixe, lignes d'heures et demi-heures sur toute la hauteur, ligne rouge « maintenant », chevauchements empilés, poignée de déplacement colorée, zones libres survolables et cibles de drop précises)
- Toggle Vue classique ↔ Vue horizontale via bouton ⊞ dans popup user-badge - Toggle Vue classique ↔ Vue horizontale via bouton ⊞ dans popup user-badge
- Persistance localStorage (`view_mode`) - Persistance localStorage (`view_mode`)
@@ -66,19 +65,12 @@ Les mises à jour sont **manuelles** : à chaque nouvelle release, retélécharg
- Badge + barre gauche colorée + dégradé fond - Badge + barre gauche colorée + dégradé fond
- Absences récurrentes (configurées par tech) : cyan (depuis v2026.5.30) - Absences récurrentes (configurées par tech) : cyan (depuis v2026.5.30)
### Email après un déplacement (depuis v2026.5.48)
- Brouillon d'email pré-adressé aux deux techniciens après un déplacement vers un autre jour ou un autre technicien (fenêtre de modification ou glisser-déposer) — l'envoi reste manuel, rien ne part automatiquement
- Case « Prévenir les techniciens par email » dans les fenêtres de déplacement : la décocher ne vaut que pour ce déplacement-là
- Onglet Paramètres → Email : option et modèle du message personnalisable (variables cliquables par thème, aperçu en direct)
- Adresses des techniciens dans l'onglet Équipe : récupération depuis l'annuaire EasyVista (complète aussi un nom affiché tronqué), déduction `prenom.nom@domaine` sinon
### User et session ### User et session
- Badge user avec photo/initiales en topbar - Badge user avec photo/initiales en topbar
- Badge cliquable (depuis v2026.5.26) : popup avec ⚙ Paramètres + ⊞ Vue + compteur session MM:SS - Badge cliquable (depuis v2026.5.26) : popup avec ⚙ Paramètres + ⊞ Vue + compteur session MM:SS
- Retry automatique en cas d'échec fetch user (60s, max 10 essais) - Retry automatique en cas d'échec fetch user (60s, max 10 essais)
- Compteur de session EasyVista (tick 1s, depuis v5.0.0) — recalé sur la vraie valeur serveur (depuis v2026.5.46) - Compteur de session EasyVista (tick 1s, depuis v5.0.0)
- Reconnexion automatique - Reconnexion automatique
- Détection de session fiabilisée (v2026.5.46) : plus de faux « déconnecté » (cookie réécrit, erreurs transitoires, onglet EV facultatif avec la permission cookies)
### Admin et configuration ### Admin et configuration
- Mode admin : bouton ⚙ Paramètres dans popup user-badge (depuis v2026.5.25) - Mode admin : bouton ⚙ Paramètres dans popup user-badge (depuis v2026.5.25)
@@ -121,45 +113,7 @@ Le numéro de **majeure** n'est **pas** un mois et **pas** un chiffre lié au ca
## Versions notables ## Versions notables
### `v2026.5.50` (latest, 29 septembre 2026) — Texte des actions rétabli, catégories Roll Out et Déménagement ### `v2026.5.45` (latest, 8 mai 2026) — Dock latéral drag&drop, fix verdicts ghost, multi-onglets EZV résolu
- Correctif : depuis le 29 septembre 2026, EasyVista renvoie les actions avec une cellule de moins et le texte complet des actions ne s'affichait plus ; l'extension retrouve désormais ces informations par leur contenu et par le nom des colonnes, pour tenir aux prochains changements de format
- Si EasyVista change encore de format, un avertissement clair apparaît dans la console (une seule fois)
- Réservations : deux nouvelles catégories, Roll Out et Déménagement, propres à la Planification (dans EasyVista, la réservation reste « Evènements spéciaux ») ; partagées entre postes comme les autres informations de réservation
- Mettre à jour tous les postes le même jour : un poste resté en 2026.5.49 effacerait ces catégories en modifiant la réservation
### `v2026.5.49` (1 septembre 2026) — Sécurité hors session, onglet Email clarifié, lecture au défilement, affichage fiabilisé
- Sans session EasyVista active : plus d'écriture dans le dossier partagé (cache local seul) ni de modification (déplacement, suppression, absence, réservation bloqués proprement)
- Onglet Email séparé en deux sections (déplacement / annulation), copie (CC) commune avec aperçu en direct des adresses retenues, variables en colonnes
- Vue classique : nom du technicien, compteur et frise horaire restent visibles au défilement ; survol d'une pause qui met la frise en évidence
- Bannière « Session expirée » bien positionnée et affichée uniquement sur du vrai contenu ; plus de bulles de techniciens vides quand la session est morte
- Corrections : noms de contact nettoyés, deux interventions d'un même technicien le même jour affichées séparément, caractères parasités depuis EasyVista, indicateur d'écart d'adresse remis à jour après correction
### `v2026.5.48` (24 juillet 2026) — Notification par email des déplacements, onglet Email, fiches réservation fiables entre postes
- Brouillon d'email pré-rempli après un déplacement (autre jour ou autre technicien), modèle personnalisable avec variables et aperçu, case par déplacement
- Adresses des techniciens depuis l'annuaire EasyVista, noms tronqués complétés, domaine configurable
- Les informations de réservation voyagent avec la réservation entre les postes (horodatées, plus de fuites entre créneaux ou techniciens)
- Bouton « Ouvrir EasyVista » fiable sans cache ; recherche durcie (Entrée immédiate, plus d'interruptions au survol, statuts finals seulement)
### `v2026.5.47` (18 juillet 2026) — Vue « échelle de temps exacte » : aide à la saisie, erreurs mises en évidence
- Retour visuel de pose (zone + heure choisie + bulle « temps disponible »), rendez-vous à durée nulle visibles en rouge et bloquants, interventions courtes visibles
- Corrections : navigation rapide entre dates, synchro du dossier partagé (conflit d'index), doublons d'absences, réservations multiples le même jour
- Panneau Synchro réorganisé + bouton « Tester la communication » ; script du poste 2026.1.8
### `v2026.5.46` (9 juillet 2026) — Couche données v2, vue « échelle de temps exacte », session fiabilisée, réservations partagées
- Refonte complète de la couche données : store unifié par jour, un seul verdict par intervention, synchronisation du dossier partagé en sous-dossier v2
- Nouvelle disposition « échelle de temps exacte » pour la vue horizontale (option Apparence)
- Fin des faux « déconnecté » EasyVista (cookies, erreurs transitoires, contre-vérification serveur)
- Les informations des réservations (n°, lieu, catégorie) sont partagées entre tous les postes
- Douchette : vraie confirmation EasyVista + resynchronisation automatique après déplacement
- Recherche : opérateurs, pagination, mise en avant stabilisée
- Le script PowerShell du dossier partagé est publié et téléchargeable depuis Paramètres → Dossier partagé
### `v2026.5.45` (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. - **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. - **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. - **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.
@@ -270,10 +224,10 @@ de cette version dans `firefox-updates.json`.
## Licence ## Licence
[MIT License](LICENSE) — Copyright (c) 2026 Quentin [MIT License](LICENSE) — Copyright (c) 2026 Quentin Rouiller
## Auteur ## Auteur
**Quentin** (FRS) **Quentin Rouiller** (QRO)
Technicien DGNSI — Canton de Vaud Technicien DGNSI — Canton de Vaud
Contact : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact) Contact : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact)
Regular → Executable
-3
View File
@@ -25,9 +25,6 @@ cp -r src/* dist/firefox/
python3 - <<EOF python3 - <<EOF
import json import json
with open('src/manifest.json', 'r') as f: m = json.load(f) with open('src/manifest.json', 'r') as f: m = json.load(f)
# Le champ 'key' (ID Chromium déterministe) n'a pas de sens côté Firefox et
# fait râler le linter AMO → on le retire du build Firefox.
m.pop('key', None)
m['browser_specific_settings'] = { m['browser_specific_settings'] = {
'gecko': { 'gecko': {
'id': '[email protected]', 'id': '[email protected]',
-25
View File
@@ -2,31 +2,6 @@
"addons": { "addons": {
"[email protected]": { "[email protected]": {
"updates": [ "updates": [
{
"version": "2026.5.50",
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.50/planification-v2026.5.50-firefox.xpi",
"update_hash": "sha256:314ae3cba7e31d1f6eb89c419a666d49d156cd78a83c116de03f5e48b8a094df"
},
{
"version": "2026.5.49",
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.49/planification-v2026.5.49-firefox.xpi",
"update_hash": "sha256:73ac4e44e9fdfb1c3c179c7cc5d2e497b5cf27d207947cc559ab620bddd09fa1"
},
{
"version": "2026.5.48",
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.48/planification-v2026.5.48-firefox.xpi",
"update_hash": "sha256:be687aa2795649d7f1f9883f3384365bd224e0ab94987ff852b0901f5554a51a"
},
{
"version": "2026.5.47",
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.47/planification-v2026.5.47-firefox.xpi",
"update_hash": "sha256:0418062357b7c29bdd7a01b6d6e06eeb20e2ee419c67684a84855862af113cc2"
},
{
"version": "2026.5.46",
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.46/planification-v2026.5.46-firefox.xpi",
"update_hash": "sha256:673dc0527073523750de348414ae1d3b5c0cb42cc8128a7839bbb09bffb6242c"
},
{ {
"version": "2026.5.45", "version": "2026.5.45",
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.45/planification-v2026.5.45-firefox.xpi", "update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.45/planification-v2026.5.45-firefox.xpi",
-3
View File
@@ -1,3 +0,0 @@
@echo off
cd /d "%~dp0"
powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -Command "Get-ChildItem -LiteralPath '%~dp0' -Recurse -File | Unblock-File -ErrorAction SilentlyContinue; & '%~dp0bin\manage.ps1'"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 B

-387
View File
@@ -1,387 +0,0 @@
# Planification - native messaging host // 2026.1.4 - FRS
# Developpe par Quentin (FRS) - DGNSI, Canton de Vaud.
# Licence MIT - voir le fichier LICENSE.
#
# Pont entre l'extension et un dossier reseau partage pour le cache du planning.
#
# Layout: <share>\AAAA\MM\AAAA-MM-JJ.json (une image par jour, par annee/mois).
# Index racine <share>\_index.json : date -> savedAt (stat/list rapides).
# Anciens fichiers plats (<share>\AAAA-MM-JJ.json) restent lus (compat).
#
# Protocole: stdio = prefixe longueur 4 octets (little-endian) + corps JSON UTF-8.
# Boucle jusqu'a EOF (marche pour sendNativeMessage et connectNative).
#
# Actions:
# {action:"version"} -> {ok,version}
# {action:"stat", path, date} -> {ok,exists,savedAt}
# {action:"read", path, date, offset, note} -> {ok,exists,total,offset,chunk,more}
# {action:"save", path, date, json, savedAt, note} -> {ok}
# {action:"list", path} -> {ok,days:[{date,savedAt}]}
# {action:"log", verbose} -> {ok,version,log}
# {action:"logmsg",level,message} -> {ok}
#
# Journal d'activite (active.log) : lignes "JJ/MM/AAAA HH:MM:SS [NIVEAU] message".
# Niveaux: INFO (evenements utiles), DEBUG (detail technique), ERREUR.
# Les messages par defaut de l'hote sont SANS accent (encodage .ps1 PS 5.1) ;
# les "note" envoyees par l'extension transitent en UTF-8 et gardent les accents.
#
# IMPORTANT: rien ne doit atteindre stdout sauf via Write-Message.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$HOST_VERSION = '2026.1.8'
# v2026.1.7 - le host n'est plus persistant a vie : s'il ne recoit AUCUN message
# pendant ce delai, il se ferme proprement (le navigateur le relance a la
# prochaine activite de l'extension). Il lance aussi le tray a la demande.
$IDLE_TIMEOUT_MS = 2 * 60 * 60 * 1000 # 2 heures
$CHUNK_BYTES = 600000
$DATE_RE = '^\d{4}-\d{2}-\d{2}$'
$INDEX_NAME = '_index.json'
$MAX_IN = 67108864
$stdin = [Console]::OpenStandardInput()
$stdout = [Console]::OpenStandardOutput()
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
# host.ps1 vit dans <install>\bin -> la racine de l'install est le parent.
$LOG_DIR = if ($PSScriptRoot) { Split-Path -Parent $PSScriptRoot } else { Join-Path $env:LOCALAPPDATA 'Planification' }
$LOG_FILE = Join-Path $LOG_DIR 'activity.log'
function Format-DateFR([string]$iso) {
if ($iso -match '^(\d{4})-(\d{2})-(\d{2})$') { return "$($Matches[3])/$($Matches[2])/$($Matches[1])" }
return $iso
}
function Write-Activity([string]$level, [string]$msg) {
try {
if ((Test-Path -LiteralPath $LOG_FILE) -and ((Get-Item -LiteralPath $LOG_FILE).Length -gt 1048576)) {
Move-Item -LiteralPath $LOG_FILE -Destination ($LOG_FILE + '.1') -Force
}
$ts = (Get-Date).ToString('dd/MM/yyyy HH:mm:ss')
[System.IO.File]::AppendAllText($LOG_FILE, "$ts [$level] $msg`r`n", $Utf8NoBom)
} catch {}
}
function Read-Exactly([int]$n) {
$buf = New-Object byte[] $n
$off = 0
while ($off -lt $n) {
$r = $stdin.Read($buf, $off, $n - $off)
if ($r -le 0) { return $null }
$off += $r
}
return $buf
}
# v2026.1.7 - lit les 4 octets de prefixe de longueur AVEC un timeout sur le
# 1er octet (= attente du prochain message). Renvoie le byte[], $null sur EOF,
# ou la chaine 'TIMEOUT' si rien n'arrive dans le delai -> le host se ferme.
function Read-LengthOrTimeout([int]$timeoutMs) {
$buf = New-Object byte[] 4
$off = 0
while ($off -lt 4) {
$ar = $stdin.BeginRead($buf, $off, 4 - $off, $null, $null)
if ($off -eq 0) {
if (-not $ar.AsyncWaitHandle.WaitOne($timeoutMs)) { return 'TIMEOUT' }
} else {
[void]$ar.AsyncWaitHandle.WaitOne()
}
$r = $stdin.EndRead($ar)
if ($r -le 0) { return $null }
$off += $r
}
return $buf
}
function Read-Message {
$lenBytes = Read-LengthOrTimeout $IDLE_TIMEOUT_MS
if ($lenBytes -is [string]) { return $lenBytes } # 'TIMEOUT' -> arret propre
if ($null -eq $lenBytes) { return $null }
$len = [BitConverter]::ToInt32($lenBytes, 0)
if ($len -le 0 -or $len -gt $MAX_IN) { return $null }
$payload = Read-Exactly $len
if ($null -eq $payload) { return $null }
$json = [System.Text.Encoding]::UTF8.GetString($payload)
return ($json | ConvertFrom-Json)
}
function Write-Message($obj) {
$json = $obj | ConvertTo-Json -Compress -Depth 50
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
$lenBytes = [BitConverter]::GetBytes([int]$bytes.Length)
$stdout.Write($lenBytes, 0, 4)
$stdout.Write($bytes, 0, $bytes.Length)
$stdout.Flush()
}
function Test-DateStr([string]$d) { return ($d -match $DATE_RE) }
function Resolve-Share([string]$path) {
if ([string]::IsNullOrWhiteSpace($path)) { throw 'empty_path' }
if (-not (Test-Path -LiteralPath $path)) {
New-Item -ItemType Directory -Path $path -Force | Out-Null
}
return (Get-Item -LiteralPath $path).FullName
}
function Get-DayDir([string]$share, [string]$date) {
$yyyy = $date.Substring(0,4)
$mm = $date.Substring(5,2)
return (Join-Path (Join-Path $share $yyyy) $mm)
}
function Get-DayFileWrite([string]$share, [string]$date) {
$dir = Get-DayDir $share $date
if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
return (Join-Path $dir ($date + '.json'))
}
function Get-DayFileRead([string]$share, [string]$date) {
$ym = Join-Path (Get-DayDir $share $date) ($date + '.json')
if (Test-Path -LiteralPath $ym) { return $ym }
$flat = Join-Path $share ($date + '.json')
if (Test-Path -LiteralPath $flat) { return $flat }
return $ym
}
function Read-Index([string]$share) {
$idx = Join-Path $share $INDEX_NAME
if (Test-Path -LiteralPath $idx) {
try { return (Get-Content -LiteralPath $idx -Raw -Encoding UTF8 | ConvertFrom-Json) } catch { return $null }
}
return $null
}
function Update-Index([string]$share, [string]$date, [long]$savedAt) {
$idx = Join-Path $share $INDEX_NAME
# v2026.1.8 - verrou DEDIE a l'index. Plusieurs hotes (plusieurs onglets
# Planification ouverts, ou plusieurs postes) font un read-modify-write
# concurrent sur _index.json ; sans verrou, deux Move-Item se recouvrent et la
# maj d'un hote est perdue (lost update) -> l'index prend du retard sur les
# fichiers-jour -> conflits CAS a repetition ("refusion" en boucle). On
# serialise donc la sequence lecture->modif->ecriture. Vol du verrou perime
# (> 10 s) comme pour les fichiers-jour. Best-effort : si on n'obtient pas le
# verrou, on renonce (la prochaine ecriture reussie remettra l'index a jour).
$lock = $idx + '.lock'
$haveLock = $false
for ($i = 0; $i -lt 20 -and -not $haveLock; $i++) {
try { [System.IO.File]::Open($lock, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write).Close(); $haveLock = $true }
catch {
try {
$age = ((Get-Date) - (Get-Item -LiteralPath $lock).LastWriteTime).TotalSeconds
if ($age -gt 10) { Remove-Item -LiteralPath $lock -Force -ErrorAction SilentlyContinue }
else { Start-Sleep -Milliseconds 50 }
} catch { Start-Sleep -Milliseconds 50 }
}
}
if (-not $haveLock) { Write-Activity 'DEBUG' "Index non mis a jour pour le $(Format-DateFR $date) (verrou index indisponible)"; return }
try {
# lecture DANS le verrou -> on part toujours de la derniere version.
$map = @{}
if (Test-Path -LiteralPath $idx) {
try {
$existing = Get-Content -LiteralPath $idx -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($p in $existing.PSObject.Properties) { $map[$p.Name] = $p.Value }
} catch {}
}
$map[$date] = $savedAt
$tmp = $idx + '.tmp'
[System.IO.File]::WriteAllText($tmp, ($map | ConvertTo-Json -Compress -Depth 5), $Utf8NoBom)
Move-Item -LiteralPath $tmp -Destination $idx -Force
Write-Activity 'DEBUG' "Index du dossier mis a jour pour le $(Format-DateFR $date)"
} finally {
Remove-Item -LiteralPath $lock -Force -ErrorAction SilentlyContinue
}
}
function Get-SavedAt([string]$share, [string]$date, [string]$file) {
$ix = Read-Index $share
if ($ix -and ($ix.PSObject.Properties.Name -contains $date)) {
try { return [long]$ix.$date } catch {}
}
try {
$o = Get-Content -LiteralPath $file -Raw -Encoding UTF8 | ConvertFrom-Json
return [long]$o.savedAt
} catch { return 0 }
}
# v2026.1.7 - le host vient d'etre lance par le navigateur (connexion de
# l'extension). On (re)lance le tray s'il n'est pas deja la, et on horodate
# l'activite (sert aussi de battement de coeur au tray fraichement lance).
# Best-effort, tout en try/catch : ne JAMAIS perturber le protocole stdio.
try {
$trayRunning = $false
try { $tm = [System.Threading.Mutex]::OpenExisting('Local\PlanificationTraySingleton'); $tm.Dispose(); $trayRunning = $true } catch { $trayRunning = $false }
if (-not $trayRunning -and $PSScriptRoot) {
$trayVbs = Join-Path $PSScriptRoot 'tray.vbs'
if (Test-Path -LiteralPath $trayVbs) { Start-Process wscript.exe -ArgumentList "`"$trayVbs`"" }
}
} catch {}
Write-Activity 'INFO' "Host demarre (v$HOST_VERSION) - connexion de l'extension."
while ($true) {
$msg = Read-Message
# v2026.1.7 - inactivite > 2 h : arret propre (le navigateur relancera le host
# a la prochaine activite de l'extension, qui relancera aussi le tray).
if ($msg -is [string] -and $msg -eq 'TIMEOUT') {
Write-Activity 'INFO' 'Aucune communication depuis 2 h - arret du host (relance auto a la prochaine activite).'
break
}
if ($null -eq $msg) { break }
try {
$action = [string]$msg.action
switch ($action) {
'version' {
Write-Message @{ ok = $true; version = $HOST_VERSION }
}
'stat' {
if (-not (Test-DateStr $msg.date)) { Write-Message @{ ok=$false; error='bad_date' }; break }
$share = Resolve-Share $msg.path
$f = Get-DayFileRead $share $msg.date
$fr = Format-DateFR $msg.date
if (Test-Path -LiteralPath $f) {
$sa = Get-SavedAt $share $msg.date $f
Write-Activity 'DEBUG' "Verification du planning du $fr : present dans le dossier"
Write-Message @{ ok=$true; exists=$true; savedAt=$sa }
} else {
Write-Activity 'DEBUG' "Verification du planning du $fr : absent du dossier"
Write-Message @{ ok=$true; exists=$false }
}
}
'read' {
if (-not (Test-DateStr $msg.date)) { Write-Message @{ ok=$false; error='bad_date' }; break }
$share = Resolve-Share $msg.path
$f = Get-DayFileRead $share $msg.date
$fr = Format-DateFR $msg.date
if (-not (Test-Path -LiteralPath $f)) { Write-Message @{ ok=$true; exists=$false }; break }
$offset = 0
if ($null -ne $msg.offset) { $offset = [int]$msg.offset }
$fs = [System.IO.File]::OpenRead($f)
try {
$total = [int]$fs.Length
if ($offset -lt 0) { $offset = 0 }
if ($offset -gt $total) { $offset = $total }
$len = [Math]::Min($CHUNK_BYTES, $total - $offset)
$b64 = ''
if ($len -gt 0) {
$buf = New-Object byte[] $len
$fs.Position = $offset
$got = 0
while ($got -lt $len) {
$r = $fs.Read($buf, $got, $len - $got)
if ($r -le 0) { break }
$got += $r
}
$b64 = [Convert]::ToBase64String($buf, 0, $got)
}
$more = (($offset + $len) -lt $total)
if ($offset -eq 0) {
if ($msg.note) { Write-Activity 'INFO' ([string]$msg.note) }
else { Write-Activity 'INFO' "Lecture du planning du $fr depuis le dossier ($([Math]::Round($total/1024)) Ko)" }
} else {
Write-Activity 'DEBUG' "Transfert d'une tranche du planning du $fr (position $offset)"
}
Write-Message @{ ok=$true; exists=$true; total=$total; offset=$offset; chunk=$b64; more=$more }
} finally { $fs.Close() }
}
'save' {
# v2026.1.6 - FRS : ecriture compare-and-swap + verrou (merge concurrent).
# Si expectedSavedAt fourni, on n'ecrit QUE si le savedAt du dossier est
# toujours celui-la (sinon conflit -> l'extension refusionne et reessaie).
if (-not (Test-DateStr $msg.date)) { Write-Message @{ ok=$false; error='bad_date' }; break }
$share = Resolve-Share $msg.path
$f = Get-DayFileWrite $share $msg.date
$fr = Format-DateFR $msg.date
$json = [string]$msg.json
$hasExpect = ($null -ne $msg.expectedSavedAt)
$expect = if ($hasExpect) { [long]$msg.expectedSavedAt } else { 0 }
$lock = $f + '.lock'
# Verrou inter-machines : creation exclusive ; vol si perime (> 10 s).
$haveLock = $false
try { [System.IO.File]::Open($lock, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write).Close(); $haveLock = $true }
catch {
try {
$age = ((Get-Date) - (Get-Item -LiteralPath $lock).LastWriteTime).TotalSeconds
if ($age -gt 10) {
Remove-Item -LiteralPath $lock -Force -ErrorAction SilentlyContinue
[System.IO.File]::Open($lock, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write).Close(); $haveLock = $true
}
} catch {}
}
if (-not $haveLock) { Write-Message @{ ok=$false; locked=$true }; break }
try {
$proceed = $true
if ($hasExpect) {
$cur = 0
$existing = Get-DayFileRead $share $msg.date
if (Test-Path -LiteralPath $existing) {
try { $o = Get-Content -LiteralPath $existing -Raw -Encoding UTF8 | ConvertFrom-Json; $cur = [long]$o.savedAt } catch { $cur = 0 }
}
if ($cur -ne $expect) {
Write-Activity 'DEBUG' "Conflit d'ecriture sur le $fr (attendu $expect, dossier $cur) - refusion cote extension"
Write-Message @{ ok=$false; conflict=$true; currentSavedAt=$cur }
$proceed = $false
}
}
if ($proceed) {
$tmp = $f + '.tmp'
[System.IO.File]::WriteAllText($tmp, $json, $Utf8NoBom)
Move-Item -LiteralPath $tmp -Destination $f -Force
$savedAt = 0
if ($null -ne $msg.savedAt) { $savedAt = [long]$msg.savedAt }
try { Update-Index $share $msg.date $savedAt } catch {}
if ($msg.note) { Write-Activity 'INFO' ([string]$msg.note) }
else { Write-Activity 'INFO' "Sauvegarde du planning du $fr dans le dossier ($([Math]::Round($json.Length/1024)) Ko)" }
Write-Message @{ ok=$true; savedAt=$savedAt }
}
} finally {
Remove-Item -LiteralPath $lock -Force -ErrorAction SilentlyContinue
}
}
'list' {
$share = Resolve-Share $msg.path
$days = @()
$ix = Read-Index $share
if ($ix) {
foreach ($p in $ix.PSObject.Properties) {
if ($p.Name -match $DATE_RE) { $days += @{ date=$p.Name; savedAt=[long]$p.Value } }
}
}
Write-Activity 'DEBUG' "Liste des plannings disponibles dans le dossier ($($days.Count) jour(s))"
Write-Message @{ ok=$true; days=@($days) }
}
'log' {
$txt = ''
if (Test-Path -LiteralPath $LOG_FILE) {
try {
$all = Get-Content -LiteralPath $LOG_FILE -Encoding UTF8 -ErrorAction SilentlyContinue
if ($all) { $txt = ($all | Select-Object -Last 400) -join "`n" }
} catch {}
}
Write-Message @{ ok=$true; version=$HOST_VERSION; log=$txt }
}
'logmsg' {
$lvl = if ($msg.level) { [string]$msg.level } else { 'INFO' }
if ($msg.message) { Write-Activity $lvl ([string]$msg.message) }
Write-Message @{ ok=$true }
}
default {
Write-Message @{ ok=$false; error=('unknown_action:' + $action) }
}
}
} catch {
try { Write-Activity 'ERREUR' "$action : $($_.Exception.Message)" } catch {}
Write-Message @{ ok=$false; error=([string]$_.Exception.Message) }
}
}
-231
View File
@@ -1,231 +0,0 @@
# Planification - gestionnaire graphique du helper de sync. // 2026.1.5 - FRS
# Developpe par Quentin (FRS) - DGNSI, Canton de Vaud.
# Licence MIT - voir le fichier LICENSE.
#
# Fenetre avec : Installer / Mettre a jour, Reparer, Desinstaller.
# Per-user (HKCU), sans admin, sans signature. Lance par Gerer.cmd.
#
# Structure du paquet : Gerer.cmd (racine), bin\ (manage/host/tray/.vbs),
# assets\ (icon.png). Structure installee (%LOCALAPPDATA%\Planification) :
# bin\ (host.ps1, host.bat, tray.ps1, tray.vbs), assets\ (icon.png),
# manifests\ (nm-*.json), activity.log (a la racine).
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$HostName = 'ch.netaplaid.planification'
$ExtIdChromium = 'hlkepgcngeckdbapafkheondiagjgphc'
$ExtIdFirefox = '[email protected]'
$ScriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path } # paquet\bin
$PkgRoot = Split-Path -Parent $ScriptDir # paquet\
$InstallDir = Join-Path $env:LOCALAPPDATA 'Planification'
$InstallBin = Join-Path $InstallDir 'bin'
$InstallAssets = Join-Path $InstallDir 'assets'
$InstallManifests = Join-Path $InstallDir 'manifests'
function Get-HostVersion([string]$psFile) {
if (Test-Path -LiteralPath $psFile) {
$m = Select-String -LiteralPath $psFile -Pattern "HOST_VERSION\s*=\s*'([^']*)'" -ErrorAction SilentlyContinue
if ($m) { return $m.Matches[0].Groups[1].Value }
}
return $null
}
function Stop-Tray {
Get-CimInstance Win32_Process -Filter "Name='wscript.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.CommandLine -like '*tray.vbs*' } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.CommandLine -like '*tray.ps1*' } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
}
function Install-Host {
Stop-Tray
foreach ($d in @($InstallBin, $InstallAssets, $InstallManifests)) { New-Item -ItemType Directory -Path $d -Force | Out-Null }
# Scripts -> bin\ (ecriture fraiche = pas de Mark of the Web).
# v2026.1.7 - on copie AUSSI manage.ps1 dans l'install pour que le gestionnaire
# (Reparer / Desinstaller) reste accessible via le Menu Demarrer meme apres
# suppression du dossier du paquet.
foreach ($name in @('host.ps1', 'tray.ps1', 'manage.ps1')) {
$src = Join-Path $ScriptDir $name
if (Test-Path -LiteralPath $src) {
Get-Content -LiteralPath $src -Raw | Set-Content -LiteralPath (Join-Path $InstallBin $name) -Encoding UTF8
Unblock-File -LiteralPath (Join-Path $InstallBin $name) -ErrorAction SilentlyContinue
}
}
$srcVbs = Join-Path $ScriptDir 'tray.vbs'
if (Test-Path -LiteralPath $srcVbs) {
Get-Content -LiteralPath $srcVbs -Raw | Set-Content -LiteralPath (Join-Path $InstallBin 'tray.vbs') -Encoding ASCII
Unblock-File -LiteralPath (Join-Path $InstallBin 'tray.vbs') -ErrorAction SilentlyContinue
}
# Icone -> assets\ (binaire : Copy-Item).
$srcIcon = Join-Path $PkgRoot 'assets\icon.png'
if (Test-Path -LiteralPath $srcIcon) { Copy-Item -LiteralPath $srcIcon -Destination (Join-Path $InstallAssets 'icon.png') -Force }
# Wrapper .bat dans bin\ (host.ps1 est dans le meme dossier -> %~dp0host.ps1).
$dstBat = Join-Path $InstallBin 'host.bat'
Set-Content -LiteralPath $dstBat -Encoding ASCII -Value @(
'@echo off',
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0host.ps1" %*'
)
# Manifests natifs -> manifests\ (path = bin\host.bat).
$manChromium = Join-Path $InstallManifests 'nm-chromium.json'
$manFirefox = Join-Path $InstallManifests 'nm-firefox.json'
(([ordered]@{ name=$HostName; description='Planification cache sync host'; path=$dstBat; type='stdio'; allowed_origins=@("chrome-extension://$ExtIdChromium/") }) | ConvertTo-Json -Depth 5) | Set-Content -LiteralPath $manChromium -Encoding UTF8
(([ordered]@{ name=$HostName; description='Planification cache sync host'; path=$dstBat; type='stdio'; allowed_extensions=@($ExtIdFirefox) }) | ConvertTo-Json -Depth 5) | Set-Content -LiteralPath $manFirefox -Encoding UTF8
function Reg([string]$base, [string]$manifest) {
$key = Join-Path $base $HostName
New-Item -Path $key -Force | Out-Null
Set-Item -Path $key -Value $manifest
}
Reg 'HKCU:\Software\Google\Chrome\NativeMessagingHosts' $manChromium
Reg 'HKCU:\Software\Microsoft\Edge\NativeMessagingHosts' $manChromium
Reg 'HKCU:\Software\Mozilla\NativeMessagingHosts' $manFirefox
# v2026.1.7 - le tray ne demarre PLUS au login et n'est PLUS lance a l'install.
# Il est demarre A LA DEMANDE par host.ps1 quand l'extension se connecte, et
# s'auto-ferme apres 2 h sans activite. On retire l'ancien autostart s'il
# existe (installs precedentes) pour que "de base = eteint".
Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'PlanificationTray' -ErrorAction SilentlyContinue
# v2026.1.7 - Menu Demarrer : raccourci vers le gestionnaire installe
# (Installer / Reparer / Desinstaller), retrouvable sans le dossier du paquet.
# 1) lanceur .vbs (fenetre PowerShell masquee) du manage.ps1 installe.
$dstGererVbs = Join-Path $InstallBin 'gerer.vbs'
Set-Content -LiteralPath $dstGererVbs -Encoding ASCII -Value @(
"' Lance le gestionnaire Planification (fenetre PowerShell masquee).",
'Set sh = CreateObject("WScript.Shell")',
'Set fso = CreateObject("Scripting.FileSystemObject")',
'dir = fso.GetParentFolderName(WScript.ScriptFullName)',
'sh.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File """ & dir & "\manage.ps1""", 0, False'
)
# 2) icone .ico (depuis icon.png) pour le raccourci, best-effort.
$icoPath = Join-Path $InstallAssets 'icon.ico'
try {
$pngForIco = Join-Path $InstallAssets 'icon.png'
if (Test-Path -LiteralPath $pngForIco) {
Add-Type -AssemblyName System.Drawing
$bmp = New-Object System.Drawing.Bitmap($pngForIco)
$ico = [System.Drawing.Icon]::FromHandle($bmp.GetHicon())
$fs = [System.IO.File]::Create($icoPath)
$ico.Save($fs); $fs.Close()
}
} catch { $icoPath = $null }
# 3) raccourcis .lnk dans le Menu Demarrer (deux entrees) :
# - "gerer" -> manage.ps1 (installer / reparer / desinstaller)
# - "demarrer" -> tray.vbs (relance/affiche la sync a la demande)
$dstTrayVbs = Join-Path $InstallBin 'tray.vbs'
try {
$startDir = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs'
New-Item -ItemType Directory -Path $startDir -Force | Out-Null
# Nettoyage de l'ancien nom de raccourci (installs precedentes).
Remove-Item -LiteralPath (Join-Path $startDir 'Planification - synchronisation.lnk') -Force -ErrorAction SilentlyContinue
$ws = New-Object -ComObject WScript.Shell
function New-Lnk([string]$name, [string]$targetVbs, [string]$desc) {
$sc = $ws.CreateShortcut((Join-Path $startDir $name))
$sc.TargetPath = 'wscript.exe'
$sc.Arguments = '"' + $targetVbs + '"'
$sc.WorkingDirectory = $InstallBin
$sc.Description = $desc
if ($icoPath -and (Test-Path -LiteralPath $icoPath)) { $sc.IconLocation = "$icoPath,0" }
$sc.Save()
}
New-Lnk 'Planification - gerer.lnk' $dstGererVbs 'Gerer la synchronisation Planification (installer / reparer / desinstaller)'
New-Lnk 'Planification - demarrer la synchro.lnk' $dstTrayVbs 'Demarrer / afficher la synchronisation Planification'
} catch {}
}
function Uninstall-Host {
Stop-Tray
Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'PlanificationTray' -ErrorAction SilentlyContinue
foreach ($base in @(
'HKCU:\Software\Google\Chrome\NativeMessagingHosts',
'HKCU:\Software\Microsoft\Edge\NativeMessagingHosts',
'HKCU:\Software\Mozilla\NativeMessagingHosts')) {
Remove-Item -Path (Join-Path $base $HostName) -Recurse -Force -ErrorAction SilentlyContinue
}
if (Test-Path -LiteralPath $InstallDir) { Remove-Item -LiteralPath $InstallDir -Recurse -Force -ErrorAction SilentlyContinue }
# v2026.1.7 - retirer les raccourcis du Menu Demarrer (hors InstallDir).
$startDir = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs'
foreach ($lnk in @('Planification - synchronisation.lnk', 'Planification - gerer.lnk', 'Planification - demarrer la synchro.lnk')) {
Remove-Item -LiteralPath (Join-Path $startDir $lnk) -Force -ErrorAction SilentlyContinue
}
}
# ----------------------------- Interface -----------------------------------
$available = Get-HostVersion (Join-Path $ScriptDir 'host.ps1')
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Planification - synchronisation'
$form.Size = New-Object System.Drawing.Size(460, 320)
$form.StartPosition = 'CenterScreen'
$form.FormBorderStyle = 'FixedSingle'
$form.MaximizeBox = $false
try {
$ic = Join-Path $PkgRoot 'assets\icon.png'
if (Test-Path -LiteralPath $ic) { $form.Icon = [System.Drawing.Icon]::FromHandle((New-Object System.Drawing.Bitmap($ic)).GetHicon()) }
} catch {}
$title = New-Object System.Windows.Forms.Label
$title.Text = 'Synchronisation du planning entre postes'
$title.Font = New-Object System.Drawing.Font('Segoe UI', 11, [System.Drawing.FontStyle]::Bold)
$title.AutoSize = $true
$title.Location = New-Object System.Drawing.Point(18, 16)
$form.Controls.Add($title)
$status = New-Object System.Windows.Forms.Label
$status.AutoSize = $false
$status.Size = New-Object System.Drawing.Size(420, 40)
$status.Location = New-Object System.Drawing.Point(18, 48)
$form.Controls.Add($status)
function Refresh-Status {
$installed = Get-HostVersion (Join-Path $InstallBin 'host.ps1')
if ($installed) { $status.Text = "Installe : version $installed`r`nDisponible (ce dossier) : version $available" }
else { $status.Text = "Non installe sur ce poste.`r`nDisponible (ce dossier) : version $available" }
}
Refresh-Status
$result = New-Object System.Windows.Forms.Label
$result.AutoSize = $false
$result.Size = New-Object System.Drawing.Size(420, 30)
$result.Location = New-Object System.Drawing.Point(18, 248)
$result.ForeColor = [System.Drawing.Color]::DarkGreen
function Add-Button([string]$text, [int]$y, [scriptblock]$action) {
$b = New-Object System.Windows.Forms.Button
$b.Text = $text
$b.Size = New-Object System.Drawing.Size(420, 38)
$b.Location = New-Object System.Drawing.Point(18, $y)
$b.Font = New-Object System.Drawing.Font('Segoe UI', 10)
$b.add_Click($action)
$form.Controls.Add($b)
}
Add-Button 'Installer / Mettre a jour' 100 {
try { Install-Host; $result.ForeColor=[System.Drawing.Color]::DarkGreen; $result.Text='Installation / mise a jour terminee.'; Refresh-Status }
catch { $result.ForeColor=[System.Drawing.Color]::Firebrick; $result.Text=('Erreur : ' + $_.Exception.Message) }
}
Add-Button 'Reparer (reinstaller proprement)' 144 {
try { Install-Host; $result.ForeColor=[System.Drawing.Color]::DarkGreen; $result.Text='Reparation terminee.'; Refresh-Status }
catch { $result.ForeColor=[System.Drawing.Color]::Firebrick; $result.Text=('Erreur : ' + $_.Exception.Message) }
}
Add-Button 'Desinstaller' 188 {
$r = [System.Windows.Forms.MessageBox]::Show('Desinstaller le helper de synchronisation ? (le cache du dossier partage n''est pas touche)', 'Confirmer', 'YesNo', 'Question')
if ($r -eq 'Yes') {
try { Uninstall-Host; $result.ForeColor=[System.Drawing.Color]::DarkGreen; $result.Text='Desinstalle. Pense a retirer l''extension du navigateur.'; Refresh-Status }
catch { $result.ForeColor=[System.Drawing.Color]::Firebrick; $result.Text=('Erreur : ' + $_.Exception.Message) }
}
}
$form.Controls.Add($result)
[System.Windows.Forms.Application]::EnableVisualStyles()
[void]$form.ShowDialog()
-225
View File
@@ -1,225 +0,0 @@
# Planification - icone barre des taches (best-effort, optionnel) // 2026.1.4 - FRS
# Developpe par Quentin (FRS) - DGNSI, Canton de Vaud.
# Licence MIT - voir le fichier LICENSE.
#
# Indique que le helper de sync est installe. Clic / menu -> fenetre du journal
# (aspect application : journal colore par niveau, auto-rafraichi, bouton
# "Tout afficher"). Lance par tray.vbs (fenetre masquee), au login.
$ErrorActionPreference = 'SilentlyContinue'
# Instance unique.
$mutex = New-Object System.Threading.Mutex($false, 'Local\PlanificationTraySingleton')
if (-not $mutex.WaitOne(0)) { exit }
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# tray.ps1 vit dans <install>\bin -> racine = parent ; assets et log a la racine.
$root = if ($PSScriptRoot) { Split-Path -Parent $PSScriptRoot } else { Join-Path $env:LOCALAPPDATA 'Planification' }
$logFile = Join-Path $root 'activity.log'
$iconPng = Join-Path (Join-Path $root 'assets') 'icon.png'
function Get-TrayIcon {
try {
if (Test-Path -LiteralPath $iconPng) {
$bmp = New-Object System.Drawing.Bitmap($iconPng)
return [System.Drawing.Icon]::FromHandle($bmp.GetHicon())
}
} catch {}
return [System.Drawing.SystemIcons]::Information
}
$script:logForm = $null
$script:logRtb = $null
$script:logChk = $null
$script:logSig = ''
$colInfo = [System.Drawing.Color]::FromArgb(40, 70, 120)
$colDebug = [System.Drawing.Color]::FromArgb(150, 150, 150)
$colErr = [System.Drawing.Color]::FromArgb(200, 40, 40)
$colTs = [System.Drawing.Color]::FromArgb(120, 120, 120)
function Update-LogText {
if (-not $script:logRtb -or $script:logRtb.IsDisposed) { return }
$lines = @()
if (Test-Path -LiteralPath $logFile) {
try {
$all = Get-Content -LiteralPath $logFile -Encoding UTF8 -ErrorAction SilentlyContinue
if ($all) {
if ($script:logChk -and -not $script:logChk.Checked) {
$all = $all | Where-Object { $_ -notmatch '\[DEBUG\]' }
}
$lines = @($all | Select-Object -Last 500)
}
} catch {}
}
$sig = ($lines -join "`n")
if ($sig -eq $script:logSig) { return }
$script:logSig = $sig
$rtb = $script:logRtb
$rtb.SuspendLayout()
$rtb.Clear()
if ($lines.Count -eq 0) {
$rtb.SelectionColor = $colDebug
$rtb.AppendText("(aucune activite de synchronisation pour le moment)")
} else {
foreach ($line in $lines) {
$col = $colInfo
if ($line -match '\[ERREUR\]') { $col = $colErr }
elseif ($line -match '\[DEBUG\]') { $col = $colDebug }
if ($line -match '^(\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2})(.*)$') {
$rtb.SelectionStart = $rtb.TextLength; $rtb.SelectionColor = $colTs
$rtb.AppendText($Matches[1])
$rtb.SelectionStart = $rtb.TextLength; $rtb.SelectionColor = $col
$rtb.AppendText($Matches[2] + "`n")
} else {
$rtb.SelectionStart = $rtb.TextLength; $rtb.SelectionColor = $col
$rtb.AppendText($line + "`n")
}
}
}
$rtb.ResumeLayout()
$rtb.SelectionStart = $rtb.TextLength; $rtb.ScrollToCaret()
}
function Show-LogWindow {
if ($script:logForm -and -not $script:logForm.IsDisposed) {
$script:logForm.WindowState = 'Normal'; $script:logForm.Activate(); return
}
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Planification - journal de synchronisation'
$form.Size = New-Object System.Drawing.Size(760, 540)
$form.StartPosition = 'CenterScreen'
$form.BackColor = [System.Drawing.Color]::FromArgb(245, 246, 248)
try { $form.Icon = Get-TrayIcon } catch {}
# 1) zone de texte (Fill) ajoutee en premier
$rtb = New-Object System.Windows.Forms.RichTextBox
$rtb.Dock = 'Fill'
$rtb.ReadOnly = $true
$rtb.BorderStyle = 'None'
$rtb.BackColor = [System.Drawing.Color]::White
$rtb.Font = New-Object System.Drawing.Font('Consolas', 9.5)
$rtb.DetectUrls = $false
$form.Controls.Add($rtb)
# 2) barre d'outils claire avec le BOUTON bascule "Tout afficher" bien visible
$bar = New-Object System.Windows.Forms.Panel
$bar.Dock = 'Top'; $bar.Height = 40
$bar.BackColor = [System.Drawing.Color]::FromArgb(238, 240, 243)
$chk = New-Object System.Windows.Forms.CheckBox
$chk.Appearance = 'Button'
$chk.Text = 'Tout afficher (mode detaille)'
$chk.TextAlign = 'MiddleCenter'
$chk.AutoSize = $false
$chk.Size = New-Object System.Drawing.Size(200, 28)
$chk.Location = New-Object System.Drawing.Point(10, 6)
$chk.FlatStyle = 'System'
$chk.add_CheckedChanged({ $script:logSig = '~'; Update-LogText })
$bar.Controls.Add($chk)
$hint = New-Object System.Windows.Forms.Label
$hint.Text = 'Par defaut : l''essentiel. Activez pour voir tout le detail technique.'
$hint.ForeColor = [System.Drawing.Color]::FromArgb(90, 90, 90)
$hint.AutoSize = $true
$hint.Location = New-Object System.Drawing.Point(220, 12)
$bar.Controls.Add($hint)
$form.Controls.Add($bar)
# 3) bandeau titre (bleu) ajoute en dernier -> tout en haut
$hdrBar = New-Object System.Windows.Forms.Panel
$hdrBar.Dock = 'Top'; $hdrBar.Height = 32
$hdrBar.BackColor = [System.Drawing.Color]::FromArgb(40, 70, 120)
$hdr = New-Object System.Windows.Forms.Label
$hdr.Text = 'Activite de synchronisation - Planification'
$hdr.ForeColor = [System.Drawing.Color]::White
$hdr.Font = New-Object System.Drawing.Font('Segoe UI', 10, [System.Drawing.FontStyle]::Bold)
$hdr.AutoSize = $true
$hdr.Location = New-Object System.Drawing.Point(10, 7)
$hdrBar.Controls.Add($hdr)
$form.Controls.Add($hdrBar)
$script:logForm = $form
$script:logRtb = $rtb
$script:logChk = $chk
$script:logSig = ''
Update-LogText
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 2000
$timer.add_Tick({ Update-LogText })
$timer.Start()
$form.add_FormClosed({ $timer.Stop(); $timer.Dispose() })
$form.Show(); $form.Activate()
}
$ni = New-Object System.Windows.Forms.NotifyIcon
$ni.Icon = Get-TrayIcon
$ni.Text = 'Planification - synchronisation active'
$ni.Visible = $true
# v2026.1.8 - "toujours present si actif" : le NotifyIcon disparait quand
# explorer.exe redemarre (Windows rediffuse alors le message "TaskbarCreated").
# Sans le traiter, l'icone ne revient JAMAIS tant que le tray tourne. On ecoute
# ce message via une petite fenetre native et on re-affiche l'icone.
try {
Add-Type -ReferencedAssemblies System.Windows.Forms -TypeDefinition @'
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
public class PlanifTrayWatcher : NativeWindow {
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern uint RegisterWindowMessage(string lpString);
public static readonly uint TaskbarCreated = RegisterWindowMessage("TaskbarCreated");
private NotifyIcon _icon;
public PlanifTrayWatcher(NotifyIcon icon) {
_icon = icon;
this.CreateHandle(new CreateParams());
}
protected override void WndProc(ref Message m) {
if (m.Msg == TaskbarCreated && _icon != null) {
try { _icon.Visible = false; _icon.Visible = true; } catch {}
}
base.WndProc(ref m);
}
}
'@ -ErrorAction Stop
$script:trayWatcher = New-Object PlanifTrayWatcher($ni)
} catch {}
$menu = New-Object System.Windows.Forms.ContextMenuStrip
$miLog = $menu.Items.Add('Voir le journal de synchronisation')
$miLog.add_Click({ Show-LogWindow })
$miQuit = $menu.Items.Add('Quitter')
$miQuit.add_Click({ $ni.Visible = $false; $ni.Dispose(); [System.Windows.Forms.Application]::Exit() })
$ni.ContextMenuStrip = $menu
$ni.add_MouseClick({ param($s, $e) if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Left) { Show-LogWindow } })
# v2026.1.7 - auto-extinction apres 2 h SANS activite de synchronisation. On se
# base sur la date de derniere ecriture du journal (le host y ecrit a chaque
# operation). Le navigateur relancera le host a la prochaine activite, qui
# relancera ce tray. -> "de base eteint, s'active a l'usage, se ferme au repos".
$IDLE_LIMIT_MIN = 120
$idleTimer = New-Object System.Windows.Forms.Timer
$idleTimer.Interval = 300000 # verifie toutes les 5 min
$idleTimer.add_Tick({
try {
$age = $null
if (Test-Path -LiteralPath $logFile) {
$age = ((Get-Date) - (Get-Item -LiteralPath $logFile).LastWriteTime).TotalMinutes
}
# Pas de journal, ou journal inactif depuis >= 2 h -> on s'eteint.
if (($null -eq $age) -or ($age -ge $IDLE_LIMIT_MIN)) {
$ni.Visible = $false; $ni.Dispose()
[System.Windows.Forms.Application]::Exit()
}
} catch {}
})
$idleTimer.Start()
[System.Windows.Forms.Application]::EnableVisualStyles()
[System.Windows.Forms.Application]::Run()
$idleTimer.Stop()
$ni.Dispose()
-5
View File
@@ -1,5 +0,0 @@
' Lance tray.ps1 en arriere-plan, fenetre PowerShell masquee (pas de console).
Set sh = CreateObject("WScript.Shell")
Set fso = CreateObject("Scripting.FileSystemObject")
dir = fso.GetParentFolderName(WScript.ScriptFullName)
sh.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File """ & dir & "\tray.ps1""", 0, False
+81 -160
View File
@@ -4,22 +4,15 @@
* Service worker (Manifest V3) : récupération session EV, fetch planning XML, * Service worker (Manifest V3) : récupération session EV, fetch planning XML,
* fetch fiches détaillées, gestion cache. * fetch fiches détaillées, gestion cache.
* *
* Copyright (c) 2026 Quentin * Copyright (c) 2026 Quentin Rouiller
* Licensed under the MIT License — see LICENSE file in the project root. * Licensed under the MIT License — see LICENSE file in the project root.
* *
* @author Quentin * @author Quentin Rouiller
* @repository https://gitea.netaplaid.ch/FroSteel/Planification * @repository https://gitea.netaplaid.ch/FroSteel/Planification
*
* Développé par Quentin (FRS) — DGNSI, Canton de Vaud. Licence MIT.
*/ */
// background.js — Service worker (Manifest V3) — v4 // background.js — Service worker (Manifest V3) — v4
// //
// v2026.5.46 — fiabilisation de la détection de session (faux
// « déconnecté ») : le cookie PHPSESSID suffit sans onglet EV ouvert
// (permission cookies), contre-vérification session_time avant de classer
// un statut HTTP en « session expirée », cache session réactivé (200 ms).
//
// Rôles : // Rôles :
// 1. Au clic sur l'icône : ouvrir le viewer // 1. Au clic sur l'icône : ouvrir le viewer
// 2. Répondre aux messages du viewer : // 2. Répondre aux messages du viewer :
@@ -262,7 +255,7 @@ chrome.action.onClicked.addListener(async () => {
* dans l'URL des onglets historiques après un relog. Sinon, fallback sur * dans l'URL des onglets historiques après un relog. Sinon, fallback sur
* l'ancienne logique URL (compat sans la permission). * l'ancienne logique URL (compat sans la permission).
* *
* @author Quentin * @author Quentin Rouiller
*/ */
async function _hasCookiesPermission() { async function _hasCookiesPermission() {
return new Promise(resolve => { return new Promise(resolve => {
@@ -313,12 +306,11 @@ async function _readPhpsessidFromCookie(origin) {
let _sessionCache = null; let _sessionCache = null;
let _sessionCacheTs = 0; let _sessionCacheTs = 0;
// v2026.5.46 — cache anti-rafale RÉACTIVÉ (200 ms). Le soupçon qui // Cache désactivé temporairement (TTL=0) — soupçon que la durée de
// l'avait fait désactiver (« page de login ~8 Ko au fetch fiche ») venait du // fenêtre stale puisse causer une réponse "page de login" (~8 Ko) lors
// PHPSESSID périmé lu dans l'URL des onglets historiques — résolu depuis par // d'un fetch fiche. À remettre à 200 après confirmation que ce n'était
// la lecture du cookie (issue #4). 200 ms de fenêtre stale sont inoffensifs // pas le souci.
// et évitent le bombardement tabs.query+cookies.get pendant la phase A. const _SESSION_CACHE_TTL_MS = 0;
const _SESSION_CACHE_TTL_MS = 200;
let _lastSessionSignature = null; let _lastSessionSignature = null;
function _invalidateSessionCache() { function _invalidateSessionCache() {
@@ -330,16 +322,9 @@ async function _findEasyVistaSessionRaw() {
const origins = await getEvOrigins(); const origins = await getEvOrigins();
const hasCookies = await _hasCookiesPermission(); const hasCookies = await _hasCookiesPermission();
// v2026.5.46 — avec la permission cookies, le cookie PHPSESSID
// HttpOnly SUFFIT : les fetch partent avec credentials:include, un onglet
// EV ouvert n'est PAS nécessaire. Avant, fermer l'onglet EV faisait
// perdre la session à l'extension (bannière « déconnecté » à tort) alors
// que la session serveur vivait toujours. On PRÉFÈRE toujours une origine
// avec onglet ouvert (fraîcheur SSO), mais on retombe sur « cookie seul ».
let cookieOnly = null;
for (const origin of origins) { for (const origin of origins) {
const tabs = await chrome.tabs.query({ url: origin + "/*" }); const tabs = await chrome.tabs.query({ url: origin + "/*" });
if (!tabs.length) continue;
let phpsessid = null; let phpsessid = null;
let expirationDate = null; let expirationDate = null;
@@ -354,13 +339,6 @@ async function _findEasyVistaSessionRaw() {
} }
} }
if (!tabs.length) {
if (phpsessid && !cookieOnly) {
cookieOnly = { phpsessid, origin, tabId: null, source: "cookie", expirationDate };
}
continue;
}
if (!phpsessid) { if (!phpsessid) {
for (const tab of tabs) { for (const tab of tabs) {
const m = (tab.url || "").match(/[?&]PHPSESSID=([a-zA-Z0-9]+)/); const m = (tab.url || "").match(/[?&]PHPSESSID=([a-zA-Z0-9]+)/);
@@ -376,7 +354,7 @@ async function _findEasyVistaSessionRaw() {
return { phpsessid, origin, tabId: tabs[0].id, source, expirationDate }; return { phpsessid, origin, tabId: tabs[0].id, source, expirationDate };
} }
} }
return cookieOnly; return null;
} }
async function findEasyVistaSession() { async function findEasyVistaSession() {
@@ -400,11 +378,11 @@ async function findEasyVistaSession() {
if (sig !== _lastSessionSignature) { if (sig !== _lastSessionSignature) {
if (result) { if (result) {
LOG.warn("session", LOG.warn("session",
`PHPSESSID via ${result.source.toUpperCase()} (${result.phpsessid.slice(0, 8)}…) sur ${result.origin}`, `🔑 PHPSESSID via ${result.source.toUpperCase()} (${result.phpsessid.slice(0, 8)}…) sur ${result.origin}`,
{ source: result.source, origin: result.origin, { source: result.source, origin: result.origin,
expirationDate: result.expirationDate || null }); expirationDate: result.expirationDate || null });
} else { } else {
LOG.warn("session", "Aucun PHPSESSID disponible (pas d'onglet EZV ouvert ou cookie absent)"); LOG.warn("session", "❌ Aucun PHPSESSID disponible (pas d'onglet EZV ouvert ou cookie absent)");
} }
_lastSessionSignature = sig; _lastSessionSignature = sig;
} }
@@ -423,7 +401,7 @@ async function findEasyVistaSession() {
* Ce n'est PAS le HTML de la page Planning — le serveur ne rend pas les données * 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. * dans le HTML, elles arrivent via cet endpoint AJAX.
* *
* @author Quentin * @author Quentin Rouiller
*/ */
async function fetchPlanningXml(origin, phpsessid, unixDate) { async function fetchPlanningXml(origin, phpsessid, unixDate) {
// v2026.5.41 : groupId vient de admin_config (défaut SI-CSS). // v2026.5.41 : groupId vient de admin_config (défaut SI-CSS).
@@ -462,7 +440,7 @@ async function fetchPlanningXml(origin, phpsessid, unixDate) {
// v4.2 : classifier l'erreur HTTP pour que le viewer affiche le bon // v4.2 : classifier l'erreur HTTP pour que le viewer affiche le bon
// écran (session expirée vs EV inaccessible). // écran (session expirée vs EV inaccessible).
const err = new Error("HTTP " + r.status); const err = new Error("HTTP " + r.status);
err.kind = await classifyHttpStatusVerified(r.status, origin, phpsessid); err.kind = classifyHttpStatus(r.status);
err.status = r.status; err.status = r.status;
throw err; throw err;
} }
@@ -531,47 +509,6 @@ function classifyHttpStatus(status) {
return "ev_unreachable"; return "ev_unreachable";
} }
// v2026.5.46 — CONTRE-VÉRIFICATION avant de déclarer « session
// expirée » sur un simple statut HTTP. EV renvoie historiquement 401/403/404
// quand le PHPSESSID est mort — mais ces statuts arrivent AUSSI sur un hoquet
// ponctuel (proxy, maintenance, endpoint) alors que la session est
// parfaitement vivante → bannière « déconnecté » à tort. On demande son avis
// à l'endpoint d'autorité timeout_ajax session_time (celui qu'EV utilise pour
// sa propre modal ; il ne prolonge pas la session) : s'il répond un temps
// restant > 0, la session VIT et l'erreur est reclassée « ev_unreachable »
// (transitoire). Sonde cachée 5 s : une rafale d'échecs ne sonde qu'une fois.
let _sessProbeCache = { ts: 0, verdict: null };
async function _probeSessionAlive(origin, phpsessid) {
const now = Date.now();
if (now - _sessProbeCache.ts < 5000 && _sessProbeCache.verdict !== null) {
return _sessProbeCache.verdict;
}
let verdict;
try {
const ms = await fetchSessionTimeRemaining(origin, phpsessid);
verdict = ms > 1000 ? "alive" : "dead";
} catch (e) {
verdict = (e && e.message === "session_expired") ? "dead" : "unknown";
}
_sessProbeCache = { ts: now, verdict };
return verdict;
}
async function classifyHttpStatusVerified(status, origin, phpsessid) {
const kind = classifyHttpStatus(status);
if (kind !== "session_expired") return kind;
const verdict = await _probeSessionAlive(origin, phpsessid);
if (verdict === "alive") {
LOG.warn("session", "HTTP " + status + " mais session_time confirme la session vivante → reclassé ev_unreachable (pas d'expiration)");
return "ev_unreachable";
}
if (verdict === "unknown") {
LOG.warn("session", "HTTP " + status + " et sonde session_time injoignable → reclassé ev_unreachable (prudence, pas d'expiration sur simple statut)");
return "ev_unreachable";
}
return "session_expired";
}
/** /**
* Fetch planning_xhr_2.php?id=ACTIONID pour UNE intervention. * Fetch planning_xhr_2.php?id=ACTIONID pour UNE intervention.
* Retourne ~400 octets au format custom : * Retourne ~400 octets au format custom :
@@ -582,7 +519,7 @@ async function fetchXhr2(origin, phpsessid, actionId) {
const r = await evFetch(url, origin); const r = await evFetch(url, origin);
if (!r.ok) { if (!r.ok) {
const err = new Error("HTTP " + r.status); const err = new Error("HTTP " + r.status);
err.kind = await classifyHttpStatusVerified(r.status, origin, phpsessid); err.kind = classifyHttpStatus(r.status);
err.status = r.status; err.status = r.status;
throw err; throw err;
} }
@@ -604,7 +541,7 @@ async function fetchFicheHtml(origin, phpsessid, formLink) {
const r = await evFetch(url, origin); const r = await evFetch(url, origin);
if (!r.ok) { if (!r.ok) {
const err = new Error("HTTP " + r.status); const err = new Error("HTTP " + r.status);
err.kind = await classifyHttpStatusVerified(r.status, origin, phpsessid); err.kind = classifyHttpStatus(r.status);
err.status = r.status; err.status = r.status;
throw err; throw err;
} }
@@ -612,24 +549,24 @@ async function fetchFicheHtml(origin, phpsessid, formLink) {
// v2026.5.38 : on log seulement les retries (utile en cas de pb SSO), // v2026.5.38 : on log seulement les retries (utile en cas de pb SSO),
// pas chaque tentative normale qui réussit du premier coup. // pas chaque tentative normale qui réussit du premier coup.
if (attempt > 1) { if (attempt > 1) {
LOG.info("bg", `fiche tentative ${attempt}/${MAX_RETRIES} (taille = ${html.length})`); console.log(`[bg] fiche tentative ${attempt}/${MAX_RETRIES} (taille = ${html.length})`);
} }
// Si réponse clairement une redirection courte → login expiré, inutile de retry // Si réponse clairement une redirection courte → login expiré, inutile de retry
if (html.length < 500) { if (html.length < 500) {
LOG.warn("bg", "fiche très courte, contenu =", JSON.stringify(html)); console.warn("[bg] ⚠ fiche très courte, contenu =", JSON.stringify(html));
return html; return html;
} }
// Si taille suspecte (< 20 Ko), probable page intermédiaire SSO : retry // Si taille suspecte (< 20 Ko), probable page intermédiaire SSO : retry
if (html.length < MIN_VALID_SIZE && attempt < MAX_RETRIES) { if (html.length < MIN_VALID_SIZE && attempt < MAX_RETRIES) {
LOG.warn("bg", `fiche anormalement petite (${html.length} octets), retry dans ${RETRY_DELAY_MS} ms...`); console.warn(`[bg] ⚠ fiche anormalement petite (${html.length} octets), retry dans ${RETRY_DELAY_MS} ms...`);
await new Promise(res => setTimeout(res, RETRY_DELAY_MS)); await new Promise(res => setTimeout(res, RETRY_DELAY_MS));
continue; continue;
} }
// on signale au foreground si la dernière réponse est tronquée pour // on signale au foreground si la dernière réponse est tronquée pour
// qu'il puisse afficher un et probe la session. // qu'il puisse afficher un ⚠ et probe la session.
return { html, truncated: html.length < MIN_VALID_SIZE, size: html.length }; return { html, truncated: html.length < MIN_VALID_SIZE, size: html.length };
} }
throw new Error("fetchFicheHtml: max retries reached"); throw new Error("fetchFicheHtml: max retries reached");
@@ -642,31 +579,23 @@ async function fetchFicheHtml(origin, phpsessid, formLink) {
// v4.1.9 : le GUID du form est passé en paramètre (extrait dynamiquement du // 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... // HTML de la fiche par le viewer). Il est différent pour une demande S...
// ({C99ECD05}) vs un incident I... ({07ED9C68}). // ({C99ECD05}) vs un incident I... ({07ED9C68}).
async function fetchTimelineApi(origin, phpsessid, guid, formId, formChecksum, timelineType, sectionId) { async function fetchTimelineApi(origin, phpsessid, guid, formId, formChecksum) {
// Sécurité : GUID doit être de la forme %7B...%7D ou {...} // Sécurité : GUID doit être de la forme %7B...%7D ou {...}
if (!/^(%7B|\{)[A-F0-9\-]{36}(%7D|\})$/i.test(guid)) { if (!/^(%7B|\{)[A-F0-9\-]{36}(%7D|\})$/i.test(guid)) {
throw new Error("Invalid GUID: " + guid); throw new Error("Invalid GUID: " + guid);
} }
// S'assurer qu'on a la forme encodée %7B...%7D // S'assurer qu'on a la forme encodée %7B...%7D
const encodedGuid = guid.startsWith("%7B") ? guid : `%7B${guid.replace(/[{}]/g, "")}%7D`; const encodedGuid = guid.startsWith("%7B") ? guid : `%7B${guid.replace(/[{}]/g, "")}%7D`;
// v2026.5.46 — `type` paramétrable. "todo" = action en cours (1 action) ;
// "done" = actions TERMINÉES (où vit le commentaire du tech). Défaut "todo"
// pour ne rien changer aux appels existants (tooltip).
// v2026.5.46 — `sectionId` paramétrable (défaut 1). Les actions
// AL-Intervention (planification, avec champ Intervenant) sont dans une
// autre section que le workflow DS.
const tType = timelineType || "todo";
const tSection = (sectionId != null && sectionId !== "") ? sectionId : 1;
const url = const url =
`${origin}/api/v1/internal/forms/${encodedGuid}/timeline` + `${origin}/api/v1/internal/forms/${encodedGuid}/timeline` +
`?target=${encodeURIComponent(formId)}` + `?target=${encodeURIComponent(formId)}` +
`&checksum=${encodeURIComponent(formChecksum)}` + `&checksum=${encodeURIComponent(formChecksum)}` +
`&type=${encodeURIComponent(tType)}&sectionId=${encodeURIComponent(tSection)}&navigator=&nbRecord=0` + `&type=todo&sectionId=1&navigator=&nbRecord=0` +
`&PHPSESSID=${encodeURIComponent(phpsessid)}`; `&PHPSESSID=${encodeURIComponent(phpsessid)}`;
const r = await evFetch(url, origin); const r = await evFetch(url, origin);
if (!r.ok) { if (!r.ok) {
const err = new Error("HTTP " + r.status); const err = new Error("HTTP " + r.status);
err.kind = await classifyHttpStatusVerified(r.status, origin, phpsessid); err.kind = classifyHttpStatus(r.status);
err.status = r.status; err.status = r.status;
throw err; throw err;
} }
@@ -712,9 +641,8 @@ function looksLikeLoginPage(text) {
* Retourne le nombre de millisecondes restantes avant expiration de la * Retourne le nombre de millisecondes restantes avant expiration de la
* session EasyVista (0 à 1 800 000 = 30 min max). * session EasyVista (0 à 1 800 000 = 30 min max).
* *
* EasyVista appelle LUI-MÊME cet endpoint en continu pour alimenter sa * Attention : cette requête EST authentifiée et prolonge probablement la
* propre modal de timeout (visible dans le réseau du planning natif) : il ne * session (comme toute requête PHP authentifiée). À utiliser avec parcimonie.
* prolonge donc PAS la session — utilisable pour un calibrage périodique.
*/ */
async function fetchSessionTimeRemaining(origin, phpsessid) { async function fetchSessionTimeRemaining(origin, phpsessid) {
const url = `${origin}/timeout_ajax.php` const url = `${origin}/timeout_ajax.php`
@@ -728,7 +656,7 @@ async function fetchSessionTimeRemaining(origin, phpsessid) {
const body = (await r.text()).trim(); const body = (await r.text()).trim();
// Vérifier que c'est bien un nombre (sinon = session morte probable) // Vérifier que c'est bien un nombre (sinon = session morte probable)
if (!/^\d+$/.test(body)) { if (!/^\d+$/.test(body)) {
LOG.warn("bg", "réponse session_time anormale :", body.substring(0, 200)); console.warn("[bg] réponse session_time anormale :", body.substring(0, 200));
// Si c'est une page de login/redirect → session expirée // Si c'est une page de login/redirect → session expirée
if (looksLikeLoginPage(body)) { if (looksLikeLoginPage(body)) {
throw new Error("session_expired"); throw new Error("session_expired");
@@ -781,7 +709,7 @@ async function extendSessionKeepAlive(origin, phpsessid) {
* @param {boolean} force - si true, ignore le cache et refait le test * @param {boolean} force - si true, ignore le cache et refait le test
* @returns {Promise<"internal"|"external">} * @returns {Promise<"internal"|"external">}
* *
* @author Quentin * @author Quentin Rouiller
*/ */
async function detectNetworkContext(force = false) { async function detectNetworkContext(force = false) {
const CACHE_KEY = "network_context_v2"; // v5.0.12 : nouvelle clé pour invalider le cache fautif v5.0.11 const CACHE_KEY = "network_context_v2"; // v5.0.12 : nouvelle clé pour invalider le cache fautif v5.0.11
@@ -792,7 +720,7 @@ async function detectNetworkContext(force = false) {
const data = await chrome.storage.local.get([CACHE_KEY]); const data = await chrome.storage.local.get([CACHE_KEY]);
const cached = data[CACHE_KEY]; const cached = data[CACHE_KEY];
if (cached && cached.detectedAt && (Date.now() - cached.detectedAt) < CACHE_MAX_AGE_MS) { if (cached && cached.detectedAt && (Date.now() - cached.detectedAt) < CACHE_MAX_AGE_MS) {
LOG.info("bg", "detectNetworkContext : cache hit =", cached.networkContext); console.log("[bg] detectNetworkContext : cache hit =", cached.networkContext);
return cached.networkContext; return cached.networkContext;
} }
} catch (e) {} } catch (e) {}
@@ -802,7 +730,7 @@ async function detectNetworkContext(force = false) {
const timer = setTimeout(() => controller.abort(), 2500); const timer = setTimeout(() => controller.abort(), 2500);
let context = "external"; let context = "external";
try { try {
LOG.info("bg", "detectNetworkContext : test de itsma.etat-de-vaud.ch (mode no-cors)..."); console.log("[bg] detectNetworkContext : test de itsma.etat-de-vaud.ch (mode no-cors)...");
// v5.0.11 (fix) : no-cors évite l'erreur CORS de la redirection SSO. // v5.0.11 (fix) : no-cors évite l'erreur CORS de la redirection SSO.
// Si le fetch termine sans throw, le serveur est joignable = interne. // Si le fetch termine sans throw, le serveur est joignable = interne.
// Si timeout ou DNS fail, throw → externe. // Si timeout ou DNS fail, throw → externe.
@@ -814,12 +742,12 @@ async function detectNetworkContext(force = false) {
cache: "no-store" cache: "no-store"
}); });
clearTimeout(timer); clearTimeout(timer);
LOG.info("bg", "detectNetworkContext : interne accessible"); console.log("[bg] detectNetworkContext : interne accessible");
context = "internal"; context = "internal";
} catch (err) { } catch (err) {
clearTimeout(timer); clearTimeout(timer);
// AbortError (timeout) ou TypeError (DNS unreachable / réseau coupé) // AbortError (timeout) ou TypeError (DNS unreachable / réseau coupé)
LOG.info("bg", "detectNetworkContext : interne inaccessible → externe (" + err.name + ": " + err.message + ")"); console.log("[bg] detectNetworkContext : interne inaccessible → externe (" + err.name + ": " + err.message + ")");
context = "external"; context = "external";
} }
@@ -880,9 +808,9 @@ function watchReconnectTabForIamLogin(tabId) {
} else { } else {
targetUrl = "https://www.portail.vd.ch/iam/accueil/"; targetUrl = "https://www.portail.vd.ch/iam/accueil/";
} }
LOG.info("bg", `watchReconnectTab : iamlogin détecté, redirection vers ${targetUrl}`); console.log(`[bg] watchReconnectTab : iamlogin détecté, redirection vers ${targetUrl}`);
chrome.tabs.update(tabId, { url: targetUrl }).catch(e => { chrome.tabs.update(tabId, { url: targetUrl }).catch(e => {
LOG.warn("bg", "watchReconnectTab : update failed", e); console.warn("[bg] watchReconnectTab : update failed", e);
}); });
} }
}; };
@@ -921,7 +849,7 @@ function watchReconnectTabForIamLogin(tabId) {
* Retourne { name: "Nom Prénom" | null, login: "..." | null } ou null si * Retourne { name: "Nom Prénom" | null, login: "..." | null } ou null si
* tout a échoué. * tout a échoué.
* *
* @author Quentin * @author Quentin Rouiller
*/ */
async function fetchCurrentUser(origin, phpsessid) { async function fetchCurrentUser(origin, phpsessid) {
const url = `${origin}/index.php?PHPSESSID=${encodeURIComponent(phpsessid)}`; const url = `${origin}/index.php?PHPSESSID=${encodeURIComponent(phpsessid)}`;
@@ -1074,8 +1002,8 @@ async function submitAbsence(origin, phpsessid, opts) {
body.set("label_guid", opts.typeGuid); body.set("label_guid", opts.typeGuid);
body.set("dialog_action", "save_holidays"); body.set("dialog_action", "save_holidays");
LOG.info("bg", "submitAbsence →", url.substring(0, 140)); console.log("[bg] submitAbsence →", url.substring(0, 140));
LOG.info("bg", "body:", body.toString()); console.log("[bg] body:", body.toString());
const r = await fetch(url, { const r = await fetch(url, {
method: "POST", method: "POST",
@@ -1084,7 +1012,7 @@ async function submitAbsence(origin, phpsessid, opts) {
body: body.toString() body: body.toString()
}); });
LOG.info("bg", "status =", r.status); console.log("[bg] status =", r.status);
if (!r.ok) { if (!r.ok) {
throw new Error("HTTP " + r.status); throw new Error("HTTP " + r.status);
@@ -1143,9 +1071,9 @@ async function submitDouchette(origin, phpsessid, opts) {
const body = new URLSearchParams(); const body = new URLSearchParams();
body.set("dialog_action", "save_planif"); body.set("dialog_action", "save_planif");
LOG.info("bg", "submitDouchette →", url.substring(0, 160)); console.log("[bg] submitDouchette →", url.substring(0, 160));
LOG.info("bg", "body:", body.toString()); console.log("[bg] body:", body.toString());
LOG.info("bg", "techs:", emplIds); console.log("[bg] techs:", emplIds);
try { try {
const r = await fetch(url, { const r = await fetch(url, {
@@ -1154,7 +1082,7 @@ async function submitDouchette(origin, phpsessid, opts) {
headers: { "Content-Type": "application/x-www-form-urlencoded" }, headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString() body: body.toString()
}); });
LOG.info("bg", "status =", r.status); console.log("[bg] status =", r.status);
if (r.status === 401 || r.status === 403) { if (r.status === 401 || r.status === 403) {
return { okCount: 0, errors: techIds.map(t => ({ techId: t, error: "session_expired" })) }; return { okCount: 0, errors: techIds.map(t => ({ techId: t, error: "session_expired" })) };
@@ -1166,13 +1094,7 @@ async function submitDouchette(origin, phpsessid, opts) {
if (looksLikeLoginPage(responseText)) { if (looksLikeLoginPage(responseText)) {
return { okCount: 0, errors: techIds.map(t => ({ techId: t, error: "session_expired" })) }; return { okCount: 0, errors: techIds.map(t => ({ techId: t, error: "session_expired" })) };
} }
// v2026.5.46 — confirmation EV : en cas de succes, le 1er document return { okCount: techIds.length, errors: [] };
// de la reponse ne contient qu un script qui ferme la popup et recharge le
// planning parent (gl_plan_blk.submit). Sa presence = ordre pris en compte
// (le formulaire re-rendu derriere contient close() mais PAS gl_plan_blk).
const confirmed = responseText.indexOf("integratedDialog.close()") !== -1
&& responseText.indexOf("gl_plan_blk.submit()") !== -1;
return { okCount: techIds.length, errors: [], confirmed };
} catch (err) { } catch (err) {
const msg = err && err.message ? err.message : String(err); const msg = err && err.message ? err.message : String(err);
return { okCount: 0, errors: techIds.map(t => ({ techId: t, error: msg })) }; return { okCount: 0, errors: techIds.map(t => ({ techId: t, error: msg })) };
@@ -1229,7 +1151,7 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) {
+ `&function_name=${encodeURIComponent(fn)}` + `&function_name=${encodeURIComponent(fn)}`
+ `&action_id=${encodeURIComponent(actionId)}`; + `&action_id=${encodeURIComponent(actionId)}`;
LOG.info("bg", `deletePlanningItem → tente function_name=${fn}`, url.substring(0, 180)); console.log(`[bg] deletePlanningItem → tente function_name=${fn}`, url.substring(0, 180));
try { try {
// v5.0.13 : utiliser evFetch() au lieu de fetch() brut pour que les // v5.0.13 : utiliser evFetch() au lieu de fetch() brut pour que les
@@ -1238,7 +1160,7 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) {
// notre heuristique le prenait à tort pour un succès. // notre heuristique le prenait à tort pour un succès.
const r = await evFetch(url, origin, { method: "GET" }); const r = await evFetch(url, origin, { method: "GET" });
const body = await r.text(); const body = await r.text();
LOG.info("bg", `status=${r.status} | body (200 premiers chars) : ${(body || "").substring(0, 200)}`); console.log(`[bg] status=${r.status} | body (200 premiers chars) : ${(body || "").substring(0, 200)}`);
if (r.status === 401 || r.status === 403) { if (r.status === 401 || r.status === 403) {
throw new Error("session_expired"); throw new Error("session_expired");
@@ -1258,7 +1180,7 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) {
// Succès explicite : réponse XML du type <X>true</X> // Succès explicite : réponse XML du type <X>true</X>
if (/^<\w+>true<\/\w+>\s*$/i.test(trimmed)) { if (/^<\w+>true<\/\w+>\s*$/i.test(trimmed)) {
LOG.info("bg", `→ SUCCÈS confirmé par XML <...>true</...> avec function_name=${fn}`); console.log(`[bg] → SUCCÈS confirmé par XML <...>true</...> avec function_name=${fn}`);
return { status: r.status, functionName: fn, body: trimmed }; return { status: r.status, functionName: fn, body: trimmed };
} }
@@ -1271,17 +1193,17 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) {
|| lower.includes("<html") || lower.includes("<html")
|| lower.includes("window.location.href"); || lower.includes("window.location.href");
if (looksLikeError) { if (looksLikeError) {
LOG.info("bg", `→ réponse ressemble à une erreur, on tente le prochain nom`); console.log(`[bg] → réponse ressemble à une erreur, on tente le prochain nom`);
lastBody = body; lastBody = body;
continue; continue;
} }
// Pas d'erreur évidente mais pas de succès explicite non plus // Pas d'erreur évidente mais pas de succès explicite non plus
// (ex: réponse vide ou "1" ou "ok"). On considère comme succès. // (ex: réponse vide ou "1" ou "ok"). On considère comme succès.
LOG.info("bg", `→ suppression probablement OK (body neutre) avec function_name=${fn}`); console.log(`[bg] → suppression probablement OK (body neutre) avec function_name=${fn}`);
return { status: r.status, functionName: fn, body: trimmed.substring(0, 200) }; return { status: r.status, functionName: fn, body: trimmed.substring(0, 200) };
} catch (err) { } catch (err) {
if (err.message === "session_expired") throw err; if (err.message === "session_expired") throw err;
LOG.warn("bg", `erreur avec ${fn}:`, err); console.warn(`[bg] erreur avec ${fn}:`, err);
lastErr = err; lastErr = err;
} }
} }
@@ -1306,22 +1228,22 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) {
* Retourne { groups: [{id, name}, ...] }. Liste vide si le fetch échoue ou * Retourne { groups: [{id, name}, ...] }. Liste vide si le fetch échoue ou
* si EV renvoie une page de login / redirection JS. * si EV renvoie une page de login / redirection JS.
* *
* @author Quentin * @author Quentin Rouiller
*/ */
async function detectGroupsFromEV(origin, phpsessid) { async function detectGroupsFromEV(origin, phpsessid) {
const url = `${origin}/index.php?eventName=HelpDesk_PlanningItem&PHPSESSID=${encodeURIComponent(phpsessid)}`; const url = `${origin}/index.php?eventName=HelpDesk_PlanningItem&PHPSESSID=${encodeURIComponent(phpsessid)}`;
LOG.info("bg", "detectGroupsFromEV → fetch page planning"); console.log("[bg] detectGroupsFromEV → fetch page planning");
LOG.info("bg", "URL =", url); console.log("[bg] URL =", url);
const r = await evFetch(url, origin); const r = await evFetch(url, origin);
if (!r.ok) { if (!r.ok) {
const err = new Error("HTTP " + r.status); const err = new Error("HTTP " + r.status);
err.kind = await classifyHttpStatusVerified(r.status, origin, phpsessid); err.kind = classifyHttpStatus(r.status);
err.status = r.status; err.status = r.status;
throw err; throw err;
} }
const html = await r.text(); const html = await r.text();
LOG.info("bg", "page planning taille =", html.length); console.log("[bg] page planning taille =", html.length);
if (looksLikeLoginPage(html)) { if (looksLikeLoginPage(html)) {
const err = new Error("session_expired"); const err = new Error("session_expired");
@@ -1346,7 +1268,7 @@ async function detectGroupsFromEV(origin, phpsessid) {
} }
} }
} }
LOG.info("bg", "" + groups.length + " groupes détectés :", groups); console.log("[bg] " + groups.length + " groupes détectés :", groups);
// Fallback : si le <select> est absent (rendu côté client tardif), on // Fallback : si le <select> est absent (rendu côté client tardif), on
// tente de lire les favoris menuGlobals.tech4 où les noms sont en base64. // tente de lire les favoris menuGlobals.tech4 où les noms sont en base64.
@@ -1360,7 +1282,7 @@ async function detectGroupsFromEV(origin, phpsessid) {
// On n'a pas l'ID dans les favoris : on garde au moins le nom pour // On n'a pas l'ID dans les favoris : on garde au moins le nom pour
// affichage info, mais sans ID ce groupe n'est pas sélectionnable // affichage info, mais sans ID ce groupe n'est pas sélectionnable
// pour fetcher le planning. On ignore donc côté retour. // pour fetcher le planning. On ignore donc côté retour.
LOG.info("bg", "fallback favoris : " + title + " (sans ID, ignoré)"); console.log("[bg] fallback favoris : " + title + " (sans ID, ignoré)");
} }
} }
@@ -1390,7 +1312,7 @@ async function detectTeamFromEV(origin, phpsessid, groupIdArg, supportIdsArg) {
const supportIds = (typeof supportIdsArg === "string") const supportIds = (typeof supportIdsArg === "string")
? supportIdsArg ? supportIdsArg
: await getSupportIds(); : await getSupportIds();
LOG.info("bg", `detectTeamFromEV : group_id = ${groupId} | support_ids = ${supportIds}`); console.log("[bg] detectTeamFromEV : group_id =", groupId, "| support_ids =", supportIds);
// Fetch la popup de sélection des intervenants du groupe // Fetch la popup de sélection des intervenants du groupe
const popupUrl = origin + "/include/components/staff/planning/plan_view_group_supports.php" const popupUrl = origin + "/include/components/staff/planning/plan_view_group_supports.php"
@@ -1400,18 +1322,18 @@ async function detectTeamFromEV(origin, phpsessid, groupIdArg, supportIdsArg) {
+ "&support_ids=" + encodeURIComponent(supportIds) + "&support_ids=" + encodeURIComponent(supportIds)
+ "&group_id=" + encodeURIComponent(groupId); + "&group_id=" + encodeURIComponent(groupId);
LOG.info("bg", "detectTeamFromEV → popup group_supports"); console.log("[bg] detectTeamFromEV → popup group_supports");
LOG.info("bg", "URL =", popupUrl.substring(0, 240)); console.log("[bg] URL =", popupUrl.substring(0, 240));
let popupHtml = ""; let popupHtml = "";
try { try {
const r = await fetch(popupUrl, { method: "GET", credentials: "include" }); const r = await fetch(popupUrl, { method: "GET", credentials: "include" });
LOG.info("bg", "popup status =", r.status); console.log("[bg] popup status =", r.status);
if (!r.ok) throw new Error("HTTP " + r.status + " sur popup group"); if (!r.ok) throw new Error("HTTP " + r.status + " sur popup group");
popupHtml = await r.text(); popupHtml = await r.text();
LOG.info("bg", "popup taille HTML =", popupHtml.length); console.log("[bg] popup taille HTML =", popupHtml.length);
if (looksLikeLoginPage(popupHtml)) throw new Error("session_expired"); if (looksLikeLoginPage(popupHtml)) throw new Error("session_expired");
} catch (e) { } catch (e) {
LOG.warn("bg", "detectTeam: fetch popup failed:", e); console.warn("[bg] detectTeam: fetch popup failed:", e);
// v2026.5.41 : on retourne les IDs déjà sélectionnés par l'user (s'il y en a) // v2026.5.41 : on retourne les IDs déjà sélectionnés par l'user (s'il y en a)
// plutôt qu'une liste hardcodée. Si vide, le viewer affichera juste 0 résultat. // plutôt qu'une liste hardcodée. Si vide, le viewer affichera juste 0 résultat.
const ids = supportIds.split(",").filter(Boolean); const ids = supportIds.split(",").filter(Boolean);
@@ -1426,7 +1348,7 @@ async function detectTeamFromEV(origin, phpsessid, groupIdArg, supportIdsArg) {
const currentIdsSet = new Set(supportIds.split(",").filter(Boolean)); const currentIdsSet = new Set(supportIds.split(",").filter(Boolean));
// v5.0.1 : log le début du HTML pour diagnostic si parsing échoue // v5.0.1 : log le début du HTML pour diagnostic si parsing échoue
LOG.info("bg", "popup HTML (début) =", popupHtml.substring(0, 500)); console.log("[bg] popup HTML (début) =", popupHtml.substring(0, 500));
// Pattern 1 : checkboxes + texte voisin // Pattern 1 : checkboxes + texte voisin
const rxCheckbox = /<input[^>]*type=["']checkbox["'][^>]*value=["'](\d{4,7})["'][^>]*>([\s\S]{0,400}?)(?=<input|<\/tr|<\/table|$)/gi; const rxCheckbox = /<input[^>]*type=["']checkbox["'][^>]*value=["'](\d{4,7})["'][^>]*>([\s\S]{0,400}?)(?=<input|<\/tr|<\/table|$)/gi;
@@ -1440,7 +1362,7 @@ async function detectTeamFromEV(origin, phpsessid, groupIdArg, supportIdsArg) {
results.push({ id, name: name || "? (" + id + ")", alreadyInTeam: currentIdsSet.has(id) }); results.push({ id, name: name || "? (" + id + ")", alreadyInTeam: currentIdsSet.has(id) });
} }
} }
LOG.info("bg", "parsing pattern 1 (checkbox) :", results.length, "résultats"); console.log("[bg] parsing pattern 1 (checkbox) :", results.length, "résultats");
// Pattern 2 : fallback <option value="NNNNN">Nom...</option> // Pattern 2 : fallback <option value="NNNNN">Nom...</option>
if (results.length === 0) { if (results.length === 0) {
@@ -1453,7 +1375,7 @@ async function detectTeamFromEV(origin, phpsessid, groupIdArg, supportIdsArg) {
results.push({ id, name: name || "? (" + id + ")", alreadyInTeam: currentIdsSet.has(id) }); results.push({ id, name: name || "? (" + id + ")", alreadyInTeam: currentIdsSet.has(id) });
} }
} }
LOG.info("bg", "parsing pattern 2 (option) :", results.length, "résultats"); console.log("[bg] parsing pattern 2 (option) :", results.length, "résultats");
} }
// Pattern 3 : fallback brut tags HTML contenant ID à proximité d'un nom // Pattern 3 : fallback brut tags HTML contenant ID à proximité d'un nom
@@ -1471,7 +1393,7 @@ async function detectTeamFromEV(origin, phpsessid, groupIdArg, supportIdsArg) {
results.push({ id, name: nameMatch[1].trim(), alreadyInTeam: currentIdsSet.has(id) }); results.push({ id, name: nameMatch[1].trim(), alreadyInTeam: currentIdsSet.has(id) });
} }
} }
LOG.info("bg", "parsing pattern 3 (brut) :", results.length, "résultats"); console.log("[bg] parsing pattern 3 (brut) :", results.length, "résultats");
} }
// Ajouter les IDs actuels manquants (sans nom) // Ajouter les IDs actuels manquants (sans nom)
@@ -1481,7 +1403,7 @@ async function detectTeamFromEV(origin, phpsessid, groupIdArg, supportIdsArg) {
} }
} }
LOG.info("bg", "" + results.length + " personnes retournées"); console.log("[bg] " + results.length + " personnes retournées");
return { ids: results, groupId: groupId }; return { ids: results, groupId: groupId };
} }
@@ -1604,10 +1526,10 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
`&minute=${encodeURIComponent(msg.minute)}` + `&minute=${encodeURIComponent(msg.minute)}` +
`&multi_day_mode_act=0`; `&multi_day_mode_act=0`;
LOG.warn("reschedule", LOG.warn("reschedule",
`reschedule actionId=${msg.actionId} → tech=${msg.employeeId} ${msg.date} ${msg.hour}:${String(msg.minute).padStart(2,"0")}`); `📅 reschedule actionId=${msg.actionId} → tech=${msg.employeeId} ${msg.date} ${msg.hour}:${String(msg.minute).padStart(2,"0")}`);
const r = await evFetch(url, session.origin); const r = await evFetch(url, session.origin);
if (!r.ok) { if (!r.ok) {
sendResponse({ ok: false, error: await classifyHttpStatusVerified(r.status, session.origin, session.phpsessid), httpStatus: r.status }); sendResponse({ ok: false, error: classifyHttpStatus(r.status), httpStatus: r.status });
return; return;
} }
const txt = await r.text(); const txt = await r.text();
@@ -1645,14 +1567,14 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
const url = `${session.origin}/planning_updator_xhr.php` + const url = `${session.origin}/planning_updator_xhr.php` +
`?PHPSESSID=${encodeURIComponent(session.phpsessid)}`; `?PHPSESSID=${encodeURIComponent(session.phpsessid)}`;
LOG.warn("reschedule", LOG.warn("reschedule",
`updateActionTimes actionId=${msg.actionId} ${msg.startDate} ${msg.startTime}→${msg.endTime}`); `⏱ updateActionTimes actionId=${msg.actionId} ${msg.startDate} ${msg.startTime}→${msg.endTime}`);
const r = await evFetch(url, session.origin, { const r = await evFetch(url, session.origin, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" }, headers: { "Content-Type": "application/x-www-form-urlencoded" },
body body
}); });
if (!r.ok) { if (!r.ok) {
sendResponse({ ok: false, error: await classifyHttpStatusVerified(r.status, session.origin, session.phpsessid), httpStatus: r.status }); sendResponse({ ok: false, error: classifyHttpStatus(r.status), httpStatus: r.status });
return; return;
} }
const txt = await r.text(); const txt = await r.text();
@@ -1678,7 +1600,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
const url = `${session.origin}/index.php?eventName=HelpDesk_PlanningItem&PHPSESSID=${encodeURIComponent(session.phpsessid)}`; const url = `${session.origin}/index.php?eventName=HelpDesk_PlanningItem&PHPSESSID=${encodeURIComponent(session.phpsessid)}`;
const r = await evFetch(url, session.origin); const r = await evFetch(url, session.origin);
if (!r.ok) { if (!r.ok) {
sendResponse({ ok: false, error: await classifyHttpStatusVerified(r.status, session.origin, session.phpsessid), httpStatus: r.status }); sendResponse({ ok: false, error: classifyHttpStatus(r.status), httpStatus: r.status });
return; return;
} }
const txt = await r.text(); const txt = await r.text();
@@ -1702,8 +1624,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
try { try {
const body = await fetchTimelineApi( const body = await fetchTimelineApi(
session.origin, session.phpsessid, session.origin, session.phpsessid,
msg.guid, msg.formId, msg.formChecksum, msg.guid, msg.formId, msg.formChecksum
msg.timelineType, msg.sectionId
); );
if (looksLikeLoginPage(body)) { if (looksLikeLoginPage(body)) {
sendResponse({ ok: false, error: "session_expired" }); sendResponse({ ok: false, error: "session_expired" });
@@ -1768,7 +1689,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
} }
try { try {
const result = await submitDouchette(session.origin, session.phpsessid, msg); const result = await submitDouchette(session.origin, session.phpsessid, msg);
sendResponse({ ok: true, okCount: result.okCount, errors: result.errors, confirmed: result.confirmed === true }); sendResponse({ ok: true, okCount: result.okCount, errors: result.errors });
} catch (err) { } catch (err) {
sendResponse({ ok: false, error: err.message || String(err) }); sendResponse({ ok: false, error: err.message || String(err) });
} }
@@ -1923,7 +1844,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
sendResponse({ ok: false, error: "unknown_message" }); sendResponse({ ok: false, error: "unknown_message" });
} catch (err) { } catch (err) {
LOG.error("bg", "background error:", err); console.error("background error:", err);
sendResponse({ ok: false, error: err.message || String(err) }); sendResponse({ ok: false, error: err.message || String(err) });
} }
})(); })();
@@ -1944,7 +1865,7 @@ async function clearLegacyRefreshAlarms() {
await chrome.alarms.clear("refresh_12h"); await chrome.alarms.clear("refresh_12h");
await chrome.alarms.clear("refresh_15h"); await chrome.alarms.clear("refresh_15h");
} catch (e) { } catch (e) {
LOG.warn("bg", "clearLegacyRefreshAlarms:", e); console.warn("clearLegacyRefreshAlarms:", e);
} }
} }
@@ -2032,7 +1953,7 @@ function _attachCookiesListener() {
if (!chrome.cookies || !chrome.cookies.onChanged) return; if (!chrome.cookies || !chrome.cookies.onChanged) return;
if (_attachCookiesListener._attached) return; if (_attachCookiesListener._attached) return;
_attachCookiesListener._attached = true; _attachCookiesListener._attached = true;
LOG.warn("cookies", "listener PHPSESSID activé (permission cookies accordée)"); LOG.warn("cookies", "🍪 listener PHPSESSID activé (permission cookies accordée)");
chrome.cookies.onChanged.addListener((info) => { chrome.cookies.onChanged.addListener((info) => {
try { try {
if (!info || !info.cookie) return; if (!info || !info.cookie) return;
@@ -2044,9 +1965,9 @@ function _attachCookiesListener() {
// événements importants → LOG.warn (toujours visibles). // événements importants → LOG.warn (toujours visibles).
// Détail (cause, expirationDate, …) → LOG.info (debug only). // Détail (cause, expirationDate, …) → LOG.info (debug only).
if (info.removed) { if (info.removed) {
LOG.warn("cookies", `PHPSESSID supprimé sur ${dom} (cause=${info.cause}) → session morte`); LOG.warn("cookies", `🚫 PHPSESSID supprimé sur ${dom} (cause=${info.cause}) → session morte`);
} else { } else {
LOG.warn("cookies", `PHPSESSID ${phpsessid8} sur ${dom} (cause=${info.cause})`); LOG.warn("cookies", `🍪 PHPSESSID ${phpsessid8} sur ${dom} (cause=${info.cause})`);
} }
// Invalide immédiatement le cache findEasyVistaSession() pour que // Invalide immédiatement le cache findEasyVistaSession() pour que
// le prochain appel reflète le nouveau cookie sans attendre la TTL. // le prochain appel reflète le nouveau cookie sans attendre la TTL.
@@ -2076,18 +1997,18 @@ function _attachCookiesListener() {
(async () => { (async () => {
try { try {
const has = await _hasCookiesPermission(); const has = await _hasCookiesPermission();
LOG.warn("cookies", `permission cookies au boot : ${has ? "ACCORDÉE → lecture cookie active" : "non accordée → fallback URL"}`); LOG.warn("cookies", `🔧 permission cookies au boot : ${has ? "ACCORDÉE → lecture cookie active" : "non accordée → fallback URL"}`);
if (has) _attachCookiesListener(); if (has) _attachCookiesListener();
} catch (e) { /* silent */ } } catch (e) { /* silent */ }
})(); })();
chrome.permissions.onAdded && chrome.permissions.onAdded.addListener((p) => { chrome.permissions.onAdded && chrome.permissions.onAdded.addListener((p) => {
if (p && p.permissions && p.permissions.includes("cookies")) { if (p && p.permissions && p.permissions.includes("cookies")) {
LOG.warn("cookies", "permission cookies ACCORDÉE par l'utilisateur"); LOG.warn("cookies", "✅ permission cookies ACCORDÉE par l'utilisateur");
_attachCookiesListener(); _attachCookiesListener();
} }
}); });
chrome.permissions.onRemoved && chrome.permissions.onRemoved.addListener((p) => { chrome.permissions.onRemoved && chrome.permissions.onRemoved.addListener((p) => {
if (p && p.permissions && p.permissions.includes("cookies")) { if (p && p.permissions && p.permissions.includes("cookies")) {
LOG.warn("cookies", "permission cookies RETIRÉE → fallback URL réactivé"); LOG.warn("cookies", "🛑 permission cookies RETIRÉE → fallback URL réactivé");
} }
}); });
+3 -5
View File
@@ -1,16 +1,14 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Planification", "name": "Planification",
"version": "2026.5.50", "version": "2026.5.45",
"description": "Vue claire et rapide du planning des techniciens EasyVista. Développé par Quentin — DGNSI, Canton de Vaud.", "description": "Vue claire et rapide du planning des techniciens EasyVista. Développé par Quentin Rouiller — DGNSI, Canton de Vaud.",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl/El4I9gxcZkp5bnheg4ZBCKxckRLt9xQ5x+i6phEPYlydl6NapWlkbOyycUaeTZcpoEh/tKQnpMpoEc5jY8zCoRTwh/vfejVBZE6ANDG/dc3iix/1Z8OhRI4zMaRYQ+DQTjS5Q6l6wkcwzbQB85HZNhke/k1qIW6RCuBjeJYmES5WVDwzfCNVvaF6uQvdOCpLjrA3+LI6vUBEwaMg1hM42mC3dU0z9p7qzAby/dH3XpZD9HTFXLJL9/1zryN9rXNDpvZwYWnJ0hLTu5tfmURkvgKOwquX1KJy9vupmhUALRtS08Fplx/e3kUH3LD+coS1ExLwoOZ+1ORBUE9JCB8wIDAQAB",
"permissions": [ "permissions": [
"activeTab", "activeTab",
"scripting", "scripting",
"storage", "storage",
"tabs", "tabs",
"alarms", "alarms"
"nativeMessaging"
], ],
"optional_permissions": [ "optional_permissions": [
"cookies" "cookies"
+226 -1637
View File
File diff suppressed because it is too large Load Diff
+20 -52
View File
@@ -2,10 +2,10 @@
<!-- <!--
Planification — Extension navigateur EasyVista (Canton de Vaud / DGNSI) Planification — Extension navigateur EasyVista (Canton de Vaud / DGNSI)
Copyright (c) 2026 Quentin Copyright (c) 2026 Quentin Rouiller
Licensed under the MIT License — see LICENSE file in the project root. Licensed under the MIT License — see LICENSE file in the project root.
@author Quentin @author Quentin Rouiller
@repository https://gitea.netaplaid.ch/FroSteel/Planification @repository https://gitea.netaplaid.ch/FroSteel/Planification
--> -->
<html lang="fr"> <html lang="fr">
@@ -54,19 +54,16 @@
<!-- v5.0.9 : compteur de session EasyVista (visible < 5 min restantes) --> <!-- v5.0.9 : compteur de session EasyVista (visible < 5 min restantes) -->
<div id="app-session" class="app-session hidden"></div> <div id="app-session" class="app-session hidden"></div>
<div class="topbar-right"> <div class="topbar-right">
<!-- v2026.5.46 — bouton principal "Ajouter" : ouvre un menu déroulant <!-- v4.2.6 : bouton créer une absence pour un ou plusieurs techs -->
avec Réservation / Absence / Pompier (remplace les boutons séparés). --> <button id="absence-btn" class="btn btn-action" title="Créer une absence pour un ou plusieurs techniciens">
<div id="add-menu-wrap" class="add-menu-wrap"> <svg class="btn-action-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<button id="add-menu-btn" class="btn btn-action" title="Ajouter : réservation, absence ou pompier" aria-haspopup="true" aria-expanded="false"> <rect x="3" y="5" width="18" height="16" rx="2" stroke="currentColor" stroke-width="1.8"/>
<span class="btn-action-emoji">➕</span> <path d="M3 9h18M8 3v4M16 3v4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<span class="btn-action-label">Ajouter</span> <circle cx="12" cy="15" r="4.2" stroke="currentColor" stroke-width="1.8" fill="none"/>
<line x1="9" y1="15" x2="15" y2="15" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" transform="rotate(-45 12 15)"/>
</svg>
<span class="btn-action-label">Absence</span>
</button> </button>
<div id="add-menu" class="add-menu hidden" role="menu">
<button id="add-menu-reservation" class="add-menu-item" type="button" role="menuitem"><span class="add-menu-emoji">📅</span> Réservation</button>
<button id="add-menu-absence" class="add-menu-item" type="button" role="menuitem"><span class="add-menu-emoji">🗓️</span> Absence</button>
<button id="add-menu-pompier" class="add-menu-item" type="button" role="menuitem"><span class="add-menu-emoji">🚒</span> Pompier</button>
</div>
</div>
<!-- v4.2.6 : bouton envoyer la planification sur la douchette --> <!-- v4.2.6 : bouton envoyer la planification sur la douchette -->
<button id="douchette-btn" class="btn btn-action" title="Envoyer la planification sur la douchette des techniciens"> <button id="douchette-btn" class="btn btn-action" title="Envoyer la planification sur la douchette des techniciens">
<span class="btn-action-emoji">🎯</span> <span class="btn-action-emoji">🎯</span>
@@ -92,10 +89,7 @@
</div> </div>
</header> </header>
<!-- v2026.5.46 — bandeaux pleine largeur en haut (sous la topbar). Le <!-- Bannière session expirée (non bloquante, apparaît en haut du contenu) -->
chevauchement avec la ligne stats+recherche est réglé par l'empilement
sticky (--stack-top), pas en les déplaçant. Un seul visible à la fois
(coordinateur _hideAllSessionBanners / _setSessionBanner). -->
<div id="session-expired-banner" class="session-banner hidden"> <div id="session-expired-banner" class="session-banner hidden">
<span class="session-banner-icon">⚠</span> <span class="session-banner-icon">⚠</span>
<span class="session-banner-text"> <span class="session-banner-text">
@@ -105,6 +99,8 @@
<button id="session-banner-reconnect" class="btn btn-primary btn-sm">Ouvrir EasyVista</button> <button id="session-banner-reconnect" class="btn btn-primary btn-sm">Ouvrir EasyVista</button>
<button id="session-banner-close" class="btn btn-icon" title="Masquer">×</button> <button id="session-banner-close" class="btn btn-icon" title="Masquer">×</button>
</div> </div>
<!-- v4.2.5 : Bannière EasyVista inaccessible (non bloquante, avec cache) -->
<div id="ev-unreachable-banner" class="session-banner ev-banner hidden"> <div id="ev-unreachable-banner" class="session-banner ev-banner hidden">
<span class="session-banner-icon">⚠</span> <span class="session-banner-icon">⚠</span>
<span class="session-banner-text"> <span class="session-banner-text">
@@ -116,44 +112,14 @@
<button id="ev-unreachable-banner-close" class="btn btn-icon" title="Masquer">×</button> <button id="ev-unreachable-banner-close" class="btn btn-icon" title="Masquer">×</button>
</div> </div>
<main id="main"> <!-- Barre de progression (visible uniquement pendant un refresh actif) -->
<div id="error-box" class="error-box hidden"></div>
<!-- v2026.5.46 — ligne collante (sticky) EN PREMIER : stats + barre de
recherche sur la même hauteur, toujours visibles au scroll. En vue
horizontale, les deux enfants sont relocalisés dans la sidebar
(cf. ELEMENTS_TO_RELOCATE). -->
<div id="topline" class="topline">
<div id="stats" class="stats hidden"></div>
<div id="search-zone" class="search-zone hidden">
<div class="search-bar">
<span class="search-ico" aria-hidden="true">🔍</span>
<input id="ev-search" class="search-input" type="search" autocomplete="off"
spellcheck="false" placeholder="Rechercher : réf, personne, matériel, lieu…">
<span id="ev-search-spin" class="search-spin hidden" aria-hidden="true"></span>
<button id="ev-search-clear" class="search-clear hidden" type="button" title="Effacer" aria-label="Effacer">×</button>
<!-- v2026.5.46 — aide sur la syntaxe de recherche -->
<button id="ev-search-help" class="search-help" type="button" title="Aide sur la recherche" aria-label="Aide sur la recherche">❕</button>
</div>
<div id="ev-search-results" class="search-results hidden" role="listbox"></div>
</div>
<!-- v2026.5.46 — 3e colonne (droite) : bouton « Mes actions » du mode
coordinateur, calé complètement à droite de la ligne. -->
<div id="topline-coord" class="topline-coord"></div>
</div>
<!-- v2026.5.46 — barre de progression du rafraîchissement : SOUS la ligne
stats+recherche (#topline), sticky juste en dessous (avant : au-dessus). -->
<div id="progress-bar" class="progress-bar hidden"> <div id="progress-bar" class="progress-bar hidden">
<div class="progress-bar-fill" id="progress-bar-fill"></div> <div class="progress-bar-fill" id="progress-bar-fill"></div>
<span class="progress-bar-label" id="progress-bar-label"></span> <span class="progress-bar-label" id="progress-bar-label"></span>
</div> </div>
<!-- v2026.5.46 — le « Chargement… » vit DANS la zone des cartes
(sous la ligne stats+recherche), jamais en haut de page. --> <main id="main">
<div id="loading" class="loading">Chargement…</div> <div id="error-box" class="error-box hidden"></div>
<!-- v2026.5.49 — les messages « session expirée » / « EV inaccessible »
CENTRÉS vivent DANS la zone de contenu (sous la barre de recherche, à
la place des cartes), pas en haut de #main : sinon ils s'affichaient
au-dessus de la ligne stats+recherche. showSessionNeeded/showEvUnreachable
vident #cards et affichent le message ici. -->
<div id="session-needed" class="session-needed hidden"> <div id="session-needed" class="session-needed hidden">
<h2>Session EasyVista expirée</h2> <h2>Session EasyVista expirée</h2>
<p>Reconnectez-vous à EasyVista pour continuer.</p> <p>Reconnectez-vous à EasyVista pour continuer.</p>
@@ -165,6 +131,8 @@
<button id="open-ev-btn-2" class="btn btn-primary">Ouvrir EasyVista</button> <button id="open-ev-btn-2" class="btn btn-primary">Ouvrir EasyVista</button>
<button id="retry-btn" class="btn btn-subtle">Réessayer</button> <button id="retry-btn" class="btn btn-subtle">Réessayer</button>
</div> </div>
<div id="loading" class="loading">Chargement…</div>
<div id="stats" class="stats hidden"></div>
<div id="cards" class="cards"></div> <div id="cards" class="cards"></div>
</main> </main>
+2145 -9181
View File
File diff suppressed because it is too large Load Diff