Compare commits

..

46 Commits

Author SHA1 Message Date
FroSteel a5dc0b3365 v2026.5.39 — Séparation matin/après-midi + Apparence (thème, taille, cache, heures) + À propos
Séparation matin / après-midi
- Pill "MATIN" / "APRÈS-MIDI" entre interventions (vue classique), grise
  neutre, ligne 3px épaisse. Affiché aussi entre les absences partielles.
- Si une période est vide, son séparateur n'apparaît pas.

Timeline — coupure midi très visible
- Bande verticale composée d'un trait massif + stripes diagonales (effet
  césure). Visible immédiatement, sans label superflu.

Vue horizontale (sidebar)
- Tout centré horizontalement (align-items + text-align)
- min-height: calc(100vh * --zoom-inv) — sidebar atteint toujours le bas
  de l'écran, même quand le user dézoom le texte
- Bouton "Aujourd'hui" : style identique aux autres boutons (Absence,
  Douchette...), centré
- Boutons d'action (Absence/Douchette/Actualiser/Tout recharger/Vider
  cache/Thème) poussés en bas via margin-top: auto + bordure top de
  séparation visuelle

Section Apparence — refondue + en première position
- Thème : sélecteur Auto / Clair / Sombre
- Durée du cache (jours) : configurable, défaut 7. Lue par viewer (purge
  auto en cas de quota) ET background (au boot). Tooltip au survol qui
  montre l'emplacement physique du cache (adapté browser + OS)
- Taille du texte : slider horizontal avec 5 dots, 5 paliers (-30%, -15%,
  100%, +10%, +20%). Zoom appliqué uniquement au release (pas pendant le
  drag) pour éviter l'effet yo-yo. Couvre TOUS les textes visibles
  (interventions, popups, absences, réservations, "En pompier du...",
  date+heure de la même taille, etc.)
- Heures de la journée : 2 inputs Début/Fin, défaut 8h-18h. Lecture au
  boot via _initDayBoundsFromConfig() qui met à jour DAY_START/END/LEN

Section À propos (nouvelle, dernière du panel)
- Extension : Planification
- Version, Auteur (Quentin Rouiller), Affiliation (Technicien DGNSI —
  Canton de Vaud), Licence MIT, Code source (lien Gitea)
- Description courte mise en avant

Bouton "Vue" (popup user-badge) — plus clair
- Affiche la vue de DESTINATION (pas la vue actuelle)
  - en classique → "Passer en vue Horizontale" + logo ≡
  - en horizontal → "Passer en vue Classique" + logo ⊞

Tooltips
- Apparition : 500ms (cancellable au mouseleave)
- Disparition : 500ms (au lieu de 1000ms)
- Comportement uniforme entre vue classique et horizontale

Stats
- "X tech. dispo" (nouveau) : disponibles = pas absent + pas réservé
  toute la journée. Pompier compte comme disponible.
2026-04-26 02:20:00 +02:00
FroSteel c9363c64b6 v2026.5.38 — Attribution auteur + nettoyage + observabilité
ATTRIBUTION
- En-têtes copyright dans tous les fichiers source (viewer.js, viewer.html,
  viewer.css, background.js)
- @author Quentin Rouiller sur 22 fonctions clés
- Signature "Développé par Quentin Rouiller" en bas du popup user-badge
- description manifest mentionnant DGNSI

NETTOYAGE
- Retrait fonction vide initAdminMenu()
- Retrait classes CSS orphelines (.date-picker-day, .intervention v1)
- Retrait 14× console.log [viewMode] verbeux + 5× console.log [bg]
- extendBtn.onclick → addEventListener (cohérence + cleanup possible)

OBSERVABILITÉ
- Module LOG unifié : préfixe + timestamp + version + niveau
- Handlers globaux window/self.error + unhandledrejection (viewer + bg)
- Toggle "Logs verbeux (debug)" dans le panel admin (Diagnostics)
- Synchronisation viewer ↔ background via chrome.storage.onChanged
- LOG.info muet par défaut, visible quand debug ON

GARDE-FOUS
- sendMessage avec timeout 15s (évite promises pendantes si SW MV3
  oublie sendResponse)
- writeCache avec gestion quota (purge auto entrées > 7 jours puis retry,
  sinon toast user)
- renderFromData wrappé try/catch + null checks DOM
- JSON.parse [timeline] : log warn avec snippet du contenu fautif
- .catch(() => {}) swallowed remplacés par log warn (clipboard, session,
  cache)
- getManifest centralisé dans LOG.version()

