Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06c0195130 | |||
| c65e943dac | |||
| d23824359f | |||
| 2d242d26ec | |||
| 54b8f826df | |||
| 8ecf2d3df4 | |||
| 9d8d8102d7 | |||
| 48b00a8585 | |||
| 6bb97addd6 | |||
| 05275a3be5 | |||
| d7b680fb3f | |||
| 3c7e7c0c25 | |||
| b3677d661a | |||
| 8390db9937 | |||
| 2cc9552fbf | |||
| 0327a55c74 | |||
| 67708d1ad3 | |||
| 1730758cb4 | |||
| 7c0742594c | |||
| af85473837 | |||
| 47a0bca998 | |||
| e92b0c4444 | |||
| 957b754bdc | |||
| aabda3ba7e | |||
| 6a0324b252 | |||
| fd466504c2 | |||
| 02524e78b2 | |||
| 193b3252d4 | |||
| 3a28e1bd0a | |||
| 10a1aef4c7 | |||
| b77f0a9caa | |||
| f7f81f7d9d | |||
| ddb075d563 | |||
| f6dc9eaf7b | |||
| 3d5bdbab3d | |||
| ad952ebc55 | |||
| 1a7393c297 | |||
| d589447533 | |||
| ea5a42c5e1 | |||
| 763e63d9c6 | |||
| bea236ca88 | |||
| d6ab8d59e0 | |||
| 909ddb8301 | |||
| 6794360887 | |||
| 7ba28d3bac | |||
| e17f604d9e | |||
| 9d701701e6 | |||
| 77c68dbe83 | |||
| d4fc8ff250 | |||
| 3996e3fb4f | |||
| 86f52029f5 | |||
| 984f326b39 | |||
| 6d3058028f | |||
| c59abbed23 |
-40
@@ -1,40 +0,0 @@
|
|||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
desktop.ini
|
|
||||||
|
|
||||||
# Editors
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# Backups
|
|
||||||
*.bak
|
|
||||||
*.bak-*
|
|
||||||
*.orig
|
|
||||||
*.old
|
|
||||||
|
|
||||||
# Build artifacts (les ZIP/XPI livrés ne sont pas dans le repo, ils sont buildés à la demande)
|
|
||||||
dist/
|
|
||||||
*.zip
|
|
||||||
*.xpi
|
|
||||||
*.crx
|
|
||||||
|
|
||||||
# Node (si jamais utilisé pour build)
|
|
||||||
node_modules/
|
|
||||||
package-lock.json
|
|
||||||
npm-debug.log*
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
rebuild.log
|
|
||||||
|
|
||||||
# Dossiers de travail temporaires
|
|
||||||
extracted/
|
|
||||||
temp/
|
|
||||||
tmp/
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
test-output/
|
|
||||||
+364
-11
@@ -1,14 +1,366 @@
|
|||||||
# 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 Rouiller pour les techniciens 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, Claude Code se basera sur l'analyse du code
|
> Pour les versions plus anciennes, l'analyse du code source permet de
|
||||||
> source pour déterminer un message de commit pertinent.
|
> reconstituer un message de version pertinent.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v2026.5.45 — Dock latéral drag&drop, fix verdicts ghost, multi-onglets EZV résolu
|
||||||
|
|
||||||
|
> Refonte de l'expérience drag&drop avec dock latéral pour parquer des interventions entre les jours, fix critique du parser de fiches qui marquait à tort des interventions terminées comme annulées, résolution des 6 issues ouvertes (multi-onglets EZV, absences récurrentes, popups épinglés, pompier absent), et nombreux ajustements d'ergonomie.
|
||||||
|
|
||||||
|
### Issues résolues
|
||||||
|
|
||||||
|
- **#3** — Coches « Absences récurrentes » : merge propre avec l'état stocké au lieu d'écrasement, les coches sont retenues lors d'un changement de groupe ou d'une réouverture des paramètres.
|
||||||
|
- **#4** — Perte de session EZV multi-onglets : permission optionnelle `cookies` + listener `cookieChanged` côté background, toggle dans Paramètres → Diagnostics. La session reste valable même après reconnexion et fermeture d'un onglet EZV.
|
||||||
|
- **#5** — Bouton de copie de référence dans une popup épinglée : handler `copy-ref` ajouté.
|
||||||
|
- **#6** — Popup épinglée au premier plan : clic sur une popup recalcule le z-index pour la passer au-dessus des autres.
|
||||||
|
- **#7** — Notification « +2 min » fantôme : reset des flags d'alerte slide au retour de la prolongation.
|
||||||
|
- **#8** — Compteur pompier : un pompier absent toute la journée est exclu du compteur.
|
||||||
|
|
||||||
|
### Dock latéral drag&drop
|
||||||
|
|
||||||
|
Le dock à droite permet de mettre des interventions de côté pendant qu'on navigue entre les jours, puis de les redéposer plus tard.
|
||||||
|
|
||||||
|
- Apparition graduelle pendant un drag : peep-min (sans contenu), peep (avec cartes), expanded (au survol ou au bord droit).
|
||||||
|
- **Délai 500 ms** pour expand/collapse pour éviter le flicker quand le curseur effleure le bord du dock.
|
||||||
|
- Card du dock : référence + durée prévue (`1h`, `1h30`, `45min`) avec barre verticale 4 px sur la gauche dans la couleur de la catégorie. Fond transparent.
|
||||||
|
- Bouton de retrait `×` : **appui long 2 s** avec animation `conic-gradient`. Un clic simple ne fait rien.
|
||||||
|
- Drag depuis le dock : retrait différé à l'activation effective du drag (5 px). Ghost flottant sans heure à gauche. Drop sur tech → modal de confirmation. Drop hors zone / Échap / sur le même slot d'origine → restauration dans le dock.
|
||||||
|
- Plus de scrollbar horizontale en bas du dock, plus de clic-through dans la zone autour du bouton « Tout annuler ».
|
||||||
|
|
||||||
|
### Verdicts ghost — fix critique
|
||||||
|
|
||||||
|
Une intervention terminée par le tech mais dont la fiche était passée en statut « Redirigé » / « Finalisation » / « Exécution » était à tort marquée comme « annulée » et retirée du planning, parce que le parser de fiche cherchait les dates d'action aux indices `[8]`/`[9]` du tableau `rows` alors que le layout EV récent les place en `[6]`/`[7]` (et la description en `[9]` au lieu de `[11]`).
|
||||||
|
|
||||||
|
- **Détection robuste** : scan des valeurs pour le pattern `DD/MM/YYYY HH:MM:SS`, garde des 2 dernières occurrences comme (création, fin). Survit aux variations de layout EV.
|
||||||
|
- **Décorrélation logs / KEEP-forcé** : nouvelle case dédiée « Garder les disparitions » dans Paramètres → Diagnostics, indépendante des « Logs verbeux ». Mode prod par défaut → verdict `REMOVE` appliqué (statut Annulé/Supprimé, ou statut clos sans commentaire du tech, ou action sans commentaire `login:`).
|
||||||
|
|
||||||
|
### Drag&drop bloqué pour les interventions non-déplaçables
|
||||||
|
|
||||||
|
`_canRescheduleIv` refuse maintenant explicitement le drag pour :
|
||||||
|
|
||||||
|
- iv en verdict `terminated-pending` (gris « fait »).
|
||||||
|
- iv en verdict `terminated-clos` (vert ✓✓).
|
||||||
|
- iv dont le statut EV est dans `CLOSED_STATUS` ou `RESOLVED_STATUS`.
|
||||||
|
- iv en cours d'analyse de disparition (`_disappearChecking`).
|
||||||
|
|
||||||
|
### Popups dépinglés
|
||||||
|
|
||||||
|
- Auto-fermeture du popup dépinglé quand la souris quitte sa zone et celle des cards liées de la même iv (300 ms de grâce). N'interfère pas avec un drag de planning en cours.
|
||||||
|
|
||||||
|
### Tooltip / contact
|
||||||
|
|
||||||
|
- Le contact n'inclut plus les labels « fiche » (`Étage`, `Bureau`, `Service`, `Matériel`, `Problème`, `TFS`, `Date`, `Heure`, `Lieu`, `Bénéficiaire`, `Nom utilisateur`) qui se collent parfois à la valeur du contact à cause de séparateurs perdus dans la source EV. Le tooltip insère un saut de ligne avant chaque label collé pour la lisibilité.
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.5.44 — Refonte topbar, personnalisation Apparence, onboarding équipe, refresh séquentiel
|
||||||
|
|
||||||
|
> Refonte visuelle de la topbar (vue classique + horizontale), nouveau panneau
|
||||||
|
> de personnalisation (couleur de la barre du haut + police de l'application
|
||||||
|
> sur toute la page), nouvelle expérience d'onboarding quand aucun technicien
|
||||||
|
> n'est sélectionné, refonte du système de verdicts ghost (✓✓ clos / ✓ Fait /
|
||||||
|
> ✓ Suspendu), refresh strictement séquentiel avec arrêt instantané, et
|
||||||
|
> plusieurs corrections.
|
||||||
|
|
||||||
|
### Refresh / cache / verdicts ghost
|
||||||
|
|
||||||
|
- Rafraîchissement **séquentiel** (1 fiche à la fois) au lieu de 5 workers
|
||||||
|
parallèles → arrêt instantané via le bouton « ✕ Arrêter » (AbortController),
|
||||||
|
plus de races DOM, ordre d'affichage cohérent (pompier d'abord, puis alpha,
|
||||||
|
puis matin → après-midi).
|
||||||
|
- Re-fetch du checksum frais via `basicAutoComplete` + `redirectHeader`
|
||||||
|
(plus de fiche périmée entre sessions).
|
||||||
|
- Cache merge robuste (fallback `cachedByRef` quand `actionId` change) et
|
||||||
|
cache écrit toutes les 5 fiches pendant le refresh (incrémental).
|
||||||
|
- **Système de verdicts ghost unifié** : ✓✓ vert (clos / résolu officiel),
|
||||||
|
✓ gris « Fait » (terminated-pending), ✓ jaune « Suspendu »
|
||||||
|
(terminated-suspended), retrait silencieux pour cancelled / cancelled-
|
||||||
|
reservation / cancelled-absence.
|
||||||
|
- Statuts EV (clos / résolu / annulé / suspendu) éditables depuis Paramètres
|
||||||
|
→ EasyVista avec matching insensible à la casse, accents et conjugaisons.
|
||||||
|
- Mise à jour live du tooltip et du popup épinglé après un verdict (plus
|
||||||
|
besoin de fermer/réouvrir).
|
||||||
|
- Clic immédiat sur la carte dès que le verdict tombe (avant la fin du
|
||||||
|
refresh complet).
|
||||||
|
- Boutons « Actualiser » (rapide, ne re-télécharge pas les fiches déjà
|
||||||
|
connues) vs « Tout recharger » (force tout sauf les ✓✓ déjà clos).
|
||||||
|
- **Mode diagnostic optionnel** (Paramètres → Diagnostics) : aucune
|
||||||
|
intervention disparue n'est retirée silencieusement, tout est tracé sous
|
||||||
|
le préfixe `[disparition]` dans la console F12 pour debug. En PROD
|
||||||
|
(par défaut), les iv `cancelled` sont bien retirées comme avant.
|
||||||
|
|
||||||
|
### Topbar — vue classique
|
||||||
|
|
||||||
|
- Sélecteur de date du planning **ancré au centre absolu** : il ne se décale
|
||||||
|
plus quand le bouton « ✕ Arrêter » apparaît à droite pendant un
|
||||||
|
rafraîchissement.
|
||||||
|
- Bouton **« Aujourd'hui »** affiché en toutes lettres (au lieu de « Auj. »).
|
||||||
|
- Horloge contextuelle (date du jour + heure) réduite et discrète, à côté
|
||||||
|
du bouton Aujourd'hui dans un cadre encadré.
|
||||||
|
- Date du planning agrandie et neutre (couleur stable, plus de bascule
|
||||||
|
selon la date sélectionnée).
|
||||||
|
|
||||||
|
### Personnalisation — Paramètres → Apparence
|
||||||
|
|
||||||
|
- **Couleur de la barre du haut** : 12 presets cliquables (Défaut, Blanc,
|
||||||
|
Gris clair, Anthracite, Bleu DGNSI, Marine, Vert sapin, Brique, Violet,
|
||||||
|
Rouge, Bleu pastel, Vert pastel) + picker custom + champ hex `#rrggbb`
|
||||||
|
+ bouton « Réinitialiser ».
|
||||||
|
- La couleur s'applique uniquement à la topbar (et à la sidebar quand on
|
||||||
|
est en vue horizontale).
|
||||||
|
- Le texte de la topbar (titre, horloge, date, capture-info, badges,
|
||||||
|
boutons) s'adapte automatiquement (clair/foncé) selon la **luminance**
|
||||||
|
de la couleur choisie pour rester toujours lisible.
|
||||||
|
- **Police de l'application** : 28 choix organisés en familles
|
||||||
|
(sans-serif : Arial, Helvetica, Verdana, Tahoma, Trebuchet, Calibri,
|
||||||
|
Segoe UI, Gill Sans, Futura, Optima ; serif : Times New Roman, Georgia,
|
||||||
|
Cambria, Garamond, Palatino, Bookman ; monospace : Courier New, Consolas,
|
||||||
|
Lucida Console, JetBrains Mono ; display : Comic Sans MS, Impact,
|
||||||
|
Brush Script, Copperplate ; condensée : Arial Narrow). La police choisie
|
||||||
|
s'applique à **toute la page** (topbar, cards, popups, tooltips, panel
|
||||||
|
admin) et chaque option du select s'affiche dans sa propre police pour
|
||||||
|
prévisualiser le rendu, avec un aperçu live à droite.
|
||||||
|
- Export / import du cache et de `admin_config` depuis Paramètres →
|
||||||
|
Diagnostics.
|
||||||
|
|
||||||
|
### Vue horizontale
|
||||||
|
|
||||||
|
- Bloc « Aujourd'hui + horloge » empilé verticalement dans la sidebar, dans
|
||||||
|
le même cadre encadré que la vue classique.
|
||||||
|
- Date sélectionnée mise en avant (taille augmentée, en gras), date du
|
||||||
|
jour et heure réduites à la même petite taille pour rester discrètes.
|
||||||
|
- **Barre verticale verte** ajoutée à droite des mini-cards quand le
|
||||||
|
ticket est officiellement clôturé / résolu (✓✓), avec léger décalage du
|
||||||
|
✓✓ pour ne pas chevaucher la barre.
|
||||||
|
- Quand l'utilisateur a choisi une couleur de topbar, la sidebar prend
|
||||||
|
aussi la couleur : titre, horloge, capture-info, stats, today-block,
|
||||||
|
date sélectionnée, boutons, theme-toggle et séparateurs adoptent une
|
||||||
|
teinte translucide cohérente (via `color-mix`) qui contraste correctement
|
||||||
|
sur n'importe quel fond.
|
||||||
|
|
||||||
|
### Statistiques globales
|
||||||
|
|
||||||
|
- Nouveau compteur **« X faits / Y clos »** entre `(matin · après-midi)`
|
||||||
|
et `tech. dispo`. Inclut tous les tickets terminés (clos/résolus officiels
|
||||||
|
+ verdicts ghost « Fait » / « Suspendu »).
|
||||||
|
- En vue classique, séparateur `//` après `clos` (au lieu de `·`).
|
||||||
|
- En vue horizontale (sidebar), une **barre horizontale 1px** sépare le
|
||||||
|
bloc interventions/faits/clos du bloc tech. dispo + pompiers / absents.
|
||||||
|
|
||||||
|
### Onboarding équipe (1ʳᵉ install ou config vide)
|
||||||
|
|
||||||
|
- L'erreur générique « Aucun technicien sélectionné » est remplacée par une
|
||||||
|
**carte d'onboarding centrée** comprenant :
|
||||||
|
- icône (👥) cerclée en couleur accent du thème ;
|
||||||
|
- titre « Aucune équipe configurée » ;
|
||||||
|
- description claire ;
|
||||||
|
- bouton primary **« Ouvrir paramètres »** qui ouvre directement le panel
|
||||||
|
admin sur la section Équipe.
|
||||||
|
- Carte centrée verticalement et horizontalement dans la zone disponible,
|
||||||
|
identique en vue classique et horizontale.
|
||||||
|
|
||||||
|
### Bugfix
|
||||||
|
|
||||||
|
- **Issue #1 (Pompier + Absence)** : si un tech est à la fois pompier ET
|
||||||
|
absent, les deux badges s'affichent désormais avec un séparateur `/` au
|
||||||
|
lieu de masquer l'absence derrière le badge pompier.
|
||||||
|
- **Absences récurrentes** : quand on changeait de groupe puis revenait au
|
||||||
|
groupe initial, les jours d'absence cochés pour les techniciens
|
||||||
|
disparaissaient visuellement (la donnée elle-même restait en storage).
|
||||||
|
Correction : restauration depuis `cfg.recurringAbsences` à chaque
|
||||||
|
re-render.
|
||||||
|
- **Barre de progression / bannière session expirée** : suivent désormais
|
||||||
|
la hauteur dynamique de la topbar (variable CSS `--topbar-height` mesurée
|
||||||
|
par un `ResizeObserver`). Plus de chevauchement quand on scrolle.
|
||||||
|
- **STATUS_FR regex** : limite augmentée de 30 à 200 caractères (battait
|
||||||
|
sur « Suspendu : Attente info bénéficiaire/demandeur »).
|
||||||
|
- **Description action** : décodage `" → "`, `<br> → \n`, HTML
|
||||||
|
strippé. Préfixe « login: » retiré du commentaire technicien dans le
|
||||||
|
tooltip / popup.
|
||||||
|
- **Tooltip référence** : flèche « ↗ » retirée du lien cliquable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2026.5.43 — Fix Firefox : positionnement menu dock + stabilité popup pin/unpin
|
||||||
|
|
||||||
|
### Menu hover sur pastille du dock (popup réduit)
|
||||||
|
- Bug Firefox uniquement : quand un popup épinglé était réduit dans la
|
||||||
|
taskbar du bas, le menu qui apparaît au survol de la pastille
|
||||||
|
(Agrandir / Fermer) se positionnait trop haut, pas juste au-dessus de
|
||||||
|
la pastille.
|
||||||
|
- Cause : `getBoundingClientRect()` était appelé immédiatement après
|
||||||
|
`appendChild`, avant que Firefox n'ait calculé la mise en page.
|
||||||
|
Combiné avec un `transform: translateY(4px)` dans l'animation
|
||||||
|
`pill-hover-menu-appear`, Firefox lisait des dimensions décalées.
|
||||||
|
- Fix : positionnement hors écran initial, force-layout via
|
||||||
|
`void offsetHeight`, mesure des dimensions, puis pose finale. CSS de
|
||||||
|
l'animation simplifiée en opacité-only (plus de transform).
|
||||||
|
|
||||||
|
### Stabilité popup au pin/unpin
|
||||||
|
- Bug : la popup épinglée bougeait visuellement et changeait légèrement
|
||||||
|
de taille quand on la dé-épinglait avec le bouton 📌 (puis l'inverse).
|
||||||
|
- Cause : `.pinned-popup` avait `padding-top: 28px` (place pour la
|
||||||
|
dragbar) et `border: 2px`, alors que `.soft-unpinned` avait
|
||||||
|
`padding-top: 12px` et `border: 1px`. Le contenu se décalait de 16px
|
||||||
|
vers le haut et la popup devenait 1px plus fine de chaque côté.
|
||||||
|
- Fix : `.soft-unpinned` conserve désormais `padding-top: 28px` et
|
||||||
|
`border: 2px` comme `.pinned-popup`. Bordure passe juste en
|
||||||
|
`--border-strong` (gris discret) plutôt que `--accent` (bleu) pour
|
||||||
|
signaler visuellement le mode "détaché". Position et taille stables.
|
||||||
|
|
||||||
|
## v2026.5.42 — Nettoyage de commentaires + exemples génériques
|
||||||
|
|
||||||
|
- Passage en revue des commentaires de `src/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.
|
||||||
|
- Mise à jour cohérente du README, du CHANGELOG et des pages wiki Versions /
|
||||||
|
Utilisation pour utiliser les mêmes notations génériques dans les
|
||||||
|
exemples de référence.
|
||||||
|
|
||||||
|
## v2026.5.41 — Suppression des hardcodes (groupe / domaines / équipe) → tout depuis l'admin
|
||||||
|
|
||||||
|
### Plus aucun hardcode au runtime
|
||||||
|
- Le groupe EasyVista, les domaines (interne/externe) et la liste des
|
||||||
|
techniciens ne sont **plus codés en dur** dans `background.js` /
|
||||||
|
`viewer.js`. Tout est lu depuis `admin_config` (chrome.storage.local),
|
||||||
|
alimenté par les onglets **Équipe** et **EasyVista** du panel admin.
|
||||||
|
- `chrome.storage.local` survit aux mises à jour d'extension → la
|
||||||
|
configuration de l'utilisateur (groupe, équipe, absences récurrentes,
|
||||||
|
domaines) est conservée d'une version à l'autre.
|
||||||
|
|
||||||
|
### Domaines EasyVista (interne / externe)
|
||||||
|
- Défaut hardcodé conservé comme **filet de sécurité** au 1er install
|
||||||
|
(`https://itsma.etat-de-vaud.ch` + `https://itsma.vd.ch`).
|
||||||
|
- L'utilisateur peut les remplacer dans Paramètres → EasyVista. Le
|
||||||
|
service worker (`findEasyVistaSession`, `evFetch`, etc.) lit la valeur
|
||||||
|
effective via `getEvOrigins()`.
|
||||||
|
|
||||||
|
### Group ID EasyVista
|
||||||
|
- Défaut hardcodé conservé comme filet de sécurité (`191` = SI-CSS).
|
||||||
|
- Lu via `getGroupId()` dans `fetchPlanningXml`, `detectTeamFromEV` et
|
||||||
|
partout où c'était hardcodé.
|
||||||
|
- Le sélecteur de Paramètres → Équipe alimente `cfg.groupId`.
|
||||||
|
|
||||||
|
### Liste des techniciens
|
||||||
|
- **Aucun défaut hardcodé**. Sur un install vierge, `cfg.team` est
|
||||||
|
vide → le service worker lève `no_team_configured` plutôt que de
|
||||||
|
fetcher avec une liste fictive.
|
||||||
|
- Le viewer affiche : *"Aucun technicien sélectionné. Ouvrez ⚙
|
||||||
|
Paramètres → Équipe pour choisir le groupe EasyVista et cocher les
|
||||||
|
techniciens à afficher."*
|
||||||
|
- Lu via `getSupportIds()` (CSV des clés de `cfg.team`).
|
||||||
|
- Côté `viewer.js` : `TEAM` et `RECURRING_ABSENCES` sont des `let`
|
||||||
|
vides au démarrage, repeuplés par `_initTeamFromConfig()` appelé tôt
|
||||||
|
dans `init()`.
|
||||||
|
|
||||||
|
### Coulisses
|
||||||
|
- Nouveau dans `background.js` : helpers `getAdminConfig()`,
|
||||||
|
`getEvOrigins()`, `getGroupId()`, `getSupportIds()`,
|
||||||
|
`getDayBounds()` qui centralisent la lecture de la config persistée.
|
||||||
|
- `fetchPlanningXml()` lève `Error("no_team_configured")` quand la
|
||||||
|
liste de techs est vide ; le handler `fetchPlanning` propage l'erreur
|
||||||
|
au viewer via `err.kind`.
|
||||||
|
- Toutes les anciennes constantes hardcodées (`EV_ORIGINS`,
|
||||||
|
`DEFAULT_SUPPORT_IDS` interne à `detectTeamFromEV`,
|
||||||
|
`isXXXAbsentFriday`) ont été remplacées ou retirées.
|
||||||
|
|
||||||
|
### Conflits absence/réservation × intervention
|
||||||
|
- Nouveau code visuel : si une intervention est planifiée pendant
|
||||||
|
qu'un tech a une **absence** (toute la journée ou demi-journée) ou
|
||||||
|
une **réservation** sur le même créneau, sa carte (row classique +
|
||||||
|
mini-card en vue horizontale) est peinte en **rouge plein** avec
|
||||||
|
texte blanc. Logique : full-day → toutes les interv en rouge ;
|
||||||
|
partiel → seules celles en chevauchement.
|
||||||
|
|
||||||
|
### Synchronisation des heures EV ↔ admin
|
||||||
|
- Les paramètres `day_start_hour` / `day_end_hour` envoyés à
|
||||||
|
`planning_xhr.php` et `begin_hour` / `end_hour` envoyés à
|
||||||
|
`plan_set_holidays_popup.php` (création absence) et
|
||||||
|
`plan_set_tech_planif_popup.php` (douchette) lisent désormais
|
||||||
|
`cfg.dayStart` / `cfg.dayEnd` (Paramètres → Apparence → Heures de la
|
||||||
|
journée). Avant : `8` / `18` / `19` figés en dur, ce qui rendait le
|
||||||
|
réglage des heures côté UI partiellement effectif (la timeline se
|
||||||
|
redessinait, mais les requêtes EV continuaient sur la plage hardcodée).
|
||||||
|
|
||||||
|
### Édition des domaines EV → permissions runtime
|
||||||
|
- `manifest.json` : ajout de `"optional_host_permissions":
|
||||||
|
["https://*/*"]` pour permettre l'édition des domaines EasyVista
|
||||||
|
vers des origines non prévues à l'install.
|
||||||
|
- Quand l'utilisateur saisit un domaine custom dans Paramètres →
|
||||||
|
EasyVista et clique sur Enregistrer, l'extension appelle
|
||||||
|
`chrome.permissions.request()` pour demander la permission au
|
||||||
|
navigateur. Si refus → toast d'avertissement, les fetches échoueront
|
||||||
|
jusqu'à acceptation.
|
||||||
|
- Les deux domaines hardcodés (`itsma.etat-de-vaud.ch` +
|
||||||
|
`itsma.vd.ch`) restent dans `host_permissions` (toujours accordés à
|
||||||
|
l'install), pas besoin de redemander la permission pour eux.
|
||||||
|
|
||||||
|
## v2026.5.40 — Vue horizontale enrichie (ref + ville + barre couleur)
|
||||||
|
**Branche** : current
|
||||||
|
|
||||||
|
- En vue horizontale, chaque segment timeline d'intervention contient
|
||||||
|
désormais :
|
||||||
|
- Une **barre verticale couleur catégorie** à gauche (mêmes teintes que
|
||||||
|
les `.intervention-dot` de la vue classique : livraison/recup/
|
||||||
|
remplacement/incident/rollout/réservation/autre)
|
||||||
|
- La **référence** (ex: `SYYMMDD_NNNNN`) en gras
|
||||||
|
- La **ville** en gris muted
|
||||||
|
- Hauteur de la timeline en horizontale passée de 22px à 32px pour laisser
|
||||||
|
la place au texte
|
||||||
|
- Fond des segments d'intervention : `--bg-elevated` neutre + bordure 1px
|
||||||
|
pour que le texte reste lisible (la couleur catégorie n'est plus en fond
|
||||||
|
plein, juste en barre gauche)
|
||||||
|
- Vue classique inchangée
|
||||||
|
- Réorganisation interne du repo : `src/` pour les sources, `dist/`
|
||||||
|
généré, `Autres/` pour build.sh + meta files (LICENSE, README,
|
||||||
|
CHANGELOG)
|
||||||
|
|
||||||
|
## v2026.5.39 — Séparation Matin / Après-midi + Apparence (thème, zoom, cache)
|
||||||
|
|
||||||
|
### Séparation matin / après-midi
|
||||||
|
- Séparateur visuel "MATIN" / "APRÈS-MIDI" entre les interventions
|
||||||
|
dans la vue classique : pill grise neutre, ligne 3px épaisse.
|
||||||
|
- Affiché aussi entre les absences partielles (demi-journée).
|
||||||
|
- Si une période est vide, son séparateur n'est pas affiché.
|
||||||
|
- Caché en vue horizontale (les rows sont masquées de toute façon).
|
||||||
|
|
||||||
|
### Timeline — coupure midi très visible
|
||||||
|
- Bande verticale composée d'un trait massif central (couleur --text)
|
||||||
|
+ stripes diagonales en arrière-plan (effet "césure"). 6 px de large
|
||||||
|
(7 px en vue horizontale). Visible immédiatement, pas de label superflu.
|
||||||
|
|
||||||
|
### Vue horizontale (sidebar)
|
||||||
|
- Boutons (Absence, Douchette, Actualiser, Tout recharger, Vider cache,
|
||||||
|
Thème) maintenant **vraiment** poussés en bas via `min-height: 100vh`
|
||||||
|
sur la sidebar.
|
||||||
|
- Bouton "Aujourd'hui" : style cohérent avec les flèches ◀ ▶ (même
|
||||||
|
padding, font-size, hauteur), texte centré, libellé complet
|
||||||
|
"Aujourd'hui" (au lieu de "Auj.").
|
||||||
|
- Espace visuel entre `Actualisé à HH:MM` et le bouton Absence (fine
|
||||||
|
bordure top + padding).
|
||||||
|
|
||||||
|
### Vue classique (topbar)
|
||||||
|
- Ordre verrouillé via CSS `order` : badge user → titre → date-nav →
|
||||||
|
capture-info → refresh-check. Évite les déplacements au retour de
|
||||||
|
vue horizontale.
|
||||||
|
|
||||||
|
### Section Apparence (admin) — refondue + en première position
|
||||||
|
- **Thème** : sélecteur Auto / Clair / Sombre (s'enregistre direct).
|
||||||
|
- **Durée du cache (jours)** : configurable, défaut 7 jours, range 1-365.
|
||||||
|
Lue par viewer.js (purge auto) ET background.js (au boot).
|
||||||
|
- **Taille du texte** : 5 niveaux (-20%, -10%, 100%, +10%, +20%) via CSS
|
||||||
|
`zoom` sur body. Persisté dans admin_config.textZoom et appliqué dès
|
||||||
|
le boot.
|
||||||
|
- Section "Apparence" est maintenant **la première** dans le panel admin.
|
||||||
|
|
||||||
## v2026.5.38 — Attribution auteur + nettoyage code
|
## v2026.5.38 — Attribution auteur + nettoyage code
|
||||||
**Branche** : current
|
**Branche** : current
|
||||||
|
|
||||||
@@ -111,12 +463,12 @@
|
|||||||
- Stats rapides .tech-row-stats ajoutés au header (nb interv, Xm · Ya)
|
- Stats rapides .tech-row-stats ajoutés au header (nb interv, Xm · Ya)
|
||||||
|
|
||||||
## v2026.5.31 — Sarcelle pour absence récurrente (REJETÉ par utilisateur)
|
## v2026.5.31 — Sarcelle pour absence récurrente (REJETÉ par utilisateur)
|
||||||
- Couleur Pillonel vendredi : sarcelle foncée #0f766e / soft #ccfbf1
|
- Couleur absence récurrente (jour fixe) : sarcelle foncée #0f766e / soft #ccfbf1
|
||||||
- Variables --c-recurring, --c-recurring-soft
|
- Variables --c-recurring, --c-recurring-soft
|
||||||
- Layout 4 colonnes forcées + scroll interne cartes (REJETÉ : "scroll en continu")
|
- Layout 4 colonnes forcées + scroll interne cartes (REJETÉ : "scroll en continu")
|
||||||
|
|
||||||
## v2026.5.30 — Absence récurrente cyan + mode compact 24"
|
## v2026.5.30 — Absence récurrente cyan + mode compact 24"
|
||||||
- Absence récurrente Pillonel vendredi en cyan
|
- Absences récurrentes (configurées par tech) en cyan
|
||||||
- Mode compact @media (max-width: 1920px) avec grid-template-columns: repeat(4, 1fr)
|
- Mode compact @media (max-width: 1920px) avec grid-template-columns: repeat(4, 1fr)
|
||||||
|
|
||||||
## v2026.5.29 — Contraste++ + footer
|
## v2026.5.29 — Contraste++ + footer
|
||||||
@@ -165,8 +517,9 @@
|
|||||||
|
|
||||||
## Versions antérieures (v5.x et v4.x)
|
## Versions antérieures (v5.x et v4.x)
|
||||||
|
|
||||||
> Ces versions sont à analyser par Claude Code à partir des fichiers source.
|
> Ces versions ne sont pas documentées en détail. Pour les analyser à partir
|
||||||
> Indices clés à chercher dans le viewer.js :
|
> des fichiers source historiques (consultables via les tags git), voici les
|
||||||
|
> indices clés à chercher dans `viewer.js` :
|
||||||
>
|
>
|
||||||
> - Présence de `pinTooltip` → version >= v4.x
|
> - Présence de `pinTooltip` → version >= v4.x
|
||||||
> - Présence de `_softUnpinPopup` → version >= v4.3.3
|
> - Présence de `_softUnpinPopup` → version >= v4.3.3
|
||||||
@@ -194,14 +547,14 @@
|
|||||||
- v4.3.3 : _softUnpinPopup (désépinglage mou)
|
- v4.3.3 : _softUnpinPopup (désépinglage mou)
|
||||||
|
|
||||||
### v3.x et antérieures — Versions de base
|
### v3.x et antérieures — Versions de base
|
||||||
- À analyser par Claude Code
|
- Code historique consultable via les tags git correspondants.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Notes techniques persistantes (toutes versions)
|
## Notes techniques persistantes (toutes versions)
|
||||||
|
|
||||||
- 8 techs hardcodés : "76272,83725,66635,92235,90070,40944,72485,86874"
|
- 8 techs hardcodés à l'origine (depuis v2026.5.41 : retirés, alimentés par admin_config)
|
||||||
- Pillonel Olivier (ID 40944) absent tous les vendredis (hardcodé)
|
- Absences récurrentes (un tech absent un jour fixe par semaine) hardcodées à l'origine, depuis v2026.5.41 configurables via Paramètres → Équipe
|
||||||
- Group ID EasyVista : 191
|
- Group ID EasyVista : 191
|
||||||
- Domaines cibles : itsma.etat-de-vaud.ch (interne), itsma.vd.ch (externe)
|
- Domaines cibles : itsma.etat-de-vaud.ch (interne), itsma.vd.ch (externe)
|
||||||
- SSO : Canton ForgeRock OpenAM
|
- SSO : Canton ForgeRock OpenAM
|
||||||
@@ -216,4 +569,4 @@
|
|||||||
|
|
||||||
**Quentin Rouiller** (QRO)
|
**Quentin Rouiller** (QRO)
|
||||||
Technicien DGNSI — Canton de Vaud
|
Technicien DGNSI — Canton de Vaud
|
||||||
Email pour commits Git : `quentin.rouiller@ikmail.com`
|
Contact : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact)
|
||||||
|
|||||||
@@ -1,27 +1,52 @@
|
|||||||
# Planification — Extension EasyVista Canton de Vaud
|
# Planification — Extension EasyVista Canton de Vaud
|
||||||
|
|
||||||
Extension Chrome / Firefox pour visualiser de manière claire et rapide le planning des techniciens DGNSI (Canton de Vaud) dans EasyVista.
|
Extension Chrome / Firefox pour visualiser de manière claire et rapide le planning EasyVista de l'équipe technicienne DGNSI (Canton de Vaud).
|
||||||
|
|
||||||
|
> 📖 **Documentation utilisateur complète** : [wiki](https://gitea.netaplaid.ch/FroSteel/Planification/wiki) ([Home](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Home) · [Utilisation](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Utilisation) · [Versions](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Versions) · [Architecture](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Architecture) · [Dépannage](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/D%C3%A9pannage) · [Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact))
|
||||||
|
|
||||||
## Aperçu rapide
|
## Aperçu rapide
|
||||||
|
|
||||||
- **Auteur** : Quentin Rouiller (QRO)
|
- **Auteur** : Quentin Rouiller (QRO), Technicien DGNSI — Canton de Vaud
|
||||||
- **Cible** : techniciens DGNSI (Canton de Vaud), EasyVista (`itsma.etat-de-vaud.ch` / `itsma.vd.ch`)
|
- **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.37`
|
- **Version actuelle** : [`v2026.5.45`](https://gitea.netaplaid.ch/FroSteel/Planification/releases/tag/v2026.5.45) (latest)
|
||||||
- **Manifest** : V3 (Chrome/Edge/Firefox)
|
- **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)
|
||||||
- **Format** : `.zip` (Chromium) + `.xpi` signé (Firefox)
|
- **Manifest** : V3 (Chrome/Edge/Firefox 140+)
|
||||||
|
- **Format** : `.zip` (Chromium) + `.xpi` signé Mozilla (Firefox)
|
||||||
|
- **Distribution** : auto-update natif Firefox via `firefox-updates.json`
|
||||||
|
|
||||||
|
## Installation rapide
|
||||||
|
|
||||||
|
### Firefox 🦊 (recommandé — auto-update)
|
||||||
|
|
||||||
|
1. Télécharger le `.xpi` signé depuis la **[release courante](https://gitea.netaplaid.ch/FroSteel/Planification/releases/latest)**.
|
||||||
|
2. Drag-and-drop dans `about:addons` de Firefox.
|
||||||
|
3. Cliquer "Ajouter".
|
||||||
|
|
||||||
|
À partir de là, l'extension se met à jour **automatiquement** à chaque nouvelle version (vérification toutes les ~24 h via `firefox-updates.json`).
|
||||||
|
|
||||||
|
### Chrome / Edge / Brave 🌐 (manuel)
|
||||||
|
|
||||||
|
1. Télécharger le `.zip` depuis la **[release courante](https://gitea.netaplaid.ch/FroSteel/Planification/releases/latest)**.
|
||||||
|
2. Décompresser dans un dossier permanent.
|
||||||
|
3. `chrome://extensions/` (ou `edge://extensions/`) → activer **Mode développeur** → "Charger l'extension non empaquetée" → sélectionner le dossier décompressé.
|
||||||
|
|
||||||
|
Les mises à jour sont **manuelles** : à chaque nouvelle release, retélécharger le `.zip`, écraser le dossier, puis cliquer ⟳ (Recharger) sur la carte de l'extension.
|
||||||
|
|
||||||
|
➡ Pour le détail complet (stockage, désinstallation, comparatif), voir [wiki Utilisation → Installation et navigateurs](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Utilisation#installation-et-navigateurs).
|
||||||
|
|
||||||
## Fonctionnalités principales
|
## Fonctionnalités principales
|
||||||
|
|
||||||
### Vue planning
|
### Vue planning
|
||||||
- Affichage des interventions et réservations groupées par technicien
|
- Affichage des interventions et réservations groupées par technicien
|
||||||
- Horaires, contact, lieu, catégorie, statut visibles d'un coup d'œil
|
- Horaires, contact, lieu, catégorie, statut visibles d'un coup d'œil
|
||||||
- 8 techniciens hardcodés (équipe IT canton)
|
- Équipe configurable depuis le panel admin (détection automatique via EasyVista)
|
||||||
- Cache local pour réduire les requêtes serveur
|
- Cache local pour réduire les requêtes serveur
|
||||||
|
|
||||||
### Modes d'affichage
|
### Modes d'affichage
|
||||||
- **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
|
||||||
- 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`)
|
||||||
|
|
||||||
@@ -38,7 +63,7 @@ Extension Chrome / Firefox pour visualiser de manière claire et rapide le plann
|
|||||||
- **Congé / Congés** : cyan `#06b6d4` (suffixe `s` adaptatif)
|
- **Congé / Congés** : cyan `#06b6d4` (suffixe `s` adaptatif)
|
||||||
- **Pompier** : rouge `#b03030`
|
- **Pompier** : rouge `#b03030`
|
||||||
- Badge + barre gauche colorée + dégradé fond
|
- Badge + barre gauche colorée + dégradé fond
|
||||||
- Absence récurrente Pillonel vendredi : cyan (depuis v2026.5.30)
|
- Absences récurrentes (configurées par tech) : cyan (depuis v2026.5.30)
|
||||||
|
|
||||||
### User et session
|
### User et session
|
||||||
- Badge user avec photo/initiales en topbar
|
- Badge user avec photo/initiales en topbar
|
||||||
@@ -48,9 +73,14 @@ Extension Chrome / Firefox pour visualiser de manière claire et rapide le plann
|
|||||||
- Reconnexion automatique
|
- Reconnexion automatique
|
||||||
|
|
||||||
### Admin et configuration
|
### Admin et configuration
|
||||||
- Mode admin caché : bouton ⚙ Paramètres dans popup user-badge (depuis v2026.5.25, remplace les 5 clics secrets sur le titre)
|
- Mode admin : bouton ⚙ Paramètres dans popup user-badge (depuis v2026.5.25)
|
||||||
- Configuration persistée dans `localStorage` (`admin_config`)
|
- Configuration persistée dans `chrome.storage.local` (`admin_config`)
|
||||||
- Catégories interventions personnalisables (livraison/recup/remplacement/incident/rollout/reservation/autre)
|
- Sélecteur de groupe EasyVista (SI-CSS, SI-EXT, …) en tête de l'onglet Équipe (depuis v2026.5.40) — détection automatique via le `<select id="plan_group_id">` de la page Planning EV, robuste aux ajouts/renommages côté EV
|
||||||
|
- Édition manuelle des domaines EasyVista interne / externe (depuis v2026.5.40)
|
||||||
|
- Tri des techniciens : actifs d'abord, puis exclus, alphabétique dans chaque groupe (depuis v2026.5.40)
|
||||||
|
- **Personnalisation Apparence (depuis v2026.5.44)** : couleur de la topbar (12 presets + custom + hex) avec contraste auto-calculé sur le texte ; 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
|
||||||
|
- **Onboarding équipe (depuis v2026.5.44)** : carte centrée propre quand aucun tech n'est sélectionné, avec bouton « Ouvrir paramètres » qui dépose directement sur la section Équipe
|
||||||
|
- **Statuts EV configurables (depuis v2026.5.44)** : clos / résolu / annulé / suspendu éditables depuis Paramètres → EasyVista, matching insensible à la casse / accents / conjugaisons
|
||||||
|
|
||||||
## Versionning — historique et conventions
|
## Versionning — historique et conventions
|
||||||
|
|
||||||
@@ -60,7 +90,7 @@ L'extension a connu **3 systèmes de versionning successifs** :
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 16-17 avril 2026 | Versions de base | `1.0.0`, `2.0.0`, `3.0.0` |
|
| 16-17 avril 2026 | Versions de base | `1.0.0`, `2.0.0`, `3.0.0` |
|
||||||
| 18-20 avril 2026 | SemVer classique | `4.1.3`, `4.2.8`, `5.0.12` |
|
| 18-20 avril 2026 | SemVer classique | `4.1.3`, `4.2.8`, `5.0.12` |
|
||||||
| 21 avril 2026 → maintenant | **`ANNÉE.MAJEURE.PATCH`** | `2026.5.16` → `2026.5.37` |
|
| 21 avril 2026 → maintenant | **`ANNÉE.MAJEURE.PATCH`** | `2026.5.16` → `2026.5.45` |
|
||||||
|
|
||||||
### Format actuel : `ANNÉE.MAJEURE.PATCH`
|
### Format actuel : `ANNÉE.MAJEURE.PATCH`
|
||||||
|
|
||||||
@@ -70,11 +100,11 @@ L'extension a connu **3 systèmes de versionning successifs** :
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `2026` | **Année** | À chaque nouvelle année calendaire |
|
| `2026` | **Année** | À chaque nouvelle année calendaire |
|
||||||
| `5` | **Majeure** | À chaque **gros changement / ajout important** (refonte, nouvelle feature majeure, bump volontaire) |
|
| `5` | **Majeure** | À chaque **gros changement / ajout important** (refonte, nouvelle feature majeure, bump volontaire) |
|
||||||
| `37` | **Patch** | À **chaque livraison** dans la majeure courante (corrections, ajustements, petites features) |
|
| `40` | **Patch** | À **chaque livraison** dans la majeure courante (corrections, ajustements, petites features) |
|
||||||
|
|
||||||
Exemples :
|
Exemples :
|
||||||
- `2026.5.16` → `2026.5.17` : petite correction ou ajustement (patch)
|
- `2026.5.16` → `2026.5.17` : petite correction ou ajustement (patch)
|
||||||
- `2026.5.37` → `2026.6.0` : refonte majeure (par exemple nouvelle vue, nouvelle architecture)
|
- `2026.5.40` → `2026.6.0` : refonte majeure (par exemple nouvelle vue, nouvelle architecture)
|
||||||
- `2026.x.y` → `2027.0.0` : passage à la nouvelle année
|
- `2026.x.y` → `2027.0.0` : passage à la nouvelle année
|
||||||
|
|
||||||
Le numéro de **majeure** n'est **pas** un mois et **pas** un chiffre lié au calendrier — c'est un compteur de versions importantes propre au projet (la `5` actuelle continue le `5.x` qui précédait, repris tel quel lors du passage au format annuel).
|
Le numéro de **majeure** n'est **pas** un mois et **pas** un chiffre lié au calendrier — c'est un compteur de versions importantes propre au projet (la `5` actuelle continue le `5.x` qui précédait, repris tel quel lors du passage au format annuel).
|
||||||
@@ -83,87 +113,92 @@ Le numéro de **majeure** n'est **pas** un mois et **pas** un chiffre lié au ca
|
|||||||
|
|
||||||
## Versions notables
|
## Versions notables
|
||||||
|
|
||||||
### `v2026.5.37` (latest, 25 avril 2026) — Refonte vue horizontale
|
### `v2026.5.45` (latest, 8 mai 2026) — Dock latéral drag&drop, fix verdicts ghost, multi-onglets EZV résolu
|
||||||
- Topbar supprimée en vue horizontale, tout passe en sidebar
|
- **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.
|
||||||
- User-badge + titre + bouton "Aujourd'hui" + date/heure + sélecteur + flèches + stats dans sidebar
|
- **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.
|
||||||
- Banderole pompier masquée (badge + barre rouge gauche conservés)
|
- **Drag&drop bloqué** sur les interventions « Fait » (gris), « Clos » (vert ✓✓), statut Clôturé/Résolu/Terminé, et pendant l’analyse de disparition d’un ghost.
|
||||||
|
- **Issue #3** : coches « Absences récurrentes » retenues (merge propre au lieu d’écrasement) lors d’un changement de groupe.
|
||||||
|
- **Issue #4** : multi-onglets EZV — permission optionnelle \`cookies\` + listener \`cookieChanged\`, la session reste valable après reconnexion + fermeture d’un onglet EZV.
|
||||||
|
- **Issue #5** : copie de référence dans une popup épinglée fonctionne.
|
||||||
|
- **Issue #6** : popup épinglée passe au premier plan au clic.
|
||||||
|
- **Issue #7** : plus de notification « +2 min » fantôme après prolongation.
|
||||||
|
- **Issue #8** : pompier absent toute la journée n’est plus compté.
|
||||||
|
- **Auto-fermeture** des popups dépinglés quand la souris quitte leur zone et celle des cards liées.
|
||||||
|
- **Tooltip / contact nettoyé** : les labels « fiche » (Étage/Bureau/Service/etc.) collés à la valeur du contact sont retirés.
|
||||||
|
|
||||||
### `v2026.5.36` — Sidebar verticale
|
### `v2026.5.44` (1 mai 2026) — Refonte topbar, personnalisation Apparence, onboarding équipe, fix #1
|
||||||
- Wrapper flex-row `#horizontal-wrapper` [sidebar 200px] + [main]
|
- **Topbar refondue (vue classique)** : sélecteur de date du planning ancré au centre absolu (ne se décale plus quand le bouton « Arrêter » apparaît), bouton « Aujourd'hui » en toutes lettres, horloge contextuelle dans un cadre encadré.
|
||||||
- Déplacement physique des éléments via `ELEMENTS_TO_RELOCATE`
|
- **Personnalisation Apparence** : couleur de la barre du haut (12 presets + picker custom + champ hex), contraste de texte calculé automatiquement par luminance ; police de l'application avec 28 choix (Arial, Helvetica, Verdana, Tahoma, Trebuchet, Calibri, Segoe UI, Times New Roman, Georgia, Cambria, Garamond, Palatino, Courier, Consolas, Comic Sans, Impact, …) appliquée à toute la page.
|
||||||
- Restauration propre en vue classique
|
- **Vue horizontale** : bloc Aujourd'hui + horloge dans le même cadre que la classique, barre verte verticale à droite des mini-cards clos/résolu, sidebar adopte la couleur de topbar custom de manière cohérente.
|
||||||
|
- **Stats globales** : nouveau compteur « X faits / Y clos » ; vue classique avec « // » après clos, vue horizontale avec barre horizontale 1px de séparation.
|
||||||
|
- **Onboarding équipe** : carte centrée propre (icône, titre, description, bouton « Ouvrir paramètres ») au lieu du bandeau d'erreur ; le bouton ouvre directement la section Équipe.
|
||||||
|
- **Refresh / cache / verdicts ghost** : rafraîchissement séquentiel avec arrêt instantané (AbortController), checksum frais via basicAutoComplete + redirectHeader, cache merge robuste, verdicts unifiés (✓✓ clos, ✓ Fait, ✓ Suspendu jaune, retrait silencieux pour cancelled), mode diagnostic optionnel.
|
||||||
|
- **Issue #1 (Pompier + Absence)** : les deux badges s'affichent désormais avec un séparateur `/` au lieu de masquer l'absence.
|
||||||
|
- **Bugfix divers** : absences récurrentes restaurées au switch de groupe, barre de progression / bannière session expirée suivent la hauteur dynamique de la topbar (`--topbar-height` via `ResizeObserver`), description action décodée proprement, flèche `↗` retirée des références cliquables.
|
||||||
|
|
||||||
### `v2026.5.32` — Vue horizontale togglable
|
### `v2026.5.43` (27 avril 2026) — Fix Firefox : menu dock + stabilité popup pin/unpin
|
||||||
- Bouton ⊞ "Vue" dans popup user-badge
|
- Firefox : le menu hover sur les pastilles du dock (popup réduit) se
|
||||||
- Chaque tech = 1 ligne horizontale compacte
|
positionne désormais correctement au-dessus de la pastille.
|
||||||
- localStorage `view_mode`
|
- Pin/unpin : la popup épinglée ne bouge plus et garde la même taille
|
||||||
|
quand on la dé-épingle / re-épingle.
|
||||||
|
|
||||||
### `v2026.5.27` — Classification absences
|
### `v2026.5.42` — Nettoyage de commentaires + exemples génériques
|
||||||
- ABSENCE_LABELS : `^(cong[ée]s|maladie|pompier)$`
|
- Uniformisation des exemples utilisés dans les commentaires de `viewer.js`
|
||||||
- Couleurs catégories
|
(parsing contacts/lieux/références/codes-barres) en placeholders abstraits.
|
||||||
- Topbar une ligne : "Jeudi 23.04.26 • 21:55"
|
Comportement runtime strictement inchangé.
|
||||||
|
|
||||||
### `v4.2.3` — Grande popup timeline persistante
|
### `v2026.5.41` — Suppression des hardcodes + UX admin + thème unifié
|
||||||
- Clic segment timeline = popup persistante
|
- **Plus aucun hardcode runtime** pour le groupe EV, les domaines, la liste de techniciens ou les absences récurrentes. Tout est piloté par `admin_config` (chrome.storage.local), persisté entre les mises à jour.
|
||||||
- Hover = popup qui suit la souris
|
- **Au 1er install** : aucun tech sélectionné, aucune absence récurrente. Le viewer affiche un message *"Aucun technicien sélectionné"* tant que l'utilisateur n'a rien configuré dans Paramètres → Équipe.
|
||||||
|
- **Édition des domaines** : `chrome.permissions.request()` au save quand l'utilisateur saisit un domaine custom (au-delà des 2 défauts). Manifest `optional_host_permissions: ["https://*/*"]` pour accepter n'importe quel domaine HTTPS après accord du navigateur.
|
||||||
|
- **Heures de la journée** : bouton ✓ Appliquer explicite (au lieu de save direct), toast de confirmation, refetch automatique du planning. Synchronisation effective avec les requêtes EV (`day_start_hour` / `day_end_hour` / `begin_hour` / `end_hour`) — avant, l'affichage changeait mais les requêtes restaient sur 8h-19h hardcodés.
|
||||||
|
- **Thème unifié** : le toggle 🌙 de la topbar et le sélecteur Apparence du panel admin écrivent dans la même clé (`cfg.theme`). Le mode "Automatique" est résolu en JS via `prefers-color-scheme` (le CSS n'avait pas de bloc `@media`, ce qui faisait retomber sur le clair même quand l'OS était en sombre). Listener `matchMedia` pour bascule live en mode auto.
|
||||||
|
- **Conflit absence/réservation × intervention** : si une intervention est planifiée pendant qu'un tech a une absence (toute la journée ou demi-journée) ou une réservation au même créneau, sa carte est peinte en **rouge plein** (intervention conflictuelle). Logique : full-day → toutes en rouge ; partiel → seules celles en chevauchement.
|
||||||
|
- **Absences récurrentes génériques** : suppression de la fonction hardcodée `isXXXAbsentFriday()`. L'absence récurrente est désormais générique : `RECURRING_ABSENCES[tech.id]` lit `cfg.recurringAbsences` et le label "Absent le X" est calculé dynamiquement depuis le jour de la semaine.
|
||||||
|
- **Notifications au-dessus du flou** : z-index `.toast-stack` relevé à 11000 (le panel admin est à 10000) pour que les toasts de feedback restent visibles quand l'admin est ouvert.
|
||||||
|
- **Vue horizontale** : popups au survol/clic limités aux candidats `dessous`/`dessus` (la sidebar à gauche et la timeline pleine largeur rendent gauche/droite peu praticables).
|
||||||
|
- **Tri équipe** : inclus d'abord, puis exclus, alphabétique dans chaque sous-groupe (ne saute plus quand on coche/décoche).
|
||||||
|
- **Auto-refresh à l'enregistrement** : ajouter/retirer un tech, changer de groupe, modifier les domaines → le planning se met à jour immédiatement (plus besoin de recharger l'extension manuellement).
|
||||||
|
- **Onglet Statuts retiré** (placeholder lecture-seule, jamais utilisé).
|
||||||
|
- **Ménage de code** : suppression de `CACHE_DAYS` (inutilisée), `LS_THEME` (clé localStorage obsolète), commentaire historique sur `initAdminMenu()`. Aucun symbole orphelin restant.
|
||||||
|
|
||||||
### `v4.1.3` — Tooltips épinglables
|
### `v2026.5.40` — Sélection groupe EV + édition domaines + tri équipe + vue horizontale enrichie
|
||||||
- Introduction de `pinTooltip`
|
- **Onglet Équipe** : sélecteur de groupe EasyVista (SI-CSS, SI-EXT, …) en tête de section, détecté automatiquement à l'ouverture du panel via le `<select id="plan_group_id">` de la page Planning EV. Robuste aux ajouts/renommages côté EV.
|
||||||
|
- ID groupe affiché en italique (ex: `ID groupe : 191`).
|
||||||
|
- Quand on change de groupe, la liste d'équipe se rafraîchit automatiquement avec les membres du nouveau groupe.
|
||||||
|
- Plus de bouton "Détecter" : tout est auto à l'ouverture.
|
||||||
|
- Tri double des techniciens : inclus d'abord, puis exclus, alphabétique dans chaque sous-groupe.
|
||||||
|
- **Onglet EasyVista** : édition manuelle des deux domaines (interne DGNSI / externe Internet), bouton Réinitialiser, normalisation auto des URLs.
|
||||||
|
- **Onglet Statuts retiré** (placeholder lecture-seule).
|
||||||
|
- **Vue horizontale enrichie** : chaque segment timeline contient désormais une barre verticale couleur catégorie à gauche, la référence (ex: `SYYMMDD_NNNNN`) en gras, et la ville en gris muted. Hauteur passée de 22px à 32px.
|
||||||
|
- **Réorganisation interne du repo** : `src/` pour les sources, `dist/` généré, `Autres/` pour build.sh + meta files (LICENSE, README, CHANGELOG), `Builds/` pour les artefacts distribués.
|
||||||
|
|
||||||
### `v1.0.0` (16 avril 2026) — Initiale
|
### `v2026.5.39` — Séparation Matin / Après-midi + Apparence
|
||||||
- Premier viewer EasyVista pour le canton
|
- Pills "MATIN" / "APRÈS-MIDI" entre les interventions de chaque tech
|
||||||
|
- Section **Apparence** dans les paramètres : thème, taille du texte, durée du cache, heures de la journée
|
||||||
|
|
||||||
Voir [CHANGELOG.md](CHANGELOG.md) pour l'historique complet (40 versions taggées).
|
➡ Pour l'historique complet (40+ versions depuis le 16 avril 2026), voir le **[CHANGELOG.md](CHANGELOG.md)** ou la **[page wiki Versions](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Versions)**.
|
||||||
|
|
||||||
## Architecture technique
|
## Architecture technique
|
||||||
|
|
||||||
```
|
```
|
||||||
manifest.json # Manifest V3 (Chrome) + gecko_settings (Firefox)
|
Planification/ # Layout du repo Gitea (public)
|
||||||
background.js # Worker fond : fetch planning XML, gestion session, fetch fiches
|
├── src/ # Sources de l'extension (chargées par le navigateur)
|
||||||
viewer.html # Interface principale
|
│ ├── manifest.json # Manifest V3 (Chrome) + browser_specific_settings (Firefox)
|
||||||
viewer.js # Logique (~9000 lignes) — voir détail ci-dessous
|
│ ├── background.js # Service worker (~1 600 lignes)
|
||||||
viewer.css # Styles + thèmes clair/sombre
|
│ ├── viewer.html # Interface principale
|
||||||
icons/ # icon16, icon48, icon128
|
│ ├── viewer.js # Logique UI (~10 700 lignes)
|
||||||
|
│ ├── viewer.css # Styles + thèmes clair/sombre (~4 800 lignes)
|
||||||
|
│ └── icons/ # icon16, icon48, icon128
|
||||||
|
├── build.sh # Génère dist/chromium/, dist/firefox/, .zip, .xpi, met à jour firefox-updates.json
|
||||||
|
├── firefox-updates.json # Manifest auto-update Firefox (servi via update_url)
|
||||||
|
├── README.md
|
||||||
|
├── CHANGELOG.md
|
||||||
|
├── LICENSE # MIT
|
||||||
|
└── .gitignore
|
||||||
```
|
```
|
||||||
|
|
||||||
### `viewer.js` — fonctions clés
|
➡ Pour le détail des composants, fonctions clés et flux de données, voir la **[page wiki Architecture](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Architecture)**.
|
||||||
|
|
||||||
| Fonction | Introduite | Rôle |
|
|
||||||
|---|---|---|
|
|
||||||
| `loadForDate` | v1.0.0 | Fetch + parse planning pour une date donnée |
|
|
||||||
| `buildTooltipHTML` | v1.0.0 | Construction HTML du tooltip d'intervention |
|
|
||||||
| `pinTooltip` | v4.1.3 | Épingler un tooltip (le rendre permanent) |
|
|
||||||
| `bindTimelinePopover` | v4.2.3 | Lier popover timeline aux segments |
|
|
||||||
| `showTimelinePopover` | v4.2.3 | Afficher popover persistante |
|
|
||||||
| `openPersistentTimelinePopup` | v4.2.3 | Grande popup détaillée |
|
|
||||||
| `setTooltipViewportPosition` | v4.2.4 | Détection auto fixed/abs |
|
|
||||||
| `_softUnpinPopup` | v4.3.3 | Désépinglage mou (popup reste visible) |
|
|
||||||
| `initAppClock` | v5.0.0 | Horloge HH:MM topbar |
|
|
||||||
| `initSessionTimer` | v5.0.0 | Compteur session EV (tick 1s) |
|
|
||||||
| `initAdminMenu` | v5.0.0 | Menu admin (5 clics titre) |
|
|
||||||
| `_applyViewMode` | v2026.5.32 | Toggle vue classique/horizontale |
|
|
||||||
| `_maybeRetryFetchUser` | v2026.5.34 | Relance opportuniste fetch user |
|
|
||||||
| `positionTooltipAnchored` | v2026.5.34 | Positionnement unifié (4 candidats) |
|
|
||||||
|
|
||||||
### Constantes persistantes (toutes versions)
|
|
||||||
|
|
||||||
- 8 techs hardcodés : `76272,83725,66635,92235,90070,40944,72485,86874`
|
|
||||||
- Pillonel Olivier (ID 40944) : absent tous les vendredis (hardcodé)
|
|
||||||
- Group ID EasyVista : `191`
|
|
||||||
- GUIDs forms EV :
|
|
||||||
- Demande : `S={C99ECD05-3D48-4C62-ABF0-66292053AED6}`
|
|
||||||
- Incident : `I={07ED9C68-6172-48EA-8A58-90912B0A283E}`
|
|
||||||
- SSO : Canton ForgeRock OpenAM
|
|
||||||
- Storage keys : `admin_config`, `view_mode` (depuis v2026.5.32)
|
|
||||||
- Domaines : `itsma.etat-de-vaud.ch` (interne), `itsma.vd.ch` (externe SSO)
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Firefox
|
|
||||||
Télécharger le `.xpi` signé depuis le serveur de mises à jour interne, ou drag-and-drop dans `about:addons`.
|
|
||||||
|
|
||||||
### Chrome / Edge
|
|
||||||
Mode développeur : décompresser le ZIP et charger en tant qu'extension non empaquetée.
|
|
||||||
|
|
||||||
## Développement
|
## Développement
|
||||||
|
|
||||||
@@ -171,17 +206,22 @@ Mode développeur : décompresser le ZIP et charger en tant qu'extension non emp
|
|||||||
git clone https://gitea.netaplaid.ch/FroSteel/Planification.git
|
git clone https://gitea.netaplaid.ch/FroSteel/Planification.git
|
||||||
cd Planification
|
cd Planification
|
||||||
|
|
||||||
# Pour packager une nouvelle version :
|
# Modifier les sources dans src/
|
||||||
# 1. modifier le code
|
# Bumper src/manifest.json + entrée CHANGELOG.md
|
||||||
# 2. bump version dans manifest.json
|
./build.sh
|
||||||
# 3. zip + xpi
|
# → dist/chromium/, dist/firefox/, dist/*.zip, dist/*.xpi
|
||||||
|
# → firefox-updates.json mis à jour (sha256 .xpi NON SIGNÉ)
|
||||||
|
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "Version YYYY.M.PATCH — description"
|
git commit -m "vYYYY.M.PATCH — description"
|
||||||
git tag vYYYY.M.PATCH
|
git tag vYYYY.M.PATCH
|
||||||
git push origin main
|
git push origin main vYYYY.M.PATCH
|
||||||
git push --tags
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Pour Firefox : signer le `.xpi` sur AMO en mode "On your own" (Unlisted),
|
||||||
|
remplacer l'asset `.xpi` de la release Gitea, puis mettre à jour le sha256
|
||||||
|
de cette version dans `firefox-updates.json`.
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
[MIT License](LICENSE) — Copyright (c) 2026 Quentin Rouiller
|
[MIT License](LICENSE) — Copyright (c) 2026 Quentin Rouiller
|
||||||
@@ -190,3 +230,4 @@ git push --tags
|
|||||||
|
|
||||||
**Quentin Rouiller** (QRO)
|
**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)
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
###############################################################################
|
||||||
|
# build.sh — génère dist/chromium/, dist/firefox/, et les archives .zip / .xpi
|
||||||
|
# à partir du code source dans src/.
|
||||||
|
#
|
||||||
|
# Usage : ./build.sh
|
||||||
|
###############################################################################
|
||||||
|
set -e
|
||||||
|
# Le script est dans Autres/ — on remonte d'un cran pour se placer à la
|
||||||
|
# racine du projet, où se trouvent src/ et dist/.
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
VERSION=$(python3 -c "import json; print(json.load(open('src/manifest.json'))['version'])")
|
||||||
|
echo "==> Build Planification v$VERSION"
|
||||||
|
|
||||||
|
rm -rf dist
|
||||||
|
mkdir -p dist/chromium dist/firefox
|
||||||
|
|
||||||
|
# ---- Chromium : copie src/ tel quel (manifest sans gecko_settings) ----
|
||||||
|
cp -r src/* dist/chromium/
|
||||||
|
echo " ✓ dist/chromium/ ($(du -sh dist/chromium | cut -f1))"
|
||||||
|
|
||||||
|
# ---- Firefox : copie src/ + manifest avec browser_specific_settings ----
|
||||||
|
cp -r src/* dist/firefox/
|
||||||
|
python3 - <<EOF
|
||||||
|
import json
|
||||||
|
with open('src/manifest.json', 'r') as f: m = json.load(f)
|
||||||
|
m['browser_specific_settings'] = {
|
||||||
|
'gecko': {
|
||||||
|
'id': 'planification-dgnsi@netaplaid.ch',
|
||||||
|
'strict_min_version': '140.0',
|
||||||
|
'update_url': 'https://gitea.netaplaid.ch/FroSteel/Planification/raw/branch/main/firefox-updates.json',
|
||||||
|
'data_collection_permissions': {'required': ['none']}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Firefox MV3 ne supporte pas (encore) 'service_worker' → AMO rejette.
|
||||||
|
# On ajoute 'scripts' (event page) comme fallback compatible Firefox.
|
||||||
|
# Chrome ignore 'scripts' quand 'service_worker' est présent ; Firefox
|
||||||
|
# ignore 'service_worker' et utilise 'scripts'. Les deux navigateurs
|
||||||
|
# chargent ainsi le même background.js.
|
||||||
|
bg = m.get('background', {})
|
||||||
|
if 'scripts' not in bg:
|
||||||
|
bg['scripts'] = ['background.js']
|
||||||
|
m['background'] = bg
|
||||||
|
with open('dist/firefox/manifest.json', 'w') as f:
|
||||||
|
json.dump(m, f, indent=2, ensure_ascii=False)
|
||||||
|
f.write('\n')
|
||||||
|
EOF
|
||||||
|
echo " ✓ dist/firefox/ ($(du -sh dist/firefox | cut -f1))"
|
||||||
|
|
||||||
|
# ---- Archives ZIP / XPI prêtes à distribuer ----
|
||||||
|
cd dist/chromium && zip -rq "../planification-v${VERSION}-chromium.zip" . && cd ../..
|
||||||
|
cd dist/firefox && zip -rq "../planification-v${VERSION}-firefox.xpi" . && cd ../..
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Builds prêts dans dist/"
|
||||||
|
ls -la dist/*.zip dist/*.xpi 2>/dev/null
|
||||||
|
|
||||||
|
# ---- firefox-updates.json : ajout/mise à jour de l'entrée pour cette version
|
||||||
|
# (sha256 du .xpi NON SIGNÉ — sera remplacé par celui du .xpi signé après AMO).
|
||||||
|
python3 - <<EOF
|
||||||
|
import json, hashlib, os
|
||||||
|
xpi = f"dist/planification-v${VERSION}-firefox.xpi"
|
||||||
|
with open(xpi, 'rb') as f: sha = hashlib.sha256(f.read()).hexdigest()
|
||||||
|
|
||||||
|
JSON_PATH = "firefox-updates.json"
|
||||||
|
ADDON_ID = "planification-dgnsi@netaplaid.ch"
|
||||||
|
update_link = f"https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v${VERSION}/planification-v${VERSION}-firefox.xpi"
|
||||||
|
|
||||||
|
if os.path.exists(JSON_PATH):
|
||||||
|
with open(JSON_PATH) as f: data = json.load(f)
|
||||||
|
else:
|
||||||
|
data = {"addons": {ADDON_ID: {"updates": []}}}
|
||||||
|
|
||||||
|
addon = data.setdefault("addons", {}).setdefault(ADDON_ID, {"updates": []})
|
||||||
|
updates = addon.setdefault("updates", [])
|
||||||
|
|
||||||
|
# Retirer toute entrée existante pour cette version (idempotent)
|
||||||
|
updates = [u for u in updates if u.get("version") != "${VERSION}"]
|
||||||
|
# Ajouter en tête
|
||||||
|
updates.insert(0, {
|
||||||
|
"version": "${VERSION}",
|
||||||
|
"update_link": update_link,
|
||||||
|
"update_hash": "sha256:" + sha,
|
||||||
|
})
|
||||||
|
addon["updates"] = updates
|
||||||
|
|
||||||
|
with open(JSON_PATH, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
f.write('\n')
|
||||||
|
|
||||||
|
print(f" ✓ firefox-updates.json mis à jour (sha256 NON SIGNÉ : {sha[:16]}…)")
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Pour Chrome : charger dist/chromium/ en mode développeur"
|
||||||
|
echo "Pour Firefox : signer dist/planification-v${VERSION}-firefox.xpi sur AMO"
|
||||||
|
echo " Après signature, remplacer le sha256 dans firefox-updates.json par celui du .xpi signé."
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"addons": {
|
||||||
|
"planification-dgnsi@netaplaid.ch": {
|
||||||
|
"updates": [
|
||||||
|
{
|
||||||
|
"version": "2026.5.45",
|
||||||
|
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.45/planification-v2026.5.45-firefox.xpi",
|
||||||
|
"update_hash": "sha256:42ac47aeda23912e80051fc941ba45a0dec74ec4a6c509a25270c27b73dddead"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "2026.5.44",
|
||||||
|
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.44/planification-v2026.5.44-firefox.xpi",
|
||||||
|
"update_hash": "sha256:e56e87d59c465e5df828b18d74376f561bf34e81e21bf4d70989a709e89217e0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "2026.5.43",
|
||||||
|
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.43/planification-v2026.5.43-firefox.xpi",
|
||||||
|
"update_hash": "sha256:7052200fab3c9266d5b809398a00dac768679ab2e96e4e147e4bb86c4ab648e5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "2026.5.42",
|
||||||
|
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.42/planification-v2026.5.42-firefox.xpi",
|
||||||
|
"update_hash": "sha256:a5291a1eab6768d430dfc1cf032f93aeb788083da620ae0568b9dee68f14734d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "2026.5.41",
|
||||||
|
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.41/planification-v2026.5.41-firefox.xpi",
|
||||||
|
"update_hash": "sha256:fbf7d8a57ad060306cb43f3db3a5d5b599bb07c75c4ef0dbd0346406bdb6c65b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "2026.5.40",
|
||||||
|
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.40/planification-v2026.5.40-firefox.xpi",
|
||||||
|
"update_hash": "sha256:2ba0758960b931f4211c613c75bbf21b3a250572dddc70d854ff1ecca3220421"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -115,16 +115,121 @@ self.addEventListener("unhandledrejection", (event) => {
|
|||||||
|
|
||||||
LOG.info("boot", "service worker démarré", { version: LOG.version() });
|
LOG.info("boot", "service worker démarré", { version: LOG.version() });
|
||||||
|
|
||||||
// Domaines EasyVista reconnus (interne d'abord, externe en fallback)
|
// ============================================================================
|
||||||
const EV_ORIGINS = [
|
// v2026.5.41 : Configuration runtime — lue depuis admin_config (chrome.storage.local).
|
||||||
"https://itsma.etat-de-vaud.ch",
|
//
|
||||||
"https://itsma.vd.ch"
|
// Les domaines EV et le group_id ont des défauts (filet de sécurité 1er install).
|
||||||
|
// La liste de techniciens, elle, n'a AUCUN défaut : tant que l'utilisateur n'a
|
||||||
|
// rien coché dans Paramètres → Équipe, l'extension ne fetche aucun planning et
|
||||||
|
// invite à configurer.
|
||||||
|
//
|
||||||
|
// chrome.storage.local survit aux mises à jour d'extension → la sélection de
|
||||||
|
// l'utilisateur est conservée d'une version à l'autre.
|
||||||
|
// ============================================================================
|
||||||
|
const DEFAULT_EV_ORIGINS = [
|
||||||
|
"https://itsma.etat-de-vaud.ch", // interne DGNSI
|
||||||
|
"https://itsma.vd.ch" // externe Internet
|
||||||
];
|
];
|
||||||
|
const DEFAULT_GROUP_ID = "191"; // SI-CSS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lit admin_config depuis chrome.storage.local. Retourne {} si absent ou
|
||||||
|
* en cas d'erreur.
|
||||||
|
*/
|
||||||
|
async function getAdminConfig() {
|
||||||
|
try {
|
||||||
|
const stored = await chrome.storage.local.get("admin_config");
|
||||||
|
return stored.admin_config || {};
|
||||||
|
} catch (e) {
|
||||||
|
LOG.warn("config", "getAdminConfig failed, using defaults", e);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Origines EV à surveiller, depuis admin_config ou défaut. */
|
||||||
|
async function getEvOrigins() {
|
||||||
|
const cfg = await getAdminConfig();
|
||||||
|
const o = Array.isArray(cfg.evOrigins) ? cfg.evOrigins.filter(Boolean) : [];
|
||||||
|
return o.length >= 1 ? o : DEFAULT_EV_ORIGINS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Group ID effectif (défaut SI-CSS). */
|
||||||
|
async function getGroupId() {
|
||||||
|
const cfg = await getAdminConfig();
|
||||||
|
return cfg.groupId || DEFAULT_GROUP_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support IDs effectifs (CSV des clés de cfg.team).
|
||||||
|
* Retourne "" si aucun tech sélectionné — l'appelant doit alors signaler
|
||||||
|
* à l'utilisateur d'aller configurer son équipe.
|
||||||
|
*/
|
||||||
|
async function getSupportIds() {
|
||||||
|
const cfg = await getAdminConfig();
|
||||||
|
const ids = Object.keys(cfg.team || {}).filter(Boolean);
|
||||||
|
return ids.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plage horaire d'affichage (heures pleines).
|
||||||
|
* Lue depuis admin_config (Paramètres → Apparence → Heures de la journée),
|
||||||
|
* défaut 8h-18h. Utilisée pour les paramètres day_start_hour / day_end_hour /
|
||||||
|
* begin_hour / end_hour des requêtes EV. Le viewer utilise déjà ces mêmes
|
||||||
|
* valeurs pour dessiner la timeline (cf. _initDayBoundsFromConfig dans viewer.js).
|
||||||
|
*/
|
||||||
|
async function getDayBounds() {
|
||||||
|
const cfg = await getAdminConfig();
|
||||||
|
const start = (typeof cfg.dayStart === "number" && cfg.dayStart >= 0 && cfg.dayStart <= 23) ? cfg.dayStart : 8;
|
||||||
|
const end = (typeof cfg.dayEnd === "number" && cfg.dayEnd > start && cfg.dayEnd <= 24) ? cfg.dayEnd : 18;
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Clic sur l'icône → ouvrir le viewer
|
// Clic sur l'icône → ouvrir le viewer
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// badge "!" clignotant sur l'icône de l'extension quand la session
|
||||||
|
// EasyVista va expirer. Permet à l'utilisateur de voir l'alerte même si
|
||||||
|
// l'onglet planning n'est pas au premier plan.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
let _expirationBlinkInterval = null;
|
||||||
|
let _expirationBlinkTimeout = null;
|
||||||
|
|
||||||
|
function _setExpirationBadge(active, durationMs) {
|
||||||
|
// Stop l'état précédent
|
||||||
|
if (_expirationBlinkInterval) {
|
||||||
|
clearInterval(_expirationBlinkInterval);
|
||||||
|
_expirationBlinkInterval = null;
|
||||||
|
}
|
||||||
|
if (_expirationBlinkTimeout) {
|
||||||
|
clearTimeout(_expirationBlinkTimeout);
|
||||||
|
_expirationBlinkTimeout = null;
|
||||||
|
}
|
||||||
|
try { chrome.action.setBadgeText({ text: "" }); } catch (e) {}
|
||||||
|
|
||||||
|
if (!active) return;
|
||||||
|
|
||||||
|
// Couleur de fond rouge pour le "!"
|
||||||
|
try { chrome.action.setBadgeBackgroundColor({ color: "#d6443d" }); } catch (e) {}
|
||||||
|
let on = true;
|
||||||
|
const tick = () => {
|
||||||
|
try {
|
||||||
|
chrome.action.setBadgeText({ text: on ? "!" : "" });
|
||||||
|
} catch (e) {}
|
||||||
|
on = !on;
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
_expirationBlinkInterval = setInterval(tick, 700);
|
||||||
|
|
||||||
|
if (durationMs && durationMs > 0) {
|
||||||
|
_expirationBlinkTimeout = setTimeout(() => {
|
||||||
|
_setExpirationBadge(false);
|
||||||
|
}, durationMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
chrome.action.onClicked.addListener(async () => {
|
chrome.action.onClicked.addListener(async () => {
|
||||||
const viewerUrl = chrome.runtime.getURL("viewer.html");
|
const viewerUrl = chrome.runtime.getURL("viewer.html");
|
||||||
// Si le viewer est déjà ouvert, on focus cet onglet plutôt que d'en ouvrir un autre
|
// Si le viewer est déjà ouvert, on focus cet onglet plutôt que d'en ouvrir un autre
|
||||||
@@ -144,22 +249,146 @@ chrome.action.onClicked.addListener(async () => {
|
|||||||
/**
|
/**
|
||||||
* Trouve l'onglet EasyVista ouvert et récupère phpsessid + origin.
|
* Trouve l'onglet EasyVista ouvert et récupère phpsessid + origin.
|
||||||
*
|
*
|
||||||
|
* v2026.5.45 (issue #4) : si la permission optionnelle "cookies" est
|
||||||
|
* accordée, on lit le PHPSESSID directement depuis le cookie HttpOnly du
|
||||||
|
* domaine EZV — toujours à jour, immune au PHPSESSID périmé qu'on trouve
|
||||||
|
* dans l'URL des onglets historiques après un relog. Sinon, fallback sur
|
||||||
|
* l'ancienne logique URL (compat sans la permission).
|
||||||
|
*
|
||||||
* @author Quentin Rouiller
|
* @author Quentin Rouiller
|
||||||
*/
|
*/
|
||||||
async function findEasyVistaSession() {
|
async function _hasCookiesPermission() {
|
||||||
// Chercher tous les onglets sur un domaine EasyVista
|
return new Promise(resolve => {
|
||||||
for (const origin of EV_ORIGINS) {
|
try {
|
||||||
|
chrome.permissions.contains({ permissions: ["cookies"] }, granted => {
|
||||||
|
resolve(!!granted);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _readPhpsessidFromCookie(origin) {
|
||||||
|
if (!chrome.cookies) return null;
|
||||||
|
return new Promise(resolve => {
|
||||||
|
try {
|
||||||
|
chrome.cookies.get({ url: origin, name: "PHPSESSID" }, c => {
|
||||||
|
if (!c || !c.value) { resolve(null); return; }
|
||||||
|
resolve({
|
||||||
|
value: c.value,
|
||||||
|
expirationDate: c.expirationDate || null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── findEasyVistaSession ───────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Stratégie unifiée : pour chaque origine EV configurée, on cherche le
|
||||||
|
// PHPSESSID dans cet ordre :
|
||||||
|
// 1. cookie HttpOnly (autoritatif, donne aussi expirationDate)
|
||||||
|
// 2. fallback URL d'un onglet ouvert (?PHPSESSID=…)
|
||||||
|
// On retourne la première origine qui a un onglet ouvert ET un PHPSESSID.
|
||||||
|
//
|
||||||
|
// Anti-rafale : la fonction est appelée par ~16 endroits dans le service
|
||||||
|
// worker, parfois en parallèle. On cache le résultat 200 ms pour éviter
|
||||||
|
// le bombardement de chrome.cookies.get + chrome.tabs.query, et de logs
|
||||||
|
// dupliqués. Le cache est invalidé immédiatement quand un événement
|
||||||
|
// cookies.onChanged tombe (relog, expiration).
|
||||||
|
//
|
||||||
|
// Logs : une seule ligne `LOG.warn` par CHANGEMENT d'état (transition
|
||||||
|
// cookie↔url, nouveau PHPSESSID, ou perte de session). Pas de log
|
||||||
|
// répétitif par appel — utiliser `LOG.info` pour les détails internes.
|
||||||
|
|
||||||
|
let _sessionCache = null;
|
||||||
|
let _sessionCacheTs = 0;
|
||||||
|
// Cache désactivé temporairement (TTL=0) — soupçon que la durée de
|
||||||
|
// fenêtre stale puisse causer une réponse "page de login" (~8 Ko) lors
|
||||||
|
// d'un fetch fiche. À remettre à 200 après confirmation que ce n'était
|
||||||
|
// pas le souci.
|
||||||
|
const _SESSION_CACHE_TTL_MS = 0;
|
||||||
|
let _lastSessionSignature = null;
|
||||||
|
|
||||||
|
function _invalidateSessionCache() {
|
||||||
|
_sessionCache = null;
|
||||||
|
_sessionCacheTs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _findEasyVistaSessionRaw() {
|
||||||
|
const origins = await getEvOrigins();
|
||||||
|
const hasCookies = await _hasCookiesPermission();
|
||||||
|
|
||||||
|
for (const origin of origins) {
|
||||||
const tabs = await chrome.tabs.query({ url: origin + "/*" });
|
const tabs = await chrome.tabs.query({ url: origin + "/*" });
|
||||||
|
if (!tabs.length) continue;
|
||||||
|
|
||||||
|
let phpsessid = null;
|
||||||
|
let expirationDate = null;
|
||||||
|
let source = null;
|
||||||
|
|
||||||
|
if (hasCookies) {
|
||||||
|
const c = await _readPhpsessidFromCookie(origin);
|
||||||
|
if (c && c.value) {
|
||||||
|
phpsessid = c.value;
|
||||||
|
expirationDate = c.expirationDate;
|
||||||
|
source = "cookie";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!phpsessid) {
|
||||||
for (const tab of tabs) {
|
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]+)/);
|
||||||
if (m) {
|
if (m) {
|
||||||
return { phpsessid: m[1], origin: origin, tabId: tab.id };
|
phpsessid = m[1];
|
||||||
|
source = "url";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (phpsessid) {
|
||||||
|
return { phpsessid, origin, tabId: tabs[0].id, source, expirationDate };
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findEasyVistaSession() {
|
||||||
|
const now = Date.now();
|
||||||
|
if (_sessionCache !== null && now - _sessionCacheTs < _SESSION_CACHE_TTL_MS) {
|
||||||
|
return _sessionCache;
|
||||||
|
}
|
||||||
|
if (_sessionCache === null && _sessionCacheTs > 0 && now - _sessionCacheTs < _SESSION_CACHE_TTL_MS) {
|
||||||
|
// Cache négatif (pas de session trouvée à l'instant) — même TTL.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await _findEasyVistaSessionRaw();
|
||||||
|
_sessionCache = result;
|
||||||
|
_sessionCacheTs = now;
|
||||||
|
|
||||||
|
// Log uniquement si l'état change (source, origine, ou PHPSESSID).
|
||||||
|
const sig = result
|
||||||
|
? `${result.source}|${result.origin}|${result.phpsessid.slice(0, 8)}`
|
||||||
|
: "none";
|
||||||
|
if (sig !== _lastSessionSignature) {
|
||||||
|
if (result) {
|
||||||
|
LOG.warn("session",
|
||||||
|
`🔑 PHPSESSID via ${result.source.toUpperCase()} (${result.phpsessid.slice(0, 8)}…) sur ${result.origin}`,
|
||||||
|
{ source: result.source, origin: result.origin,
|
||||||
|
expirationDate: result.expirationDate || null });
|
||||||
|
} else {
|
||||||
|
LOG.warn("session", "❌ Aucun PHPSESSID disponible (pas d'onglet EZV ouvert ou cookie absent)");
|
||||||
|
}
|
||||||
|
_lastSessionSignature = sig;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Fetch helpers (s'exécutent dans le contexte du service worker,
|
// Fetch helpers (s'exécutent dans le contexte du service worker,
|
||||||
// les cookies du domaine sont automatiquement inclus via credentials: include)
|
// les cookies du domaine sont automatiquement inclus via credentials: include)
|
||||||
@@ -175,8 +404,20 @@ async function findEasyVistaSession() {
|
|||||||
* @author Quentin Rouiller
|
* @author Quentin Rouiller
|
||||||
*/
|
*/
|
||||||
async function fetchPlanningXml(origin, phpsessid, unixDate) {
|
async function fetchPlanningXml(origin, phpsessid, unixDate) {
|
||||||
const techIds = "76272,83725,66635,92235,90070,40944,72485,86874";
|
// v2026.5.41 : groupId vient de admin_config (défaut SI-CSS).
|
||||||
const groupId = "191";
|
// techIds vient de admin_config — si vide, on lève une erreur claire pour
|
||||||
|
// que le viewer affiche "Aucun technicien sélectionné" plutôt qu'un planning vide.
|
||||||
|
const groupId = await getGroupId();
|
||||||
|
const techIds = await getSupportIds();
|
||||||
|
if (!techIds) {
|
||||||
|
const err = new Error("no_team_configured");
|
||||||
|
err.kind = "no_team_configured";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
// v2026.5.41 : heures synchronisées avec admin_config (Paramètres → Apparence).
|
||||||
|
// EV utilise day_end_hour exclusif (la plage rendue va jusqu'à end-1:59),
|
||||||
|
// donc on envoie end+1 pour que la dernière heure pleine soit incluse.
|
||||||
|
const { start, end } = await getDayBounds();
|
||||||
const url =
|
const url =
|
||||||
`${origin}/planning_xhr.php` +
|
`${origin}/planning_xhr.php` +
|
||||||
`?PHPSESSID=${encodeURIComponent(phpsessid)}` +
|
`?PHPSESSID=${encodeURIComponent(phpsessid)}` +
|
||||||
@@ -190,8 +431,8 @@ async function fetchPlanningXml(origin, phpsessid, unixDate) {
|
|||||||
`&end_date_label=Date` +
|
`&end_date_label=Date` +
|
||||||
`&click_here_label=Ici` +
|
`&click_here_label=Ici` +
|
||||||
`&mail_title=mail` +
|
`&mail_title=mail` +
|
||||||
`&day_start_hour=8` +
|
`&day_start_hour=${start}` +
|
||||||
`&day_end_hour=19`;
|
`&day_end_hour=${end + 1}`;
|
||||||
// v2026.5.38 : on retire les logs verbose à chaque fetch (URL/status/taille).
|
// v2026.5.38 : on retire les logs verbose à chaque fetch (URL/status/taille).
|
||||||
// En cas de souci, le throw plus bas porte assez d'info pour debug.
|
// En cas de souci, le throw plus bas porte assez d'info pour debug.
|
||||||
const r = await evFetch(url, origin);
|
const r = await evFetch(url, origin);
|
||||||
@@ -222,14 +463,39 @@ async function fetchPlanningXml(origin, phpsessid, unixDate) {
|
|||||||
* @param {string} origin - origine EasyVista (pour construire le Referer)
|
* @param {string} origin - origine EasyVista (pour construire le Referer)
|
||||||
* @param {object} [opts] - options fetch (method, body, headers supplémentaires)
|
* @param {object} [opts] - options fetch (method, body, headers supplémentaires)
|
||||||
*/
|
*/
|
||||||
|
// registre global des AbortController des fetchs EV en vol. Permet
|
||||||
|
// au foreground (viewer.js) d'envoyer un message "abortAllFetches" pour
|
||||||
|
// tuer instantanément les requêtes en cours quand l'user clique "Arrêter".
|
||||||
|
const _evFetchControllers = new Set();
|
||||||
|
function _abortAllEvFetches() {
|
||||||
|
for (const c of _evFetchControllers) {
|
||||||
|
try { c.abort(); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
_evFetchControllers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
async function evFetch(url, origin, opts = {}) {
|
async function evFetch(url, origin, opts = {}) {
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
"Referer": `${origin}/index.php?eventName=HelpDesk_PlanningItem`,
|
"Referer": `${origin}/index.php?eventName=HelpDesk_PlanningItem`,
|
||||||
"X-Requested-With": "XMLHttpRequest"
|
"X-Requested-With": "XMLHttpRequest"
|
||||||
};
|
};
|
||||||
const headers = Object.assign({}, defaultHeaders, opts.headers || {});
|
const headers = Object.assign({}, defaultHeaders, opts.headers || {});
|
||||||
const fetchOpts = Object.assign({ credentials: "include" }, opts, { headers });
|
// on ne remplace pas un signal explicitement passé par l'appelant.
|
||||||
|
let controller = null;
|
||||||
|
if (!opts.signal) {
|
||||||
|
controller = new AbortController();
|
||||||
|
_evFetchControllers.add(controller);
|
||||||
|
}
|
||||||
|
const fetchOpts = Object.assign(
|
||||||
|
{ credentials: "include" },
|
||||||
|
opts,
|
||||||
|
{ headers, signal: opts.signal || (controller && controller.signal) }
|
||||||
|
);
|
||||||
|
try {
|
||||||
return await fetch(url, fetchOpts);
|
return await fetch(url, fetchOpts);
|
||||||
|
} finally {
|
||||||
|
if (controller) _evFetchControllers.delete(controller);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -299,10 +565,10 @@ async function fetchFicheHtml(origin, phpsessid, formLink) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sinon : on retourne ce qu'on a
|
// on signale au foreground si la dernière réponse est tronquée pour
|
||||||
return html;
|
// qu'il puisse afficher un ⚠ et probe la session.
|
||||||
|
return { html, truncated: html.length < MIN_VALID_SIZE, size: html.length };
|
||||||
}
|
}
|
||||||
// Ne devrait pas arriver (la boucle fait return avant)
|
|
||||||
throw new Error("fetchFicheHtml: max retries reached");
|
throw new Error("fetchFicheHtml: max retries reached");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,17 +853,26 @@ function watchReconnectTabForIamLogin(tabId) {
|
|||||||
*/
|
*/
|
||||||
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)}`;
|
||||||
const resp = await fetch(url, {
|
// v2026.5.40 : on passe par evFetch() qui ajoute les headers Referer +
|
||||||
|
// X-Requested-With attendus par EV. Sans ça, /index.php renvoyait une
|
||||||
|
// page intermédiaire SSO sans le bloc .profile-info → user_null perpétuel.
|
||||||
|
const resp = await evFetch(url, origin, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include",
|
|
||||||
headers: { "Accept": "text/html,*/*" }
|
headers: { "Accept": "text/html,*/*" }
|
||||||
});
|
});
|
||||||
// v4.2 : cette fonction est lancée en tâche de fond au démarrage. Si la
|
// v4.2 : cette fonction est lancée en tâche de fond au démarrage. Si la
|
||||||
// session est expirée ou EV inaccessible, on retourne juste null — le
|
// session est expirée ou EV inaccessible, on retourne juste null — le
|
||||||
// planning lui-même déclenchera l'écran d'erreur approprié.
|
// planning lui-même déclenchera l'écran d'erreur approprié.
|
||||||
if (!resp.ok) return null;
|
if (!resp.ok) {
|
||||||
|
LOG.warn("currentUser", "fetch /index.php non-OK", { status: resp.status });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const html = await resp.text();
|
const html = await resp.text();
|
||||||
if (looksLikeLoginPage(html)) return null;
|
if (looksLikeLoginPage(html)) {
|
||||||
|
LOG.warn("currentUser", "page de login detectee — session probablement intermediaire SSO");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
LOG.info("currentUser", "HTML index.php recu", { taille: html.length, hasProfileInfo: /class=["']profile-info["']/.test(html) });
|
||||||
|
|
||||||
// v4.2.2 : patterns spécifiques à la structure EasyVista réelle du Canton
|
// v4.2.2 : patterns spécifiques à la structure EasyVista réelle du Canton
|
||||||
// de Vaud (identifiés à partir du HTML de la page d'accueil). L'user est
|
// de Vaud (identifiés à partir du HTML de la page d'accueil). L'user est
|
||||||
@@ -669,7 +944,18 @@ async function fetchCurrentUser(origin, phpsessid) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name && !login && !service) return null;
|
if (!name && !login && !service) {
|
||||||
|
// v2026.5.40 : log diagnostic pour comprendre pourquoi l'extraction echoue
|
||||||
|
LOG.warn("currentUser", "aucun nom/service/login extrait du HTML",
|
||||||
|
{
|
||||||
|
taille: html.length,
|
||||||
|
hasH5: /class=["']h5["']/.test(html),
|
||||||
|
hasProfileInfo: /class=["']profile-info["']/.test(html),
|
||||||
|
hasEvDropdown: /class=["'][^"']*ev-employee-dropdown/.test(html),
|
||||||
|
snippet: html.substring(0, 200).replace(/\s+/g, " ")
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return { name, login, service };
|
return { name, login, service };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,6 +980,8 @@ async function submitAbsence(origin, phpsessid, opts) {
|
|||||||
const emplIds = (opts.techIds || []).join(",");
|
const emplIds = (opts.techIds || []).join(",");
|
||||||
if (!emplIds) throw new Error("Aucun technicien sélectionné");
|
if (!emplIds) throw new Error("Aucun technicien sélectionné");
|
||||||
|
|
||||||
|
// v2026.5.41 : heures synchronisées avec admin_config.
|
||||||
|
const { start, end } = await getDayBounds();
|
||||||
const internalurltime = Math.floor(Date.now() / 1000);
|
const internalurltime = Math.floor(Date.now() / 1000);
|
||||||
const url = `${origin}/include/components/staff/planning/plan_set_holidays_popup.php`
|
const url = `${origin}/include/components/staff/planning/plan_set_holidays_popup.php`
|
||||||
+ `?PHPSESSID=${encodeURIComponent(phpsessid)}`
|
+ `?PHPSESSID=${encodeURIComponent(phpsessid)}`
|
||||||
@@ -702,8 +990,8 @@ async function submitAbsence(origin, phpsessid, opts) {
|
|||||||
+ `&ROOT_DIRECTORY=${encodeURIComponent("/ccv/data/www/itsma/htdocs/")}`
|
+ `&ROOT_DIRECTORY=${encodeURIComponent("/ccv/data/www/itsma/htdocs/")}`
|
||||||
+ `¤t_date=${encodeURIComponent(opts.currentDate)}`
|
+ `¤t_date=${encodeURIComponent(opts.currentDate)}`
|
||||||
+ `&empl_ids=${encodeURIComponent(emplIds)}`
|
+ `&empl_ids=${encodeURIComponent(emplIds)}`
|
||||||
+ `&begin_hour=8`
|
+ `&begin_hour=${start}`
|
||||||
+ `&end_hour=18`
|
+ `&end_hour=${end}`
|
||||||
+ `&plagehoraire=0`;
|
+ `&plagehoraire=0`;
|
||||||
|
|
||||||
const body = new URLSearchParams();
|
const body = new URLSearchParams();
|
||||||
@@ -765,6 +1053,8 @@ async function submitDouchette(origin, phpsessid, opts) {
|
|||||||
const techIds = opts.techIds || [];
|
const techIds = opts.techIds || [];
|
||||||
if (techIds.length === 0) throw new Error("Aucun technicien sélectionné");
|
if (techIds.length === 0) throw new Error("Aucun technicien sélectionné");
|
||||||
|
|
||||||
|
// v2026.5.41 : heures synchronisées avec admin_config.
|
||||||
|
const { start, end } = await getDayBounds();
|
||||||
const emplIds = techIds.join(",");
|
const emplIds = techIds.join(",");
|
||||||
const internalurltime = Math.floor(Date.now() / 1000);
|
const internalurltime = Math.floor(Date.now() / 1000);
|
||||||
const url = `${origin}/include/components/staff/planning/plan_set_tech_planif_popup.php`
|
const url = `${origin}/include/components/staff/planning/plan_set_tech_planif_popup.php`
|
||||||
@@ -774,8 +1064,8 @@ async function submitDouchette(origin, phpsessid, opts) {
|
|||||||
+ `&ROOT_DIRECTORY=${encodeURIComponent("/ccv/data/www/itsma/htdocs/")}`
|
+ `&ROOT_DIRECTORY=${encodeURIComponent("/ccv/data/www/itsma/htdocs/")}`
|
||||||
+ `¤t_date=${encodeURIComponent(opts.currentDate)}`
|
+ `¤t_date=${encodeURIComponent(opts.currentDate)}`
|
||||||
+ `&empl_ids=${encodeURIComponent(emplIds)}`
|
+ `&empl_ids=${encodeURIComponent(emplIds)}`
|
||||||
+ `&begin_hour=8`
|
+ `&begin_hour=${start}`
|
||||||
+ `&end_hour=18`
|
+ `&end_hour=${end}`
|
||||||
+ `&plagehoraire=0`;
|
+ `&plagehoraire=0`;
|
||||||
|
|
||||||
const body = new URLSearchParams();
|
const body = new URLSearchParams();
|
||||||
@@ -925,9 +1215,80 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// v5.0.0 : Détection de la liste des techniciens depuis la page planning EV
|
// v2026.5.41 : Détection des GROUPES EasyVista (SI-CSS, SI-EXT, …)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch le HTML de la page planning et extrait la liste des groupes depuis
|
||||||
|
* `<select id="plan_group_id">`. C'est la source autoritative : ce sont les
|
||||||
|
* `group_id` exacts qu'EasyVista lui-même utilise pour le widget de
|
||||||
|
* changement de groupe sur la page planning. Robuste aux ajouts/renommages :
|
||||||
|
* si le DGNSI ajoute un 3e groupe ou renomme SI-CSS, ça apparaît tout seul.
|
||||||
|
*
|
||||||
|
* Retourne { groups: [{id, name}, ...] }. Liste vide si le fetch échoue ou
|
||||||
|
* si EV renvoie une page de login / redirection JS.
|
||||||
|
*
|
||||||
|
* @author Quentin Rouiller
|
||||||
|
*/
|
||||||
|
async function detectGroupsFromEV(origin, phpsessid) {
|
||||||
|
const url = `${origin}/index.php?eventName=HelpDesk_PlanningItem&PHPSESSID=${encodeURIComponent(phpsessid)}`;
|
||||||
|
console.log("[bg] detectGroupsFromEV → fetch page planning");
|
||||||
|
console.log("[bg] URL =", url);
|
||||||
|
|
||||||
|
const r = await evFetch(url, origin);
|
||||||
|
if (!r.ok) {
|
||||||
|
const err = new Error("HTTP " + r.status);
|
||||||
|
err.kind = classifyHttpStatus(r.status);
|
||||||
|
err.status = r.status;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const html = await r.text();
|
||||||
|
console.log("[bg] page planning taille =", html.length);
|
||||||
|
|
||||||
|
if (looksLikeLoginPage(html)) {
|
||||||
|
const err = new Error("session_expired");
|
||||||
|
err.kind = "session_expired";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser le <select id="plan_group_id">…</select> et ses <option>.
|
||||||
|
// On accepte plan_group_id avant ou après le name=, et on tolère les
|
||||||
|
// attributs supplémentaires (onchange, class, etc.).
|
||||||
|
const groups = [];
|
||||||
|
const selectMatch = html.match(/<select[^>]*\bid=["']plan_group_id["'][^>]*>([\s\S]*?)<\/select>/i);
|
||||||
|
if (selectMatch) {
|
||||||
|
const inner = selectMatch[1];
|
||||||
|
const rxOpt = /<option[^>]*\bvalue=["'](\d+)["'][^>]*>([^<]+)<\/option>/gi;
|
||||||
|
let m;
|
||||||
|
while ((m = rxOpt.exec(inner)) !== null) {
|
||||||
|
const id = m[1];
|
||||||
|
const name = (m[2] || "").trim();
|
||||||
|
if (id && name && !groups.some(g => g.id === id)) {
|
||||||
|
groups.push({ id, name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("[bg] " + groups.length + " groupes détectés :", groups);
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// Ces favoris listent les filtres "SI-CSS"/"SI-EXT" — utile si le HTML
|
||||||
|
// initial ne contient pas encore le <select>.
|
||||||
|
if (groups.length === 0) {
|
||||||
|
const rxFav = /"TITLE"\s*:\s*"([^"]+)"[\s\S]{0,400}?"q2_value_selected"\s*:\s*"([A-Za-z0-9+/=]+)"/g;
|
||||||
|
let mF;
|
||||||
|
while ((mF = rxFav.exec(html)) !== null) {
|
||||||
|
const title = mF[1];
|
||||||
|
// 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
|
||||||
|
// pour fetcher le planning. On ignore donc côté retour.
|
||||||
|
console.log("[bg] fallback favoris : " + title + " (sans ID, ignoré)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { groups };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* v5.0.1 : Détection de la liste complète des membres du groupe EasyVista.
|
* v5.0.1 : Détection de la liste complète des membres du groupe EasyVista.
|
||||||
*
|
*
|
||||||
@@ -942,14 +1303,15 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) {
|
|||||||
*
|
*
|
||||||
* Retourne { ids: [{id, name, alreadyInTeam}], groupId }.
|
* Retourne { ids: [{id, name, alreadyInTeam}], groupId }.
|
||||||
*/
|
*/
|
||||||
async function detectTeamFromEV(origin, phpsessid) {
|
async function detectTeamFromEV(origin, phpsessid, groupIdArg, supportIdsArg) {
|
||||||
// v5.0.1 : valeurs par défaut (correspondent au groupe actuel).
|
// v2026.5.41 : groupId vient de l'argument, sinon de admin_config (défaut SI-CSS).
|
||||||
// À terme elles devraient venir de la config admin.
|
// supportIds vient de l'argument, sinon de admin_config (vide tant que rien n'est
|
||||||
const DEFAULT_GROUP_ID = "191";
|
// sélectionné). Le serveur retourne tous les membres du groupe quoi qu'il arrive ;
|
||||||
const DEFAULT_SUPPORT_IDS = "76272,83725,66635,92235,90070,40944,72485,86874";
|
// supportIds sert juste à pré-cocher les techs déjà inclus dans l'équipe.
|
||||||
|
const groupId = groupIdArg || await getGroupId();
|
||||||
const groupId = DEFAULT_GROUP_ID;
|
const supportIds = (typeof supportIdsArg === "string")
|
||||||
const supportIds = DEFAULT_SUPPORT_IDS;
|
? supportIdsArg
|
||||||
|
: await getSupportIds();
|
||||||
console.log("[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
|
||||||
@@ -972,8 +1334,9 @@ async function detectTeamFromEV(origin, phpsessid) {
|
|||||||
if (looksLikeLoginPage(popupHtml)) throw new Error("session_expired");
|
if (looksLikeLoginPage(popupHtml)) throw new Error("session_expired");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[bg] detectTeam: fetch popup failed:", e);
|
console.warn("[bg] detectTeam: fetch popup failed:", e);
|
||||||
// Fallback : au moins on retourne les IDs connus avec noms vides
|
// v2026.5.41 : on retourne les IDs déjà sélectionnés par l'user (s'il y en a)
|
||||||
const ids = DEFAULT_SUPPORT_IDS.split(",").filter(Boolean);
|
// plutôt qu'une liste hardcodée. Si vide, le viewer affichera juste 0 résultat.
|
||||||
|
const ids = supportIds.split(",").filter(Boolean);
|
||||||
return {
|
return {
|
||||||
ids: ids.map(id => ({ id, name: "? (" + id + ")", alreadyInTeam: true })),
|
ids: ids.map(id => ({ id, name: "? (" + id + ")", alreadyInTeam: true })),
|
||||||
groupId
|
groupId
|
||||||
@@ -1001,7 +1364,7 @@ async function detectTeamFromEV(origin, phpsessid) {
|
|||||||
}
|
}
|
||||||
console.log("[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="76272">Nom...</option>
|
// Pattern 2 : fallback <option value="NNNNN">Nom...</option>
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
const rxOption = /<option[^>]*value=["'](\d{4,7})["'][^>]*>([^<]+)<\/option>/gi;
|
const rxOption = /<option[^>]*value=["'](\d{4,7})["'][^>]*>([^<]+)<\/option>/gi;
|
||||||
let mO;
|
let mO;
|
||||||
@@ -1051,12 +1414,27 @@ async function detectTeamFromEV(origin, phpsessid) {
|
|||||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
|
// abort de toutes les requêtes EV en vol (clic sur "Arrêter").
|
||||||
|
if (msg.type === "abortAllFetches") {
|
||||||
|
_abortAllEvFetches();
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === "getSession") {
|
if (msg.type === "getSession") {
|
||||||
const session = await findEasyVistaSession();
|
const session = await findEasyVistaSession();
|
||||||
sendResponse({ ok: true, session });
|
sendResponse({ ok: true, session });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// badge "!" clignotant sur l'icône de l'extension.
|
||||||
|
// { active: true, durationMs?: number }
|
||||||
|
if (msg.type === "setExpirationBadge") {
|
||||||
|
_setExpirationBadge(!!msg.active, msg.durationMs || 0);
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === "fetchPlanning") {
|
if (msg.type === "fetchPlanning") {
|
||||||
const session = await findEasyVistaSession();
|
const session = await findEasyVistaSession();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -1108,12 +1486,14 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const html = await fetchFicheHtml(session.origin, session.phpsessid, msg.formLink);
|
// fetchFicheHtml renvoie maintenant { html, truncated, size }.
|
||||||
|
const result = await fetchFicheHtml(session.origin, session.phpsessid, msg.formLink);
|
||||||
|
const html = result.html;
|
||||||
if (looksLikeLoginPage(html)) {
|
if (looksLikeLoginPage(html)) {
|
||||||
sendResponse({ ok: false, error: "session_expired" });
|
sendResponse({ ok: false, error: "session_expired" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendResponse({ ok: true, html, session });
|
sendResponse({ ok: true, html, session, truncated: !!result.truncated, size: result.size });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sendResponse({
|
sendResponse({
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -1125,6 +1505,116 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// probe rapide de session — fetch un endpoint léger pour vérifier
|
||||||
|
// que PHPSESSID est toujours valide. Renvoie ok=false/error=session_expired
|
||||||
|
// si la session est morte.
|
||||||
|
// (feature reschedule) : déplace une intervention vers un autre
|
||||||
|
// tech / nouvelle date / nouvel horaire de début. Durée préservée
|
||||||
|
// automatiquement par EZV (l'API ne change que start_date+hour+minute).
|
||||||
|
// Args : actionId, employeeId, date (DD/MM/YYYY), hour (0-23), minute (0-59).
|
||||||
|
if (msg.type === "rescheduleAction") {
|
||||||
|
const session = await findEasyVistaSession();
|
||||||
|
if (!session) { sendResponse({ ok: false, error: "no_session" }); return; }
|
||||||
|
try {
|
||||||
|
const url = `${session.origin}/planning_updator_xhr.php` +
|
||||||
|
`?PHPSESSID=${encodeURIComponent(session.phpsessid)}` +
|
||||||
|
`&function_name=Planning_schedule_action_Employee` +
|
||||||
|
`&action_id=${encodeURIComponent(msg.actionId)}` +
|
||||||
|
`&employee_id=${encodeURIComponent(msg.employeeId)}` +
|
||||||
|
`&date=${encodeURIComponent(msg.date)}` +
|
||||||
|
`&hour=${encodeURIComponent(msg.hour)}` +
|
||||||
|
`&minute=${encodeURIComponent(msg.minute)}` +
|
||||||
|
`&multi_day_mode_act=0`;
|
||||||
|
LOG.warn("reschedule",
|
||||||
|
`📅 reschedule actionId=${msg.actionId} → tech=${msg.employeeId} ${msg.date} ${msg.hour}:${String(msg.minute).padStart(2,"0")}`);
|
||||||
|
const r = await evFetch(url, session.origin);
|
||||||
|
if (!r.ok) {
|
||||||
|
sendResponse({ ok: false, error: classifyHttpStatus(r.status), httpStatus: r.status });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const txt = await r.text();
|
||||||
|
if (looksLikeLoginPage(txt)) {
|
||||||
|
sendResponse({ ok: false, error: "session_expired" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendResponse({ ok: true, response: txt });
|
||||||
|
} catch (err) {
|
||||||
|
LOG.warn("reschedule", "rescheduleAction err", { err: err && err.message });
|
||||||
|
sendResponse({ ok: false, error: err.kind || "fetch_failed", detail: err.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (feature reschedule) : modifie la durée (heure début/fin) d'une
|
||||||
|
// action sans changer de tech. POST application/x-www-form-urlencoded
|
||||||
|
// sur planning_updator_xhr.php avec function_name=fc_save_inspector.
|
||||||
|
// Args : actionId, suffix (ex "act_<id>_nb_0_date_<DDMMYYYY>"),
|
||||||
|
// startDate, endDate (DD/MM/YYYY), startTime, endTime (HH:MM).
|
||||||
|
if (msg.type === "updateActionTimes") {
|
||||||
|
const session = await findEasyVistaSession();
|
||||||
|
if (!session) { sendResponse({ ok: false, error: "no_session" }); return; }
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set(`start_date_${msg.suffix}`, msg.startDate);
|
||||||
|
params.set(`start_time_${msg.suffix}`, msg.startTime);
|
||||||
|
params.set(`end_date_${msg.suffix}`, msg.endDate);
|
||||||
|
params.set(`end_time_${msg.suffix}`, msg.endTime);
|
||||||
|
params.set("action_id", msg.actionId);
|
||||||
|
params.set("suffix_act", msg.suffix);
|
||||||
|
params.set(`act_absence_${msg.suffix}`, "");
|
||||||
|
params.set("function_name", "fc_save_inspector");
|
||||||
|
const body = params.toString();
|
||||||
|
const url = `${session.origin}/planning_updator_xhr.php` +
|
||||||
|
`?PHPSESSID=${encodeURIComponent(session.phpsessid)}`;
|
||||||
|
LOG.warn("reschedule",
|
||||||
|
`⏱ updateActionTimes actionId=${msg.actionId} ${msg.startDate} ${msg.startTime}→${msg.endTime}`);
|
||||||
|
const r = await evFetch(url, session.origin, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
sendResponse({ ok: false, error: classifyHttpStatus(r.status), httpStatus: r.status });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const txt = await r.text();
|
||||||
|
if (looksLikeLoginPage(txt)) {
|
||||||
|
sendResponse({ ok: false, error: "session_expired" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendResponse({ ok: true, response: txt });
|
||||||
|
} catch (err) {
|
||||||
|
LOG.warn("reschedule", "updateActionTimes err", { err: err && err.message });
|
||||||
|
sendResponse({ ok: false, error: err.kind || "fetch_failed", detail: err.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === "checkSession") {
|
||||||
|
const session = await findEasyVistaSession();
|
||||||
|
if (!session) {
|
||||||
|
sendResponse({ ok: false, error: "no_session" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = `${session.origin}/index.php?eventName=HelpDesk_PlanningItem&PHPSESSID=${encodeURIComponent(session.phpsessid)}`;
|
||||||
|
const r = await evFetch(url, session.origin);
|
||||||
|
if (!r.ok) {
|
||||||
|
sendResponse({ ok: false, error: classifyHttpStatus(r.status), httpStatus: r.status });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const txt = await r.text();
|
||||||
|
if (looksLikeLoginPage(txt) || txt.length < 5000) {
|
||||||
|
sendResponse({ ok: false, error: "session_expired" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
} catch (err) {
|
||||||
|
sendResponse({ ok: false, error: "fetch_failed", detail: err.message || String(err) });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === "fetchTimelineApi") {
|
if (msg.type === "fetchTimelineApi") {
|
||||||
const session = await findEasyVistaSession();
|
const session = await findEasyVistaSession();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -1228,16 +1718,39 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === "detectTeam") {
|
if (msg.type === "detectGroups") {
|
||||||
// v5.0.0 : détecte la liste des IDs de techniciens depuis le HTML
|
// v2026.5.41 : détecte la liste des groupes EV (SI-CSS, SI-EXT, …)
|
||||||
// v5.0.1 : retourne aussi les noms via la popup group_supports
|
// depuis le <select id="plan_group_id"> de la page planning.
|
||||||
const session = await findEasyVistaSession();
|
const session = await findEasyVistaSession();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
sendResponse({ ok: false, error: "no_session" });
|
sendResponse({ ok: false, error: "no_session" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await detectTeamFromEV(session.origin, session.phpsessid);
|
const result = await detectGroupsFromEV(session.origin, session.phpsessid);
|
||||||
|
sendResponse({ ok: true, groups: result.groups });
|
||||||
|
} catch (err) {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: err.kind || err.message || String(err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === "detectTeam") {
|
||||||
|
// v5.0.0 : détecte la liste des IDs de techniciens depuis le HTML
|
||||||
|
// v5.0.1 : retourne aussi les noms via la popup group_supports
|
||||||
|
// v2026.5.41 : accepte msg.groupId pour basculer entre SI-CSS / SI-EXT
|
||||||
|
const session = await findEasyVistaSession();
|
||||||
|
if (!session) {
|
||||||
|
sendResponse({ ok: false, error: "no_session" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await detectTeamFromEV(
|
||||||
|
session.origin, session.phpsessid, msg.groupId, msg.supportIds
|
||||||
|
);
|
||||||
// result = { ids: [{id,name,alreadyInTeam}, ...], groupId }
|
// result = { ids: [{id,name,alreadyInTeam}, ...], groupId }
|
||||||
sendResponse({ ok: true, members: result.ids, groupId: result.groupId });
|
sendResponse({ ok: true, members: result.ids, groupId: result.groupId });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1380,13 +1893,122 @@ async function cleanupOldCaches(daysToKeep) {
|
|||||||
return toRemove.length;
|
return toRemove.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v2026.5.39 : on lit admin_config pour récupérer cacheDays. Si pas dispo,
|
||||||
|
// fallback sur 7 jours.
|
||||||
|
async function _getCacheDays() {
|
||||||
|
try {
|
||||||
|
const o = await chrome.storage.local.get("admin_config");
|
||||||
|
const cfg = o && o.admin_config;
|
||||||
|
if (cfg && typeof cfg.cacheDays === "number" && cfg.cacheDays > 0) {
|
||||||
|
return cfg.cacheDays;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
LOG.warn("cache", "lecture admin_config échouée, fallback 7 jours", { err: e && e.message });
|
||||||
|
}
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
// Au démarrage, nettoyer les anciennes alarmes et les anciens caches
|
// Au démarrage, nettoyer les anciennes alarmes et les anciens caches
|
||||||
chrome.runtime.onInstalled.addListener(() => {
|
// v2026.5.45 (issue #3) : on stocke aussi un marqueur d'install/update
|
||||||
|
// avec previousVersion. Au prochain ouverture du viewer, on affiche un
|
||||||
|
// toast préventif si la version précédente était < 2026.5.44 (où le bug
|
||||||
|
// structurel d'écrasement des absences récurrentes existait encore).
|
||||||
|
chrome.runtime.onInstalled.addListener(async (details) => {
|
||||||
clearLegacyRefreshAlarms();
|
clearLegacyRefreshAlarms();
|
||||||
cleanupOldCaches(7).catch(err => console.warn("cleanup:", err));
|
const days = await _getCacheDays();
|
||||||
|
cleanupOldCaches(days).catch(err => LOG.warn("cleanup", "échec onInstalled", { err: err && err.message }));
|
||||||
|
try {
|
||||||
|
const reason = details && details.reason;
|
||||||
|
const previousVersion = (details && details.previousVersion) || null;
|
||||||
|
if (reason === "install" || reason === "update") {
|
||||||
|
const currentVersion = (chrome.runtime.getManifest && chrome.runtime.getManifest().version) || null;
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
_lastInstallEvent: {
|
||||||
|
reason,
|
||||||
|
previousVersion,
|
||||||
|
currentVersion,
|
||||||
|
ts: Date.now(),
|
||||||
|
notified: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
LOG.info("install", "marqueur posé", { reason, previousVersion, currentVersion });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
LOG.warn("install", "écriture _lastInstallEvent échouée", { err: e && e.message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.runtime.onStartup.addListener(() => {
|
chrome.runtime.onStartup.addListener(async () => {
|
||||||
clearLegacyRefreshAlarms();
|
clearLegacyRefreshAlarms();
|
||||||
cleanupOldCaches(7).catch(err => console.warn("cleanup:", err));
|
const days = await _getCacheDays();
|
||||||
|
cleanupOldCaches(days).catch(err => LOG.warn("cleanup", "échec onStartup", { err: err && err.message }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// v2026.5.45 (issue #4 — bonus) : listener sur les changements de cookie
|
||||||
|
// EZV. Quand la permission `cookies` est accordée, on s'abonne pour détecter
|
||||||
|
// EN TEMPS RÉEL les changements de PHPSESSID (relog dans un autre onglet,
|
||||||
|
// expiration côté serveur, révocation…). On envoie un message broadcast aux
|
||||||
|
// viewers ouverts pour qu'ils recalibrent leur compteur de session.
|
||||||
|
function _attachCookiesListener() {
|
||||||
|
if (!chrome.cookies || !chrome.cookies.onChanged) return;
|
||||||
|
if (_attachCookiesListener._attached) return;
|
||||||
|
_attachCookiesListener._attached = true;
|
||||||
|
LOG.warn("cookies", "🍪 listener PHPSESSID activé (permission cookies accordée)");
|
||||||
|
chrome.cookies.onChanged.addListener((info) => {
|
||||||
|
try {
|
||||||
|
if (!info || !info.cookie) return;
|
||||||
|
const c = info.cookie;
|
||||||
|
if (c.name !== "PHPSESSID") return;
|
||||||
|
const dom = (c.domain || "").replace(/^\./, "");
|
||||||
|
if (!/itsma\.(etat-de-vaud|vd)\.ch$/.test(dom)) return;
|
||||||
|
const phpsessid8 = c.value ? c.value.slice(0, 8) + "…" : null;
|
||||||
|
// événements importants → LOG.warn (toujours visibles).
|
||||||
|
// Détail (cause, expirationDate, …) → LOG.info (debug only).
|
||||||
|
if (info.removed) {
|
||||||
|
LOG.warn("cookies", `🚫 PHPSESSID supprimé sur ${dom} (cause=${info.cause}) → session morte`);
|
||||||
|
} else {
|
||||||
|
LOG.warn("cookies", `🍪 PHPSESSID ${phpsessid8} sur ${dom} (cause=${info.cause})`);
|
||||||
|
}
|
||||||
|
// Invalide immédiatement le cache findEasyVistaSession() pour que
|
||||||
|
// le prochain appel reflète le nouveau cookie sans attendre la TTL.
|
||||||
|
if (typeof _invalidateSessionCache === "function") _invalidateSessionCache();
|
||||||
|
LOG.info("cookies", " détail onChanged",
|
||||||
|
{ domain: dom, removed: !!info.removed, cause: info.cause,
|
||||||
|
phpsessid8, expirationDate: c.expirationDate || null,
|
||||||
|
exp: c.expirationDate ? new Date(c.expirationDate * 1000).toISOString() : null });
|
||||||
|
try {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: "cookieChanged",
|
||||||
|
domain: dom,
|
||||||
|
removed: !!info.removed,
|
||||||
|
cause: info.cause,
|
||||||
|
phpsessid: c.value || null,
|
||||||
|
expirationDate: c.expirationDate || null
|
||||||
|
}).catch(() => { /* viewers fermés → pas grave */ });
|
||||||
|
} catch (e) { /* idem */ }
|
||||||
|
} catch (e) {
|
||||||
|
LOG.warn("cookies", "listener err", { err: e && e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Au boot du service worker, on log l'état de la permission (toujours visible)
|
||||||
|
// et on attache le listener si déjà accordée.
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const has = await _hasCookiesPermission();
|
||||||
|
LOG.warn("cookies", `🔧 permission cookies au boot : ${has ? "ACCORDÉE → lecture cookie active" : "non accordée → fallback URL"}`);
|
||||||
|
if (has) _attachCookiesListener();
|
||||||
|
} catch (e) { /* silent */ }
|
||||||
|
})();
|
||||||
|
chrome.permissions.onAdded && chrome.permissions.onAdded.addListener((p) => {
|
||||||
|
if (p && p.permissions && p.permissions.includes("cookies")) {
|
||||||
|
LOG.warn("cookies", "✅ permission cookies ACCORDÉE par l'utilisateur");
|
||||||
|
_attachCookiesListener();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
chrome.permissions.onRemoved && chrome.permissions.onRemoved.addListener((p) => {
|
||||||
|
if (p && p.permissions && p.permissions.includes("cookies")) {
|
||||||
|
LOG.warn("cookies", "🛑 permission cookies RETIRÉE → fallback URL réactivé");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
Before Width: | Height: | Size: 444 B After Width: | Height: | Size: 444 B |
|
Before Width: | Height: | Size: 118 B After Width: | Height: | Size: 118 B |
|
Before Width: | Height: | Size: 207 B After Width: | Height: | Size: 207 B |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Planification",
|
"name": "Planification",
|
||||||
"version": "2026.5.38",
|
"version": "2026.5.45",
|
||||||
"description": "Vue claire et rapide du planning des techniciens EasyVista. Développé par Quentin Rouiller — DGNSI, Canton de Vaud.",
|
"description": "Vue claire et rapide du planning des techniciens EasyVista. Développé par Quentin Rouiller — DGNSI, Canton de Vaud.",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"activeTab",
|
"activeTab",
|
||||||
@@ -10,10 +10,16 @@
|
|||||||
"tabs",
|
"tabs",
|
||||||
"alarms"
|
"alarms"
|
||||||
],
|
],
|
||||||
|
"optional_permissions": [
|
||||||
|
"cookies"
|
||||||
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://itsma.etat-de-vaud.ch/*",
|
"https://itsma.etat-de-vaud.ch/*",
|
||||||
"https://itsma.vd.ch/*"
|
"https://itsma.vd.ch/*"
|
||||||
],
|
],
|
||||||
|
"optional_host_permissions": [
|
||||||
|
"https://*/*"
|
||||||
|
],
|
||||||
"action": {
|
"action": {
|
||||||
"default_title": "Ouvrir la Planification"
|
"default_title": "Ouvrir la Planification"
|
||||||
},
|
},
|
||||||
+2342
-223
File diff suppressed because it is too large
Load Diff
@@ -28,26 +28,28 @@
|
|||||||
type="button" aria-label="Utilisateur connecté"
|
type="button" aria-label="Utilisateur connecté"
|
||||||
title="Utilisateur — cliquer pour accéder aux paramètres">?</button>
|
title="Utilisateur — cliquer pour accéder aux paramètres">?</button>
|
||||||
<h1 id="app-title">Planification</h1>
|
<h1 id="app-title">Planification</h1>
|
||||||
|
<!-- : bloc "Aujourd'hui + horloge" encadré, suivi DIRECTEMENT
|
||||||
|
du statut d'actualisation (MAJ + ✓), puis le sélecteur de date
|
||||||
|
du planning. -->
|
||||||
|
<div id="today-block" class="today-block">
|
||||||
|
<button id="nav-today" class="btn btn-today" title="Revenir au jour courant">Aujourd'hui</button>
|
||||||
|
<div id="app-clock" class="app-clock" title="Date et heure actuelles">
|
||||||
|
<div id="app-clock-date" class="app-clock-date"></div>
|
||||||
|
<div id="app-clock-time" class="app-clock-time"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span id="capture-info" class="capture-info"></span>
|
||||||
|
<span id="refresh-check" class="refresh-check hidden" title="Mise à jour terminée">✓</span>
|
||||||
<div class="date-nav">
|
<div class="date-nav">
|
||||||
<button id="nav-prev" class="btn btn-nav" title="Jour précédent" aria-label="Jour précédent">◀</button>
|
<button id="nav-prev" class="btn btn-nav" title="Jour précédent" aria-label="Jour précédent">◀</button>
|
||||||
<!-- v2026.5.17 : input date custom qui affiche "Vendredi 24.04.2026" -->
|
|
||||||
<div class="date-custom-wrapper">
|
<div class="date-custom-wrapper">
|
||||||
<div id="date-custom" class="date-custom" role="button" tabindex="0" title="Choisir une date">
|
<div id="date-custom" class="date-custom" role="button" tabindex="0" title="Choisir une date du planning">
|
||||||
<span id="date-custom-label"></span>
|
<span id="date-custom-label"></span>
|
||||||
<span class="date-custom-icon">📅</span>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="date" id="date-picker" class="date-input-hidden">
|
<input type="date" id="date-picker" class="date-input-hidden">
|
||||||
</div>
|
</div>
|
||||||
<button id="nav-next" class="btn btn-nav" title="Jour suivant" aria-label="Jour suivant">▶</button>
|
<button id="nav-next" class="btn btn-nav" title="Jour suivant" aria-label="Jour suivant">▶</button>
|
||||||
<button id="nav-today" class="btn btn-today" title="Aujourd'hui">Auj.</button>
|
|
||||||
</div>
|
</div>
|
||||||
<span id="capture-info" class="capture-info"></span>
|
|
||||||
<span id="refresh-check" class="refresh-check hidden" title="Mise à jour terminée">✓</span>
|
|
||||||
</div>
|
|
||||||
<!-- v2026.5.16 : date complète du jour au-dessus de l'heure dans la topbar -->
|
|
||||||
<div id="app-clock" class="app-clock" title="Date et heure actuelles">
|
|
||||||
<div id="app-clock-date" class="app-clock-date"></div>
|
|
||||||
<div id="app-clock-time" class="app-clock-time"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 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>
|
||||||
+6727
-677
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user