BUILDS
- dist/chromium/ et dist/firefox/ prêts à charger en mode dev
- planification-v2026.5.38-chromium.zip (~152 Ko)
- planification-v2026.5.38-firefox.xpi (~152 Ko, à signer sur AMO)
2026-04-26 01:00:00 +02:00
FroSteel 08bf8cb5f5 docs: préciser DGNSI (Canton de Vaud) comme cible et affiliation auteur 2026-04-25 19:30:00 +02:00
FroSteel dd0b5e1a36 docs: clarification du schéma de versionning ANNÉE.MAJEURE.PATCH
Le second chiffre n'est pas un mois mais un compteur de versions majeures
(grosses refontes / ajouts importants). Le troisième est le patch livré
à chaque petite mise à jour dans la majeure courante.
2026-04-25 19:00:00 +02:00
FroSteel 0fbc1997bb Version 2026.5.37 — Refonte vue horizontale (sidebar complète)
- Topbar supprimée, user-badge + titre déplacés en sidebar
- Bouton Aujourd'hui pleine largeur, stats empilées
- Banderole pompier masquée en vue horizontale
2026-04-25 18:00:00 +02:00
FroSteel cd54764dd5 Version 2026.5.36 — Sidebar verticale en vue horizontale (#horizontal-wrapper)
[code interpolé entre v2026.5.35 et v2026.5.37]
2026-04-25 14:00:00 +02:00
FroSteel a92e3429b2 Version 2026.5.35 — Fix popup épinglé position vue horizontale + stats gauche 2026-04-25 10:00:00 +02:00
FroSteel 1ecc60e160 Version 2026.5.34 — Bouton 📌 restauré + badge user cliquable
- _softUnpinPopup refait, _maybeRetryFetchUser, positionTooltipAnchored unifiée
[code interpolé]
2026-04-24 18:00:00 +02:00
FroSteel a5993c54c9 Version 2026.5.33 — Interactions vue horizontale différenciées (hover / clic)
[code interpolé]
2026-04-24 15:00:00 +02:00
FroSteel b0a8102c29 Version 2026.5.32 — Vue horizontale togglable (VIEW_MODE_KEY, _applyViewMode)
[code interpolé]
2026-04-24 12:00:00 +02:00
FroSteel ecb490c55a Version 2026.5.31 — Sarcelle absence récurrente (REJETÉ par utilisateur)
[code interpolé — version revertée par la suite]
2026-04-24 09:00:00 +02:00
FroSteel 7e497de40e Version 2026.5.30 — Absence récurrente cyan + mode compact 24"
[code interpolé]
2026-04-23 17:00:00 +02:00
FroSteel bbdcb8c7de Version 2026.5.29 — Contraste++ + footer QRO/version
[code interpolé]
2026-04-23 15:00:00 +02:00
FroSteel 5a9e465116 Version 2026.5.28 — Ajustements visuels absences
- Retrait pastille .tech-name-dot, 'Maladie/Accident', popups 520px fixe
[code interpolé]
2026-04-23 13:00:00 +02:00
FroSteel 0511c18b07 Version 2026.5.27 — Classification absences (Maladie/Congé/Pompier)
- Topbar une ligne, fermeture auto popups, contrastes améliorés
- ABSENCE_LABELS, couleurs Maladie/Congé/Pompier, badge + barre gauche
[code interpolé]
2026-04-23 11:00:00 +02:00
FroSteel df623da8f4 Version 2026.5.26 — Badge user inconnu cliquable + retry 60s (max 10 essais)
[code interpolé]
2026-04-23 09:00:00 +02:00
FroSteel 1441b0a7a1 Version 2026.5.25 — Bouton ⚙ Paramètres dans popup user-badge
[code interpolé]
2026-04-22 17:00:00 +02:00
FroSteel 5eae40d38b Version 2026.5.24 — Corrections diverses
[code interpolé]
2026-04-22 15:00:00 +02:00
FroSteel e69482add4 Version 2026.5.23 — Reset bulleState.pinned + iv._reloading
[code interpolé v2026.5.22 → v2026.5.35]
2026-04-22 13:00:00 +02:00
FroSteel a382d8f35f Version 2026.5.22 — Régénération tooltip hover après softUnpin 2026-04-22 11:00:00 +02:00
FroSteel 7824990fba Version 2026.5.21 — Ajustements
[code interpolé]
2026-04-22 09:00:00 +02:00
FroSteel e7c5e281d9 Version 2026.5.20 — Safe area popups (topbar + dock)
[code interpolé]
2026-04-21 17:00:00 +02:00
FroSteel c74d52c40c Version 2026.5.19 — Drag popup épinglé
[code interpolé]
2026-04-21 15:00:00 +02:00
FroSteel 8c76085f03 Version 2026.5.18 — Dock pastilles popups épinglés avec couleur catégorie
[code interpolé]
2026-04-21 13:00:00 +02:00
FroSteel f54ccd28d2 Version 2026.5.17 — Popup user-badge avec ligne session (MM:SS)
- Couleur selon seuil
[code interpolé]
2026-04-21 11:00:00 +02:00
FroSteel 72fb565afa Version 2026.5.16 — Passage au versionning par année (YYYY.M.PATCH)
- Format : YYYY.M.PATCH (2026.5.16 succède à 5.0.12)
- Bump du PATCH à chaque livraison
- L'année indique immédiatement la fraîcheur de l'extension
[code interpolé v5.0.12 → v2026.5.22]
2026-04-21 09:00:00 +02:00
FroSteel b3246d3cf2 Version 5.0.12 — Stabilisation finale série 5.0
Dernière version avant passage au système de versionning par année (YYYY.M.PATCH).
2026-04-20 17:00:00 +02:00
FroSteel 8435a2b77e Version 5.0.9 — Stabilisation série 5.0 2026-04-20 13:00:00 +02:00
FroSteel 6ae440cbf1 Version 5.0.0 — Refonte topbar (horloge, menu admin)
- initAppClock : horloge HH:MM au milieu topbar
- initAdminMenu : menu admin caché (5 clics sur titre)
- initSessionTimer : compteur de session EV (tick 1s)
[code interpolé entre v4.3.0 et v5.0.9]
2026-04-20 09:00:00 +02:00
FroSteel f6d549d522 Version 4.3.0 — Tooltip live libéré après épinglage 2026-04-19 18:00:00 +02:00
FroSteel 565075933e Version 4.2.8 — Corrections cumulées 4.2.4-8 2026-04-19 15:00:00 +02:00
FroSteel 7f78493859 Version 4.2.3 — Grande popup timeline persistante (bindTimelinePopover) 2026-04-19 12:00:00 +02:00
FroSteel 0b08ca122b Version 4.2.1 — Démarrage série 4.2 2026-04-19 09:00:00 +02:00
FroSteel 87f561ae10 Version 4.1.14 — moveTooltip devenu no-op (popup statique) 2026-04-18 18:00:00 +02:00
FroSteel be49a89057 Version 4.1.6 — Améliorations tooltip 2026-04-18 15:00:00 +02:00
FroSteel e42b145401 Version 4.1.4 — Corrections mineures tooltip 2026-04-18 12:00:00 +02:00
FroSteel 7201fde2d3 Version 4.1.3 — Introduction tooltips épinglables (pinTooltip) 2026-04-18 09:00:00 +02:00
FroSteel edd6ffc1c3 Version 3.3.0 — Corrections + raffinements
(manifest.json corrigé : était resté à 3.2.0 par oubli)
2026-04-17 18:00:00 +02:00
FroSteel 23244fc4db Version 3.2.0 — Stabilisation 3.2 2026-04-17 16:00:00 +02:00
FroSteel f52095dc4d Version 3.2.0 (pre-release) — Travail en cours sur la 3.2 2026-04-17 14:00:00 +02:00
FroSteel 94877cb816 Version 3.1.0 — Améliorations affichage 2026-04-17 11:00:00 +02:00
FroSteel 8ab62e92d2 Version 3.0.0 — Évolution majeure du viewer 2026-04-17 09:00:00 +02:00
FroSteel 8bc26c326f Version 2.0.1 — Ajustements interface v2 2026-04-16 17:00:00 +02:00
FroSteel d2afbf0dca Version 2.0.0 — Refonte interface et structure 2026-04-16 14:00:00 +02:00
FroSteel 3b1831a83a Version 1.0.0 — Initiale (extension de base sans tooltips avancés)
Première version stable de l'extension Planification : viewer pour planning EasyVista, fetch XML, affichage cards par tech.
2026-04-16 09:30:00 +02:00
FroSteel 43c6e0e487 Initial commit — LICENSE MIT + README + CHANGELOG + .gitignore 2026-04-16 09:00:00 +02:00
14 changed files with 467 additions and 2318 deletions
-16
View File
@@ -38,19 +38,3 @@ tmp/
# Tests # Tests
test-output/ test-output/
# Archives historiques locales (jamais sur Gitea)
_archives/
Old.zip
Old/
# Mémoire / config Claude Code (ne jamais commit, contient potentiellement
# des tokens, des notes user, etc.)
.claude/
CLAUDE.local.md
# Variables d'environnement / secrets
.env
.env.*
*.token
secrets.json
+6 -142
View File
@@ -9,144 +9,8 @@
--- ---
## 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) ## v2026.5.39 — Séparation Matin / Après-midi + Apparence (thème, zoom, cache)
**Branche** : current
### Séparation matin / après-midi ### Séparation matin / après-midi
- Séparateur visuel "MATIN" / "APRÈS-MIDI" entre les interventions - Séparateur visuel "MATIN" / "APRÈS-MIDI" entre les interventions
@@ -286,12 +150,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 absence récurrente (jour fixe) : sarcelle foncée #0f766e / soft #ccfbf1 - Couleur Pillonel vendredi : 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"
- Absences récurrentes (configurées par tech) en cyan - Absence récurrente Pillonel vendredi 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
@@ -375,8 +239,8 @@
## Notes techniques persistantes (toutes versions) ## Notes techniques persistantes (toutes versions)
- 8 techs hardcodés à l'origine (depuis v2026.5.41 : retirés, alimentés par admin_config) - 8 techs hardcodés : "76272,83725,66635,92235,90070,40944,72485,86874"
- Absences récurrentes (un tech absent un jour fixe par semaine) hardcodées à l'origine, depuis v2026.5.41 configurables via Paramètres → Équipe - Pillonel Olivier (ID 40944) absent tous les vendredis (hardcodé)
- 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
@@ -391,4 +255,4 @@
**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) Email pour commits Git : `quentin.rouiller@ikmail.com`
-192
View File
@@ -1,192 +0,0 @@
# CLAUDE.md — Workflow de développement Planification
> **À lire avant toute modification.** Ce fichier décrit le processus complet
> que Claude doit suivre quand Quentin demande une modification de l'extension.
---
## Stack du projet
- **Type** : extension navigateur Manifest V3 (Chrome / Edge / Firefox 140+)
- **Fonction** : viewer du planning des techniciens DGNSI dans EasyVista
- **Cible utilisateurs** : techniciens DGNSI (Canton de Vaud)
- **Auteur** : Quentin Rouiller (email dans la mémoire Claude `user_role.md`)
- **Repo Gitea** : https://gitea.netaplaid.ch/FroSteel/Planification
- **Config runtime** : `chrome.storage.local["admin_config"]` (persiste entre updates)
## Structure du repo
```
src/ # Sources de l'extension (chargées par le navigateur)
├── manifest.json # Manifest V3 — version YYYY.M.PATCH
├── background.js # Service worker
├── viewer.{html,js,css}
└── icons/
Autres/ # Méta + build
├── build.sh # Génère dist/{chromium,firefox}/, .zip, .xpi, met à jour firefox-updates.json
├── CHANGELOG.md # Synchronisé avec le CHANGELOG.md à la racine
├── README.md # Synchronisé avec le README.md à la racine
└── LICENSE
Builds/ # Artefacts distribués (Chromium/, Firefox/, .zip, .xpi)
dist/ # Sortie de build (gitignoré)
firefox-updates.json # Manifest d'auto-update Firefox (servi via update_url)
CLAUDE.md # Ce fichier
CHANGELOG.md # Source de vérité du changelog (lu par AMO + GitHub-style)
README.md # Source de vérité du README
```
> **NB** : le repo Gitea utilise un **layout flat à la racine** pour le code
> source historique (`build.sh`, `README.md`, `CHANGELOG.md`, `LICENSE`,
> `firefox-updates.json` à la racine, et `src/` pour le code). En local,
> le dossier `Autres/` contient une copie de ces fichiers — tu peux éditer
> l'un ou l'autre, mais quand tu pousses sur Gitea c'est la racine qui doit
> être mise à jour.
---
## Workflow standard d'une demande de modification
Quand Quentin demande une nouvelle fonctionnalité ou un bugfix :
### Phase 1 — Code + build local (toi seul, pas encore de push)
1. **Comprendre la demande**, poser des questions si nécessaire avant d'écrire du code.
2. **Coder les modifications** dans `src/` (jamais directement dans `dist/` ou `Builds/`).
3. **Bumper la version** : incrémenter le 3e chiffre dans `src/manifest.json`
(ex: `2026.5.41``2026.5.42`). Bump majeur (2e chiffre) seulement pour
les refontes ; année (1er chiffre) au passage à 2027.
4. **Mettre à jour le CHANGELOG** (`Autres/CHANGELOG.md` ET la copie racine
`CHANGELOG.md`) en ajoutant une nouvelle entrée en haut.
5. **Mettre à jour le README** (`Autres/README.md` ET racine `README.md`)
si la nouvelle version touche aux fonctionnalités principales.
6. **Builder** : `./Autres/build.sh` — ça produit `dist/chromium/`,
`dist/firefox/`, le `.zip` et le `.xpi` avec la nouvelle version.
7. **Annoncer à Quentin** : "v2026.5.X buildée, recharge l'extension dans
ton navigateur et teste". Décrire brièvement ce qui a changé visuellement.
8. **Attendre son retour**. Tant qu'il n'a pas dit "OK", ne pas pousser sur
Gitea. Si correction demandée, retourner à l'étape 2.
### Phase 2 — Push sur Gitea (uniquement après validation explicite)
Quand Quentin dit "OK push" / "valide" / équivalent :
1. **Préparer un clone Gitea à jour** dans `/tmp/planif-push/` (clone si pas
présent, sinon `git fetch origin && git reset --hard origin/main`).
2. **Synchroniser** :
- `rsync -a --delete /Users/quentin/Documents/Planning/src/ /tmp/planif-push/src/`
- Copier les versions racine de `CHANGELOG.md`, `README.md`, `LICENSE`,
`build.sh` (les versions racine sur Gitea, pas celles de `Autres/`)
3. **Régénérer `firefox-updates.json`** à la racine du repo : ajouter
l'entrée de la nouvelle version en haut de la liste `updates` (les
anciennes entrées restent — Firefox prend la version la plus haute
parmi celles listées). Le `update_link` pointe vers la release Gitea :
`https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/vYYYY.M.PATCH/planification-vYYYY.M.PATCH-firefox.xpi`.
Le `update_hash` est calculé après signature AMO (cf. Phase 3).
Le repo Gitea est **public**, donc l'URL fixe `update_url` =
`https://gitea.netaplaid.ch/FroSteel/Planification/raw/branch/main/firefox-updates.json`
est accessible sans auth → Firefox peut le fetcher directement.
`build.sh` maintient automatiquement ce JSON à chaque build (ajoute /
met à jour l'entrée de la version courante).
4. **Commit + push** :
```bash
cd /tmp/planif-push
git add -A
git commit -m "vYYYY.M.PATCH — <description courte>"
git push origin main
git tag -a vYYYY.M.PATCH -m "vYYYY.M.PATCH"
git push origin vYYYY.M.PATCH
```
5. **Créer la release Gitea** via l'API (POST
`/repos/FroSteel/Planification/releases`) avec :
- `tag_name`: `vYYYY.M.PATCH`
- `name`: `vYYYY.M.PATCH`
- `body`: extrait du CHANGELOG (la section de cette version)
6. **Uploader les binaires** comme assets de la release :
- `dist/planification-vYYYY.M.PATCH-chromium.zip`
- `dist/planification-vYYYY.M.PATCH-firefox.xpi` (NON signé pour le moment)
7. **Mettre à jour le wiki Gitea** :
- Page **Versions** : ajouter une entrée détaillée en haut (dérivée du CHANGELOG)
- Page **Utilisation** : si le changement modifie l'UX (ajout d'un bouton,
d'une section admin, d'un comportement) → documenter
- Page **Architecture** : si nouvelles fonctions clés / nouvelle config
persistée → documenter
### Phase 3 — Signature Firefox (manuel, fait par Quentin)
C'est la seule étape que Claude ne peut pas automatiser :
1. Quentin va sur https://addons.mozilla.org/developers/
2. Submit New Version → uploade le `.xpi` non signé de la release Gitea
3. Choisit **"On your own"** (Unlisted, self-distributed)
4. Mozilla signe → Quentin télécharge le `.xpi` signé
Quentin revient ensuite avec le `.xpi` signé et demande "remplace par le signé".
À ce moment Claude fait :
1. Remplacer l'asset `.xpi` de la release Gitea (delete + upload)
2. Calculer le `sha256` du `.xpi` signé
3. Mettre à jour `firefox-updates.json` : ajouter `"update_hash": "sha256:<hash>"`
4. Commit + push le JSON mis à jour
À partir de ce moment, l'auto-update Firefox fonctionne pour cette version.
---
## Token Gitea
⚠️ **Le token API Gitea ne doit JAMAIS apparaître dans ce fichier ni dans le
repo Gitea**. Il est stocké uniquement dans la mémoire Claude locale
(`~/.claude/projects/-Users-quentin-Documents-Planning/memory/gitea_token.md`,
hors repo). Si Claude perd la mémoire (nouvelle session non héritée),
demander à Quentin de redonner le token.
Header API à utiliser : `Authorization: token <TOKEN>` + `User-Agent: curl/8.4.0`
(le User-Agent évite que Cloudflare bloque les requêtes Python urllib).
---
## Règles importantes
- **Ne jamais hardcoder** dans `src/` : groupe EV, équipe, domaines, absences
récurrentes. Tout passe par `admin_config`. Les seuls hardcodes acceptables
sont les **filets de sécurité** (DEFAULT_GROUP_ID, DEFAULT_EV_ORIGINS pour
le 1er install). Cf. v2026.5.41 pour la migration complète.
- **Ne jamais pousser sur Gitea sans validation explicite** de Quentin.
- **Toujours bumper la version** avant un push qui modifie le code.
- **Toujours mettre à jour le CHANGELOG** avant un push.
- **Tags non touchés** sur Gitea : `v1.0.0`-`v3.3.0`, `v4.1.x`-`v4.3.0`,
`v5.0.0`, `v2026.5.27`-`v2026.5.32` (ceux-là pointent vers du code
reconstitué historique, ne jamais les bouger).
- **Force-push uniquement si Quentin le demande explicitement.**
- **L'email de l'auteur** ne doit apparaître nulle part dans `src/` ni dans
les fichiers Markdown du repo (CLAUDE.md, README.md inclus). Il est stocké
uniquement en mémoire Claude (`user_role.md` / `gitea_token.md`) et exposé
obfusqué (entités HTML) sur la page wiki Contact.
---
## Pages wiki Gitea
| Page | Contenu | Quand mettre à jour |
|---|---|---|
| **Home** | Pitch, contexte, démarrage rapide | Rarement |
| **Utilisation** | Guide complet pour l'utilisateur | À chaque changement UX |
| **Versions** | Historique détaillé des versions | À chaque release |
| **Architecture** | Doc technique (fonctions, config, structure) | À chaque ajout d'helper / changement structurel |
| **Contact** | Email obfusqué + lien Issue Gitea | Rarement |
URL de base wiki : `https://gitea.netaplaid.ch/FroSteel/Planification/wiki/<NOM>`
Endpoint API : `/api/v1/repos/FroSteel/Planification/wiki/page/<NOM>` (PATCH avec
`content_base64`).
---
## Pour résumer ton rôle, Claude
Quentin demande une modif → tu codes → tu builds → il teste → il valide →
tu push tout (Gitea + wiki + firefox-updates.json). Plus tard il revient avec
le `.xpi` signé d'AMO → tu mets à jour la release et le `update_hash` du JSON.
Si tu hésites sur quoi faire à un moment, **demande**. Ne suppose pas.
+43 -119
View File
@@ -7,8 +7,7 @@ Extension Chrome / Firefox pour visualiser de manière claire et rapide le plann
- **Auteur** : Quentin Rouiller (QRO) - **Auteur** : Quentin Rouiller (QRO)
- **Cible** : techniciens DGNSI (Canton de Vaud), EasyVista (`itsma.etat-de-vaud.ch` / `itsma.vd.ch`) - **Cible** : techniciens DGNSI (Canton de Vaud), EasyVista (`itsma.etat-de-vaud.ch` / `itsma.vd.ch`)
- **Démarrage projet** : jeudi 16 avril 2026 - **Démarrage projet** : jeudi 16 avril 2026
- **Version actuelle** : `v2026.5.43` - **Version actuelle** : `v2026.5.37`
- **Contact** : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact)
- **Manifest** : V3 (Chrome/Edge/Firefox) - **Manifest** : V3 (Chrome/Edge/Firefox)
- **Format** : `.zip` (Chromium) + `.xpi` signé (Firefox) - **Format** : `.zip` (Chromium) + `.xpi` signé (Firefox)
@@ -17,13 +16,12 @@ Extension Chrome / Firefox pour visualiser de manière claire et rapide le plann
### 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
- Équipe configurable depuis le panel admin (détection automatique via EasyVista) - 8 techniciens hardcodés (équipe IT canton)
- 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`)
@@ -40,7 +38,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
- Absences récurrentes (configurées par tech) : cyan (depuis v2026.5.30) - Absence récurrente Pillonel vendredi : 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
@@ -50,11 +48,9 @@ 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 : bouton ⚙ Paramètres dans popup user-badge (depuis v2026.5.25) - Mode admin caché : bouton ⚙ Paramètres dans popup user-badge (depuis v2026.5.25, remplace les 5 clics secrets sur le titre)
- Configuration persistée dans `chrome.storage.local` (`admin_config`) - Configuration persistée dans `localStorage` (`admin_config`)
- 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 - Catégories interventions personnalisables (livraison/recup/remplacement/incident/rollout/reservation/autre)
- É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)
## Versionning — historique et conventions ## Versionning — historique et conventions
@@ -64,7 +60,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.40` | | 21 avril 2026 → maintenant | **`ANNÉE.MAJEURE.PATCH`** | `2026.5.16``2026.5.37` |
### Format actuel : `ANNÉE.MAJEURE.PATCH` ### Format actuel : `ANNÉE.MAJEURE.PATCH`
@@ -74,11 +70,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) |
| `40` | **Patch** | À **chaque livraison** dans la majeure courante (corrections, ajustements, petites features) | | `37` | **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.40``2026.6.0` : refonte majeure (par exemple nouvelle vue, nouvelle architecture) - `2026.5.37``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).
@@ -87,68 +83,29 @@ Le numéro de **majeure** n'est **pas** un mois et **pas** un chiffre lié au ca
## Versions notables ## Versions notables
### `v2026.5.43` (latest, 27 avril 2026) — Fix Firefox : menu dock + stabilité popup pin/unpin ### `v2026.5.37` (latest, 25 avril 2026) — Refonte vue horizontale
- Firefox : le menu hover sur les pastilles du dock (popup réduit) se - Topbar supprimée en vue horizontale, tout passe en sidebar
positionne désormais correctement au-dessus de la pastille. - User-badge + titre + bouton "Aujourd'hui" + date/heure + sélecteur + flèches + stats dans sidebar
- Pin/unpin : la popup épinglée ne bouge plus et garde la même taille - Banderole pompier masquée (badge + barre rouge gauche conservés)
quand on la dé-épingle / re-épingle.
### `v2026.5.42` — Nettoyage de commentaires + exemples génériques ### `v2026.5.36` — Sidebar verticale
- Uniformisation des exemples utilisés dans les commentaires de `viewer.js`
(parsing contacts/lieux/références/codes-barres) en placeholders abstraits.
Comportement runtime strictement inchangé.
### `v2026.5.41` — Suppression des hardcodes + UX admin + thème unifié
- **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.
- **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.
### `v2026.5.40` — Sélection groupe EV + édition domaines + tri équipe + vue horizontale enrichie
- **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.
- **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.
### `v2026.5.39` — Séparation Matin / Après-midi + Apparence
- 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
- Section **À propos** (version, auteur, licence)
### `v2026.5.38` — Attribution auteur + nettoyage + observabilité
- Module `LOG` unifié + handlers globaux d'erreur
- Toggle "Logs verbeux (debug)" dans le panel admin
- En-têtes copyright dans tous les fichiers source
### `v2026.5.37` — Refonte vue horizontale (sidebar complète)
- Topbar entièrement déplacée en sidebar verticale
### `v2026.5.36` — Sidebar verticale en vue horizontale
- Wrapper flex-row `#horizontal-wrapper` [sidebar 200px] + [main] - Wrapper flex-row `#horizontal-wrapper` [sidebar 200px] + [main]
- Déplacement physique des éléments via `ELEMENTS_TO_RELOCATE`
- Restauration propre en vue classique
### `v2026.5.32` — Vue horizontale togglable ### `v2026.5.32` — Vue horizontale togglable
- Bouton ⊞ "Vue" dans popup user-badge - Bouton ⊞ "Vue" dans popup user-badge
- Chaque tech = 1 ligne horizontale compacte
- localStorage `view_mode`
### `v2026.5.27` — Classification absences ### `v2026.5.27` — Classification absences
- Maladie indigo, Congé cyan, Pompier rouge - ABSENCE_LABELS : `^(cong[ée]s|maladie|pompier)$`
- Couleurs catégories
- Topbar une ligne : "Jeudi 23.04.26 • 21:55"
### `v4.2.3` — Grande popup timeline persistante ### `v4.2.3` — Grande popup timeline persistante
- Clic segment timeline = popup persistante - Clic segment timeline = popup persistante
- Hover = popup qui suit la souris
### `v4.1.3` — Tooltips épinglables ### `v4.1.3` — Tooltips épinglables
- Introduction de `pinTooltip` - Introduction de `pinTooltip`
@@ -156,30 +113,17 @@ Le numéro de **majeure** n'est **pas** un mois et **pas** un chiffre lié au ca
### `v1.0.0` (16 avril 2026) — Initiale ### `v1.0.0` (16 avril 2026) — Initiale
- Premier viewer EasyVista pour le canton - Premier viewer EasyVista pour le canton
Voir [CHANGELOG.md](CHANGELOG.md) pour l'historique complet (40+ versions taggées). Voir [CHANGELOG.md](CHANGELOG.md) pour l'historique complet (40 versions taggées).
## Architecture technique ## Architecture technique
``` ```
Planning/ manifest.json # Manifest V3 (Chrome) + gecko_settings (Firefox)
├── src/ # Sources de l'extension (chargées par le navigateur) background.js # Worker fond : fetch planning XML, gestion session, fetch fiches
│ ├── manifest.json # Manifest V3 (Chrome) + gecko_settings (Firefox) viewer.html # Interface principale
│ ├── background.js # Service worker : fetch planning XML, gestion session, fetch fiches viewer.js # Logique (~9000 lignes) — voir détail ci-dessous
│ ├── viewer.html # Interface principale viewer.css # Styles + thèmes clair/sombre
│ ├── viewer.js # Logique (~9 500 lignes) icons/ # icon16, icon48, icon128
│ ├── viewer.css # Styles + thèmes clair/sombre
│ └── icons/ # icon16, icon48, icon128
├── Autres/ # Méta : build script + docs (depuis v2026.5.40)
│ ├── build.sh # Génère dist/chromium/, dist/firefox/, .zip, .xpi
│ ├── CHANGELOG.md
│ ├── LICENSE
│ └── README.md
├── Builds/ # Artefacts distribués aux techniciens
│ ├── Chromium/
│ ├── Firefox/
│ ├── planification-vYYYY.M.PATCH-chromium.zip
│ └── planification-vYYYY.M.PATCH-firefox.xpi
└── dist/ # Sortie de build (gitignoré)
``` ```
### `viewer.js` — fonctions clés ### `viewer.js` — fonctions clés
@@ -196,42 +140,30 @@ Planning/
| `_softUnpinPopup` | v4.3.3 | Désépinglage mou (popup reste visible) | | `_softUnpinPopup` | v4.3.3 | Désépinglage mou (popup reste visible) |
| `initAppClock` | v5.0.0 | Horloge HH:MM topbar | | `initAppClock` | v5.0.0 | Horloge HH:MM topbar |
| `initSessionTimer` | v5.0.0 | Compteur session EV (tick 1s) | | `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 | | `_applyViewMode` | v2026.5.32 | Toggle vue classique/horizontale |
| `_maybeRetryFetchUser` | v2026.5.34 | Relance opportuniste fetch user | | `_maybeRetryFetchUser` | v2026.5.34 | Relance opportuniste fetch user |
| `positionTooltipAnchored` | v2026.5.34 | Positionnement unifié (4 candidats) | | `positionTooltipAnchored` | v2026.5.34 | Positionnement unifié (4 candidats) |
| `renderAdminSectionTeam` | v5.0.0 | Onglet admin Équipe (sélecteur groupe EV depuis v2026.5.40) |
| `renderAdminSectionEV` | v5.0.0 | Onglet admin EasyVista (édition domaines depuis v2026.5.40) |
### `background.js` — fonctions clés ### Constantes persistantes (toutes versions)
| Fonction | Rôle | - 8 techs hardcodés : `76272,83725,66635,92235,90070,40944,72485,86874`
|---|---| - Pillonel Olivier (ID 40944) : absent tous les vendredis (hardcodé)
| `findEasyVistaSession` | Trouve l'onglet EV ouvert + extrait PHPSESSID | - Group ID EasyVista : `191`
| `fetchPlanningXml` | Fetch XML calendar_block du planning |
| `fetchFicheHtml` | Fetch HTML d'une fiche EV (avec retry SSO) |
| `fetchCurrentUser` | Identifie l'user EV connecté |
| `detectGroupsFromEV` (v2026.5.40) | Parse le `<select id="plan_group_id">` → liste des groupes EV |
| `detectTeamFromEV` | Liste les membres d'un groupe (paramétrable depuis v2026.5.40) |
| `evFetch` | Wrapper fetch avec headers EV (Referer, X-Requested-With) |
### Constantes / valeurs hardcodées (toutes versions)
- Group ID EV par défaut : `191` (SI-CSS) — surchargeable via le sélecteur depuis v2026.5.40
- Absences récurrentes par tech : configurables via Paramètres → Équipe (depuis v2026.5.41)
- GUIDs forms EV : - GUIDs forms EV :
- Demande : `S={C99ECD05-3D48-4C62-ABF0-66292053AED6}` - Demande : `S={C99ECD05-3D48-4C62-ABF0-66292053AED6}`
- Incident : `I={07ED9C68-6172-48EA-8A58-90912B0A283E}` - Incident : `I={07ED9C68-6172-48EA-8A58-90912B0A283E}`
- SSO : Canton ForgeRock OpenAM - SSO : Canton ForgeRock OpenAM
- Storage keys : `admin_config`, `view_mode` - Storage keys : `admin_config`, `view_mode` (depuis v2026.5.32)
- Domaines : `itsma.etat-de-vaud.ch` (interne), `itsma.vd.ch` (externe) - Domaines : `itsma.etat-de-vaud.ch` (interne), `itsma.vd.ch` (externe SSO)
## Installation ## Installation
### Firefox ### Firefox
Télécharger le `.xpi` depuis `Builds/` ou le serveur de mises à jour interne, puis drag-and-drop dans `about:addons`. Télécharger le `.xpi` signé depuis le serveur de mises à jour interne, ou drag-and-drop dans `about:addons`.
### Chrome / Edge ### Chrome / Edge
Mode développeur : décompresser `Builds/planification-vYYYY.M.PATCH-chromium.zip` (ou utiliser directement `dist/chromium/`) et charger en tant qu'extension non empaquetée. Mode développeur : décompresser le ZIP et charger en tant qu'extension non empaquetée.
## Développement ## Développement
@@ -239,19 +171,12 @@ Mode développeur : décompresser `Builds/planification-vYYYY.M.PATCH-chromium.z
git clone https://gitea.netaplaid.ch/FroSteel/Planification.git git clone https://gitea.netaplaid.ch/FroSteel/Planification.git
cd Planification cd Planification
# Modifier les sources dans src/ # Pour packager une nouvelle version :
# Bumper la version dans src/manifest.json + ajouter une entrée dans Autres/CHANGELOG.md # 1. modifier le code
# Builder : # 2. bump version dans manifest.json
./Autres/build.sh # 3. zip + xpi
# → produit dist/chromium/, dist/firefox/, dist/*.zip, dist/*.xpi
# Copier dans Builds/ pour distribution :
cp -r dist/chromium Builds/Chromium
cp -r dist/firefox Builds/Firefox
cp dist/*.zip dist/*.xpi Builds/
git add -A git add -A
git commit -m "vYYYY.M.PATCH — description" git commit -m "Version YYYY.M.PATCH — description"
git tag vYYYY.M.PATCH git tag vYYYY.M.PATCH
git push origin main git push origin main
git push --tags git push --tags
@@ -265,4 +190,3 @@ 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)
+32 -229
View File
@@ -115,74 +115,11 @@ 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)
// v2026.5.41 : Configuration runtime — lue depuis admin_config (chrome.storage.local). const EV_ORIGINS = [
// "https://itsma.etat-de-vaud.ch",
// Les domaines EV et le group_id ont des défauts (filet de sécurité 1er install). "https://itsma.vd.ch"
// 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
@@ -210,10 +147,8 @@ chrome.action.onClicked.addListener(async () => {
* @author Quentin Rouiller * @author Quentin Rouiller
*/ */
async function findEasyVistaSession() { async function findEasyVistaSession() {
// v2026.5.41 : les origines EV viennent de admin_config (éditables dans // Chercher tous les onglets sur un domaine EasyVista
// Paramètres → EasyVista), avec fallback sur DEFAULT_EV_ORIGINS. for (const origin of EV_ORIGINS) {
const origins = await getEvOrigins();
for (const origin of origins) {
const tabs = await chrome.tabs.query({ url: origin + "/*" }); const tabs = await chrome.tabs.query({ url: origin + "/*" });
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]+)/);
@@ -240,20 +175,8 @@ async function findEasyVistaSession() {
* @author Quentin Rouiller * @author Quentin Rouiller
*/ */
async function fetchPlanningXml(origin, phpsessid, unixDate) { async function fetchPlanningXml(origin, phpsessid, unixDate) {
// v2026.5.41 : groupId vient de admin_config (défaut SI-CSS). const techIds = "76272,83725,66635,92235,90070,40944,72485,86874";
// techIds vient de admin_config — si vide, on lève une erreur claire pour const groupId = "191";
// 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)}` +
@@ -267,8 +190,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=${start}` + `&day_start_hour=8` +
`&day_end_hour=${end + 1}`; `&day_end_hour=19`;
// 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);
@@ -664,26 +587,17 @@ 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)}`;
// v2026.5.40 : on passe par evFetch() qui ajoute les headers Referer + const resp = await fetch(url, {
// 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) { if (!resp.ok) return null;
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)) { if (looksLikeLoginPage(html)) return null;
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
@@ -755,18 +669,7 @@ async function fetchCurrentUser(origin, phpsessid) {
} }
} }
if (!name && !login && !service) { if (!name && !login && !service) return null;
// 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 };
} }
@@ -791,8 +694,6 @@ 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)}`
@@ -801,8 +702,8 @@ async function submitAbsence(origin, phpsessid, opts) {
+ `&ROOT_DIRECTORY=${encodeURIComponent("/ccv/data/www/itsma/htdocs/")}` + `&ROOT_DIRECTORY=${encodeURIComponent("/ccv/data/www/itsma/htdocs/")}`
+ `&current_date=${encodeURIComponent(opts.currentDate)}` + `&current_date=${encodeURIComponent(opts.currentDate)}`
+ `&empl_ids=${encodeURIComponent(emplIds)}` + `&empl_ids=${encodeURIComponent(emplIds)}`
+ `&begin_hour=${start}` + `&begin_hour=8`
+ `&end_hour=${end}` + `&end_hour=18`
+ `&plagehoraire=0`; + `&plagehoraire=0`;
const body = new URLSearchParams(); const body = new URLSearchParams();
@@ -864,8 +765,6 @@ 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`
@@ -875,8 +774,8 @@ async function submitDouchette(origin, phpsessid, opts) {
+ `&ROOT_DIRECTORY=${encodeURIComponent("/ccv/data/www/itsma/htdocs/")}` + `&ROOT_DIRECTORY=${encodeURIComponent("/ccv/data/www/itsma/htdocs/")}`
+ `&current_date=${encodeURIComponent(opts.currentDate)}` + `&current_date=${encodeURIComponent(opts.currentDate)}`
+ `&empl_ids=${encodeURIComponent(emplIds)}` + `&empl_ids=${encodeURIComponent(emplIds)}`
+ `&begin_hour=${start}` + `&begin_hour=8`
+ `&end_hour=${end}` + `&end_hour=18`
+ `&plagehoraire=0`; + `&plagehoraire=0`;
const body = new URLSearchParams(); const body = new URLSearchParams();
@@ -1026,80 +925,9 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) {
} }
// ============================================================================ // ============================================================================
// v2026.5.41 : Détection des GROUPES EasyVista (SI-CSS, SI-EXT, …) // v5.0.0 : Détection de la liste des techniciens depuis la page planning EV
// ============================================================================ // ============================================================================
/**
* 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.
* *
@@ -1114,15 +942,14 @@ async function detectGroupsFromEV(origin, phpsessid) {
* *
* Retourne { ids: [{id, name, alreadyInTeam}], groupId }. * Retourne { ids: [{id, name, alreadyInTeam}], groupId }.
*/ */
async function detectTeamFromEV(origin, phpsessid, groupIdArg, supportIdsArg) { async function detectTeamFromEV(origin, phpsessid) {
// v2026.5.41 : groupId vient de l'argument, sinon de admin_config (défaut SI-CSS). // v5.0.1 : valeurs par défaut (correspondent au groupe actuel).
// supportIds vient de l'argument, sinon de admin_config (vide tant que rien n'est // À terme elles devraient venir de la config admin.
// sélectionné). Le serveur retourne tous les membres du groupe quoi qu'il arrive ; const DEFAULT_GROUP_ID = "191";
// supportIds sert juste à pré-cocher les techs déjà inclus dans l'équipe. const DEFAULT_SUPPORT_IDS = "76272,83725,66635,92235,90070,40944,72485,86874";
const groupId = groupIdArg || await getGroupId();
const supportIds = (typeof supportIdsArg === "string") const groupId = DEFAULT_GROUP_ID;
? supportIdsArg const supportIds = DEFAULT_SUPPORT_IDS;
: 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
@@ -1145,9 +972,8 @@ async function detectTeamFromEV(origin, phpsessid, groupIdArg, supportIdsArg) {
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);
// v2026.5.41 : on retourne les IDs déjà sélectionnés par l'user (s'il y en a) // Fallback : au moins on retourne les IDs connus avec noms vides
// plutôt qu'une liste hardcodée. Si vide, le viewer affichera juste 0 résultat. const ids = DEFAULT_SUPPORT_IDS.split(",").filter(Boolean);
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
@@ -1175,7 +1001,7 @@ async function detectTeamFromEV(origin, phpsessid, groupIdArg, supportIdsArg) {
} }
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="NNNNN">Nom...</option> // Pattern 2 : fallback <option value="76272">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;
@@ -1402,39 +1228,16 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
return; return;
} }
if (msg.type === "detectGroups") {
// v2026.5.41 : détecte la liste des groupes EV (SI-CSS, SI-EXT, …)
// depuis le <select id="plan_group_id"> de la page planning.
const session = await findEasyVistaSession();
if (!session) {
sendResponse({ ok: false, error: "no_session" });
return;
}
try {
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") { if (msg.type === "detectTeam") {
// v5.0.0 : détecte la liste des IDs de techniciens depuis le HTML // 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 // 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(); 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( const result = await detectTeamFromEV(session.origin, session.phpsessid);
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) {
-98
View File
@@ -1,98 +0,0 @@
#!/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é."
-28
View File
@@ -1,28 +0,0 @@
{
"addons": {
"planification-dgnsi@netaplaid.ch": {
"updates": [
{
"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:9d1f0cb49b98cdb46a0e022dc53c77c98fde380590e72036e188c128d6b19965"
},
{
"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"
}
]
}
}
}

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 -4
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Planification", "name": "Planification",
"version": "2026.5.43", "version": "2026.5.39",
"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",
@@ -14,9 +14,6 @@
"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"
}, },
+34 -518
View File
@@ -896,43 +896,6 @@ html.view-horizontal .timeline-noon {
position: relative; position: relative;
} }
/* ==========================================================================
v2026.5.41 : Conflit absence/réservation × intervention.
Quand une intervention est planifiée pendant qu'un tech est marqué
"absent" (toute la journée ou demi-journée) ou qu'il a une réservation
sur le même créneau, on signale visuellement le conflit en peignant la
carte de l'intervention en rouge plein. S'applique à la row classique
(.intervention-v2) ET à la mini-card (.iv-mini-card) en vue horizontale.
========================================================================== */
.intervention-v2.intervention-conflict-absence,
.iv-mini-card.intervention-conflict-absence {
background: #b03030 !important;
color: #ffffff !important;
border-color: #7a1f1f !important;
}
.intervention-v2.intervention-conflict-absence::before {
/* Renforce la barre gauche pour rester cohérent avec le rouge plein. */
background: #7a1f1f !important;
}
.intervention-v2.intervention-conflict-absence .intervention-dot,
.intervention-v2.intervention-conflict-absence .iv-status-check,
.intervention-v2.intervention-conflict-absence a,
.iv-mini-card.intervention-conflict-absence .iv-mini-card-bar,
.iv-mini-card.intervention-conflict-absence .iv-mini-time-vertical,
.iv-mini-card.intervention-conflict-absence .iv-mini-card-text {
color: #ffffff !important;
background: transparent !important;
}
/* La barre couleur catégorie à gauche de la mini-card devient un blanc
semi-transparent pour rester visible sur le rouge. */
.iv-mini-card.intervention-conflict-absence .iv-mini-card-bar {
background: rgba(255, 255, 255, 0.45) !important;
}
.intervention-v2.intervention-conflict-absence:hover,
.iv-mini-card.intervention-conflict-absence:hover {
background: #c44040 !important;
}
/* ========================================================================== /* ==========================================================================
Interventions layout v2 (heures verticales) Interventions layout v2 (heures verticales)
========================================================================== */ ========================================================================== */
@@ -1395,29 +1358,6 @@ html.view-horizontal .timeline-noon {
.tt-ref-val { .tt-ref-val {
font-family: var(--mono, monospace); font-family: var(--mono, monospace);
} }
/* v2026.5.40 r18 : référence cliquable ouvre la fiche EV. Style "lien"
immédiatement reconnaissable : couleur accent, soulignée, cursor pointer. */
.tt-ref-link {
font-family: var(--mono, monospace);
color: var(--accent);
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
font-weight: 600;
transition: color 0.1s, text-shadow 0.1s;
pointer-events: auto;
}
.tt-ref-link:hover {
color: var(--accent);
text-shadow: 0 0 6px rgba(59, 130, 246, 0.4);
}
.tt-ref-link::after {
content: " ↗";
font-family: inherit;
font-size: 0.85em;
opacity: 0.7;
margin-left: 2px;
}
.tt-copy-btn { .tt-copy-btn {
background: transparent; background: transparent;
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -1519,9 +1459,7 @@ html.view-horizontal .timeline-noon {
position: fixed; position: fixed;
bottom: 20px; bottom: 20px;
right: 20px; right: 20px;
/* v2026.5.41 : au-dessus du modal-overlay du panel admin (z-index 10000) z-index: 200;
pour que les toasts de feedback restent visibles avec le flou en arrière. */
z-index: 11000;
display: flex; display: flex;
flex-direction: column-reverse; /* les nouveaux en bas, les anciens au-dessus */ flex-direction: column-reverse; /* les nouveaux en bas, les anciens au-dessus */
gap: 8px; gap: 8px;
@@ -1670,7 +1608,7 @@ html.view-horizontal .timeline-noon {
} }
/* /*
v4.1.20 : Message d'absence récurrente (configurée par tech) v4.1.20 : Message d'absence récurrente (Pillonel vendredi)
*/ */
.tech-absence-recurring { .tech-absence-recurring {
padding: 14px 12px; padding: 14px 12px;
@@ -2013,14 +1951,11 @@ body.modal-open {
z-index: 5 !important; z-index: 5 !important;
opacity: 1 !important; opacity: 1 !important;
pointer-events: auto !important; pointer-events: auto !important;
/* v2026.5.43 : on conserve les MÊMES dimensions que .pinned-popup /* Pas de bordure bleue, pas de padding-top (plus de dragbar), juste les
(padding-top 28px, border 2px) pour que la popup ne bouge ni ne change styles de base du tooltip (hérités de .tooltip). */
de taille au softUnpin. La dragbar est juste retirée (l'espace border: 1px solid var(--border-strong) !important;
reste, c'est le tradeoff pour préserver position + taille).
Bordure plus discrète (variable --border-strong au lieu de --accent). */
border: 2px solid var(--border-strong) !important;
box-shadow: var(--shadow-hover) !important; box-shadow: var(--shadow-hover) !important;
padding-top: 28px !important; padding-top: 12px !important;
animation: none !important; animation: none !important;
} }
@@ -2900,10 +2835,7 @@ header.topbar::before {
========================================================================== */ ========================================================================== */
.session-slide-alert { .session-slide-alert {
position: fixed; position: fixed;
/* v2026.5.40 r12 : on positionne l'alerte SOUS les initiales (badge user) top: 60px;
pour ne pas chevaucher l'horloge centrale. La topbar fait ~48px donc
top: 56px laisse 8px de respiration sous le badge. */
top: 56px;
left: -420px; /* hors écran au départ */ left: -420px; /* hors écran au départ */
width: 380px; width: 380px;
max-width: calc(100vw - 40px); max-width: calc(100vw - 40px);
@@ -2917,17 +2849,10 @@ header.topbar::before {
transition: left 0.28s ease-out, opacity 0.28s; transition: left 0.28s ease-out, opacity 0.28s;
opacity: 0; opacity: 0;
} }
/* v2026.5.40 r12 : alignée avec le bord gauche de la topbar (à hauteur
des initiales left: 14px = padding gauche de la topbar). */
.session-slide-alert.visible { .session-slide-alert.visible {
left: 14px; left: 20px;
opacity: 1; opacity: 1;
} }
/* En vue horizontale, l'alerte vient sous le badge user dans la sidebar
gauche, donc on laisse left: 14px aussi (la sidebar a son propre padding). */
html.view-horizontal .session-slide-alert.visible {
left: 14px;
}
.session-slide-alert.urgent { .session-slide-alert.urgent {
border-left-color: #ef4444; border-left-color: #ef4444;
animation: session-pulse 1.4s ease-in-out infinite; animation: session-pulse 1.4s ease-in-out infinite;
@@ -3048,12 +2973,9 @@ body.popup-dragging .pinned-popup {
min-width: 130px; min-width: 130px;
animation: pill-hover-menu-appear 0.12s ease-out; animation: pill-hover-menu-appear 0.12s ease-out;
} }
/* v2026.5.43 : pas de transform dans cette animation Firefox inclut les
transforms dans getBoundingClientRect ce qui fausse le calcul de position
du menu juste après son insertion dans le DOM. Animation en opacité seule. */
@keyframes pill-hover-menu-appear { @keyframes pill-hover-menu-appear {
from { opacity: 0; } from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; } to { opacity: 1; transform: translateY(0); }
} }
.pill-hover-menu-btn { .pill-hover-menu-btn {
display: flex; display: flex;
@@ -3407,14 +3329,10 @@ html.theme-dark .card.absence-cat-conge {
réduisait la largeur depuis le côté droit désormais ils restent à leur réduisait la largeur depuis le côté droit désormais ils restent à leur
taille créée et sont simplement repositionnés dans la safe area. taille créée et sont simplement repositionnés dans la safe area.
========================================================================== */ ========================================================================== */
/* v2026.5.40 r17 : on retire le `!important` sur width pour permettre à
pinTooltip d'imposer la même largeur que le tooltip live au moment du
clic (via style.width inline). 520px reste la valeur par défaut si le
JS ne définit rien. */
.pinned-popup:not(.pinned-popup-minimized):not(.pinned-popup-reduced) { .pinned-popup:not(.pinned-popup-minimized):not(.pinned-popup-reduced) {
max-width: none !important; max-width: none !important;
width: 520px; /* défaut, peut être override par pinTooltip */ width: 520px !important; /* largeur standard fixe */
min-width: 320px; min-width: 380px;
} }
/* Sur les très petits écrans (< 600px), on laisse le clamp naturel */ /* Sur les très petits écrans (< 600px), on laisse le clamp naturel */
@media (max-width: 600px) { @media (max-width: 600px) {
@@ -3425,7 +3343,7 @@ html.theme-dark .card.absence-cat-conge {
} }
/* ========================================================================== /* ==========================================================================
v2026.5.30 : absences récurrentes (configurées par tech) en cyan v2026.5.30 : absences récurrentes (Pillonel vendredi) en cyan
(même couleur que Congé mais texte distinct "Absent le vendredi") (même couleur que Congé mais texte distinct "Absent le vendredi")
========================================================================== */ ========================================================================== */
@@ -3595,51 +3513,34 @@ html.view-horizontal .card {
} }
/* Header devient une barre latérale gauche fixe */ /* Header devient une barre latérale gauche fixe */
/* v2026.5.40 r15 : 180px pour loger "Maladie/Accident" en entier sans /* v2026.5.35 : réduit à 140px (au lieu de 200px) pour donner plus de place à la timeline */
tronquer (libellé le plus long parmi tous les badges). */
html.view-horizontal .card-header { html.view-horizontal .card-header {
flex-direction: column !important; flex-direction: column !important;
align-items: flex-start !important; align-items: flex-start !important;
justify-content: center !important; justify-content: center !important;
min-width: 180px !important; min-width: 140px !important;
max-width: 180px !important; max-width: 140px !important;
border-bottom: none !important; border-bottom: none !important;
border-right: 1px solid var(--border) !important; border-right: 1px solid var(--border) !important;
padding: 6px 10px !important; padding: 6px 10px !important;
gap: 3px !important; gap: 3px !important;
flex: 0 0 auto; flex: 0 0 auto;
} }
/* v2026.5.40 r8 : nom du tech complet (jusqu'à 3 lignes). Si vraiment
trop long, .card-tech-name-tight est appliqué dynamiquement par le JS
pour réduire légèrement la font-size (12px 11.5px). */
html.view-horizontal .card-tech-name { html.view-horizontal .card-tech-name {
font-size: calc(13px * var(--text-scale)) !important; font-size: 13px !important;
font-weight: 600; font-weight: 600;
line-height: 1.2 !important; line-height: 1.2 !important;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
max-width: 100%; max-width: 100%;
word-break: break-word;
hyphens: auto;
} }
html.view-horizontal .card-tech-name.card-tech-name-tight {
font-size: calc(11.5px * var(--text-scale)) !important;
letter-spacing: -0.1px;
}
/* v2026.5.40 r16 : badge tech compact, sidebar 180px. Le texte est décalé
un peu vers la gauche via padding-right > padding-left (effet visuel
demandé). */
html.view-horizontal .card-tech-badge { html.view-horizontal .card-tech-badge {
font-size: calc(8.5px * var(--text-scale)) !important; font-size: 10px !important;
padding: 2px 14px 2px 4px !important; padding: 2px 6px !important;
line-height: 1.3 !important;
white-space: nowrap; white-space: nowrap;
text-align: center;
align-self: flex-start;
max-width: 100%;
letter-spacing: 0.02em;
} }
/* Le body prend le reste de la ligne, scroll horizontal si trop d'interv */ /* Le body prend le reste de la ligne, scroll horizontal si trop d'interv */
@@ -3658,18 +3559,7 @@ html.view-horizontal .timeline {
border-bottom: none !important; border-bottom: none !important;
} }
html.view-horizontal .timeline-bar { html.view-horizontal .timeline-bar {
height: 10px !important; /* v2026.5.40 r4 : timeline très fine */ height: 22px !important;
}
/* Timeline elle-même : padding réduit pour gagner de la hauteur */
html.view-horizontal .timeline {
padding: 4px 14px 2px 14px !important;
}
/* Échelle d'heures plus compacte */
html.view-horizontal .timeline-scale {
height: 11px !important;
}
html.view-horizontal .timeline-tick {
font-size: calc(10px * var(--text-scale)) !important;
} }
/* Liste interventions en mode "chips" (défilement horizontal) */ /* Liste interventions en mode "chips" (défilement horizontal) */
@@ -3694,22 +3584,17 @@ html.view-horizontal .card-header::after {
} }
html.view-horizontal .tech-row-stats { html.view-horizontal .tech-row-stats {
display: flex; display: flex;
flex-wrap: wrap; /* libellés complets → permet wrap */ gap: 8px;
gap: 4px; font-size: 11px;
font-size: calc(11px * var(--text-scale));
color: var(--text-muted); color: var(--text-muted);
margin-top: 4px; margin-top: 2px;
width: 100%;
justify-content: flex-start;
} }
html.view-horizontal .tech-row-stats .stat-pill { html.view-horizontal .tech-row-stats .stat-pill {
padding: 2px 8px; padding: 1px 6px;
background: var(--bg-muted); background: var(--bg-muted);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 10px; border-radius: 3px;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: calc(10.5px * var(--text-scale));
white-space: nowrap;
} }
/* En vue classique, on cache les éléments spécifiques horizontal */ /* En vue classique, on cache les éléments spécifiques horizontal */
@@ -3882,26 +3767,9 @@ html.view-horizontal .horizontal-sidebar #stats .global-stat b {
html.view-horizontal .horizontal-sidebar #stats .global-stat-main b { html.view-horizontal .horizontal-sidebar #stats .global-stat-main b {
font-size: 15px !important; font-size: 15px !important;
} }
/* v2026.5.40 r19 : en sidebar horizontale, on cache tous les séparateurs
sauf le `+` (classe .global-stat-sep-keep) qui s'affiche en bloc CENTRÉ
entre "tech. dispo" et "pompier". */
html.view-horizontal .horizontal-sidebar #stats .global-stat-sep { html.view-horizontal .horizontal-sidebar #stats .global-stat-sep {
display: none !important; display: none !important;
} }
html.view-horizontal .horizontal-sidebar #stats .global-stat-sep.global-stat-sep-keep {
display: block !important;
width: 100% !important;
text-align: center !important;
color: var(--text-faint);
font-size: 14px;
font-weight: 700;
padding: 0;
/* Compenser le gap parent (#stats a gap: 4px) pour que le `+` soit serré
entre "tech. dispo" et "pompier" pas d'espace artificiel autour. */
margin: -3px auto -3px auto;
line-height: 1;
align-self: center;
}
html.view-horizontal .horizontal-sidebar #stats .global-stat-sub { html.view-horizontal .horizontal-sidebar #stats .global-stat-sub {
display: block !important; display: block !important;
font-size: 10px; font-size: 10px;
@@ -3959,8 +3827,8 @@ html.view-horizontal main .stats {
font-size: 11px; font-size: 11px;
} }
html.view-horizontal .card-header { html.view-horizontal .card-header {
min-width: 160px !important; /* v2026.5.40 r15 : un peu plus serré sur petit écran */ min-width: 110px !important;
max-width: 160px !important; max-width: 110px !important;
} }
} }
@@ -4211,18 +4079,10 @@ html.view-horizontal #progress-bar {
} }
} }
/* v2026.5.40 r17 : banderole "En pompier du..." en vue HORIZONTALE /* 14. Banderole "En pompier du..." : masquée en vue horizontale uniquement
uniquement pleine largeur, hauteur modérée (2 fois plus que r14d), (la barre rouge à gauche et le badge POMPIER restent visibles). */
texte centré. */
html.view-horizontal .card-status-note.pompier { html.view-horizontal .card-status-note.pompier {
padding: 4px 14px !important; display: none !important;
text-align: center !important;
line-height: 1.3 !important;
font-size: calc(11.5px * var(--text-scale)) !important;
margin: 0 !important;
border-radius: 0 !important;
width: auto !important;
display: block !important;
} }
/* ========================================================================== /* ==========================================================================
@@ -4515,347 +4375,3 @@ html.view-horizontal .day-period-sep {
line-height: 1.55; line-height: 1.55;
color: var(--text); color: var(--text);
} }
/* ==========================================================================
v2026.5.40 : segments timeline enrichis (vue horizontale uniquement)
- Barre verticale couleur catégorie à gauche (comme les cards classique)
- Référence + ville lisibles dans le segment (si assez large)
- Cachés en vue classique (pas la place dans une timeline 20px de haut)
========================================================================== */
/* (retiré v2026.5.40 r3 : le contenu enrichi est maintenant dans
.iv-mini-cards en-dessous de la timeline) */
.timeline-slot-content { display: none; }
/* v2026.5.40 r3 : la timeline reste comme la vue classique (segments colorés
compacts). Les infos détaillées sont dans .iv-mini-cards juste en-dessous. */
/* ==========================================================================
v2026.5.40 r3 : mini-cartes d'intervention sous la timeline (vue horizontale
uniquement). Chaque carte contient ref / heure / ville / adresse, alignée
sur l'ordre temporel des interventions. Largeur proportionnelle à la durée
(flex-grow), avec min-width pour rester lisible. Trous représentés par des
espaces vides qui préservent l'alignement avec la timeline du dessus.
========================================================================== */
/* Par défaut (vue classique) : caché */
.iv-mini-cards { display: none; }
/* En vue horizontale : flex row qui prend toute la largeur toutes les
mini-cartes équidistantes. Si vraiment trop d'interv pour la largeur
(rare), elles se compriment via flex-shrink. */
html.view-horizontal .iv-mini-cards {
display: flex;
flex-direction: row;
align-items: stretch;
width: 100%;
gap: 3px;
padding: 2px 14px 4px 14px;
background: transparent;
overflow: hidden;
}
/* Carte mini d'intervention : v2026.5.40 r5 toutes les cartes ont la
MÊME largeur (flex: 1 1 0) pour qu'on puisse les voir toutes d'un coup
sans scroll. La position temporelle est donnée par la timeline au-dessus. */
.iv-mini-card {
flex: 1 1 0;
display: flex;
flex-direction: row;
align-items: stretch;
min-width: 0; /* permet au flex de bien rétrécir */
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
cursor: pointer;
transition: background 0.1s, border-color 0.1s, box-shadow 0.1s;
}
.iv-mini-card:hover {
background: var(--bg-hover);
border-color: var(--border-strong);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.iv-mini-card.highlight {
outline: 2px solid var(--accent);
outline-offset: -1px;
z-index: 2;
}
/* Barre verticale couleur catégorie à gauche de chaque carte (4 px) */
.iv-mini-card-bar {
flex: 0 0 4px;
background: var(--c-autre);
}
.iv-mini-card.color-livraison .iv-mini-card-bar { background: var(--c-livraison); }
.iv-mini-card.color-installation .iv-mini-card-bar { background: var(--c-installation); }
.iv-mini-card.color-recup .iv-mini-card-bar { background: var(--c-recup); }
.iv-mini-card.color-remplacement .iv-mini-card-bar { background: var(--c-remplacement); }
.iv-mini-card.color-incident .iv-mini-card-bar { background: var(--c-incident); }
.iv-mini-card.color-rollout .iv-mini-card-bar { background: var(--c-rollout); }
.iv-mini-card.color-reservation .iv-mini-card-bar { background: var(--c-reservation); }
.iv-mini-card.color-autre .iv-mini-card-bar { background: var(--c-autre); }
/* v2026.5.40 r7 : bloc heure VERTICAL (09:00 / ↓ / 10:00) */
.iv-mini-time-vertical {
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
padding: 4px 6px;
border-right: 1px solid var(--border);
font-variant-numeric: tabular-nums;
min-width: 44px;
}
.iv-mini-time-start,
.iv-mini-time-end {
font-size: calc(11px * var(--text-scale));
font-weight: 600;
color: var(--text);
line-height: 1.1;
}
.iv-mini-time-arrow {
font-size: calc(9px * var(--text-scale));
color: var(--text-faint);
line-height: 1;
margin: 1px 0;
}
/* Bloc texte (3 lignes) à droite — v2026.5.40 r7 : centré horizontalement */
.iv-mini-card-text {
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center; /* centre les lignes horizontalement */
gap: 1px;
padding: 4px 8px;
min-width: 0;
text-align: center;
}
.iv-mini-line {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
text-align: center;
}
.iv-mini-ref {
font-weight: 700;
font-size: calc(11px * var(--text-scale));
color: var(--text);
letter-spacing: 0.2px;
}
.iv-mini-ville {
font-size: calc(10.5px * var(--text-scale));
font-weight: 600;
color: var(--text);
}
.iv-mini-adresse {
font-size: calc(10px * var(--text-scale));
font-style: italic;
color: var(--text-faint);
}
/* ==========================================================================
v2026.5.40 r4 : compactage vertical des lignes tech en vue horizontale
- Card globale : pas de padding inutile en haut/bas
- Body : padding nul, gap minimal
- Card-stats (X interv. matin/apm) cachées (info redondante avec mini-cards)
========================================================================== */
html.view-horizontal .card {
margin-bottom: 6px !important;
border-radius: 6px;
}
html.view-horizontal .card-body {
padding: 0 !important;
gap: 0 !important;
}
/* v2026.5.40 r6 : la card-stats du bas est CACHÉE en horizontal l'info est
désormais dans les pills (.tech-row-stats) du header tech, libellés complets. */
html.view-horizontal .card-body > .card-stats {
display: none !important;
}
/* Header tech plus compact (centrage vertical, hauteur auto) */
html.view-horizontal .card-header {
padding: 4px 10px !important;
}
/* v2026.5.40 r7 : chiffre du nombre d'interventions mis en évidence dans
la pill du header tech (ex: "4 interventions" le 4 plus gros). */
html.view-horizontal .tech-row-stats .stat-pill .stat-pill-big-num {
font-size: calc(15px * var(--text-scale));
font-weight: 800;
color: var(--text);
margin-right: 2px;
letter-spacing: 0;
}
html.view-horizontal .tech-row-stats .stat-pill {
display: inline-flex;
align-items: baseline;
gap: 0;
}
/* v2026.5.40 r8 : en vue horizontale, le dock des popups épinglés réduits
commence APRÈS la sidebar (200px). Sinon les pills étaient cachées sous
la sidebar fixe à gauche. */
html.view-horizontal .pinned-popups-dock {
left: 200px !important;
}
@media (max-width: 1400px) {
html.view-horizontal .pinned-popups-dock {
left: 170px !important;
}
}
/* v2026.5.40 r9 : 2 blocs MATIN / APRÈS-MIDI côte à côte. Chaque bloc a
son label en haut + ses mini-cards en dessous. Séparation visuelle
entre les 2 blocs via un gap plus large + bordure gauche sur l'après-midi. */
.iv-mini-block {
flex: 1 1 0;
display: flex;
flex-direction: column;
min-width: 0;
gap: 2px;
}
.iv-mini-block.period-afternoon {
border-left: 2px solid var(--border-strong);
padding-left: 8px;
margin-left: 8px;
}
.iv-mini-block-label {
font-size: calc(10px * var(--text-scale));
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--text-muted);
text-align: center;
padding: 1px 0;
}
.iv-mini-block-cards {
display: flex;
flex-direction: row;
gap: 3px;
flex: 1 1 auto;
min-width: 0;
}
.iv-mini-block-cards .iv-mini-card {
flex: 1 1 0;
}
/* v2026.5.40 r18 : ordre fixe des boutons en vue classique topbar-right.
Absence Douchette Actualiser Tout recharger Vider cache Thème */
html.view-classic .topbar-right #absence-btn { order: 1; }
html.view-classic .topbar-right #douchette-btn { order: 2; }
html.view-classic .topbar-right #refresh-partial-btn { order: 3; }
html.view-classic .topbar-right #refresh-btn { order: 4; }
html.view-classic .topbar-right #abort-btn { order: 4; }
html.view-classic .topbar-right #clear-cache-btn { order: 5; }
html.view-classic .topbar-right #theme-toggle { order: 6; }
/* ==========================================================================
v2026.5.40 r18 : refonte visuelle onglet Équipe plus moderne, cohérent
avec le reste du panel admin (Apparence, À propos).
========================================================================== */
.admin-team-table {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
margin-top: 14px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.admin-team-table th {
background: var(--bg-muted) !important;
border-bottom: 1px solid var(--border-strong) !important;
font-size: 11.5px !important;
letter-spacing: 0.6px !important;
padding: 10px 14px !important;
}
.admin-team-table td {
padding: 10px 14px !important;
border-bottom: 1px solid var(--border) !important;
}
.admin-team-table tr:last-child td {
border-bottom: none !important;
}
.admin-team-table tr:hover td {
background: var(--bg-hover);
}
.admin-team-table .admin-input {
background: var(--bg);
transition: border-color 0.12s, box-shadow 0.12s;
}
.admin-team-table .admin-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.admin-team-table .admin-input-id {
font-weight: 600;
color: var(--text-muted);
}
/* Checkbox + label "Exclure" / jours d'absence : style pill */
.admin-team-table label {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 8px;
background: var(--bg-muted);
border: 1px solid var(--border);
border-radius: 12px;
font-size: 12px;
cursor: pointer;
margin-right: 4px;
transition: background 0.12s, border-color 0.12s;
user-select: none;
}
.admin-team-table label:hover {
background: var(--bg-hover);
border-color: var(--border-strong);
}
.admin-team-table label input[type="checkbox"] {
margin: 0;
cursor: pointer;
}
.admin-team-table label:has(input:checked) {
background: var(--accent-soft);
border-color: var(--accent);
color: var(--accent);
font-weight: 600;
}
/* v2026.5.40 r20 : onglet Équipe colonne "Inclure" plus étroite, ID +
nom en lecture seule (texte clair, pas d'input). */
.admin-team-table th:first-child,
.admin-team-table td:first-child {
width: 60px !important;
text-align: center !important;
padding-left: 8px !important;
padding-right: 8px !important;
}
.admin-team-table th:first-child {
font-size: 10px !important;
letter-spacing: 0.4px !important;
}
.admin-team-id-readonly {
font-family: var(--mono);
font-weight: 600;
color: var(--text-muted);
font-size: 12.5px;
}
.admin-team-name-readonly {
font-weight: 600;
color: var(--text);
font-size: 13px;
}
/* v2026.5.40 r21b : seul le `/` prend la couleur faint d'absent.
Le `+` reste en couleur neutre (héritée du parent stats). */
.global-stat-sep.sep-absent {
color: var(--text-faint) !important;
font-weight: 700;
}
View File
File diff suppressed because it is too large Load Diff