Clone
7
Architecture
Quentin Rouiller edited this page 2026-05-01 18:15:26 +02:00

Architecture

Structure du projet

Planification/
├── .gitignore
├── build.sh                   # Génère dist/chromium/, dist/firefox/, .zip, .xpi, met à jour firefox-updates.json
├── firefox-updates.json       # Manifest auto-update Firefox (servi via update_url)
├── CHANGELOG.md
├── LICENSE                    # MIT
├── README.md
└── src/                       # Sources de l'extension (chargées par le navigateur)
    ├── manifest.json          # Manifest V3 (Chrome) + browser_specific_settings (Firefox)
    ├── background.js          # Service worker (~1 600 lignes)
    ├── viewer.html            # Interface principale
    ├── viewer.js              # Logique UI (~10 700 lignes)
    ├── viewer.css             # Styles + thèmes clair/sombre (~4 800 lignes)
    └── icons/                 # icon16, icon48, icon128

Composants principaux

background.js — Service worker (Manifest V3)

Worker de fond responsable de :

  • Récupération de la session EasyVista (PHPSESSID)
  • Fetch du XML calendar_block (planning du jour pour les techs configurés)
  • Fetch des fiches détaillées (HTML)
  • Fetch du timeline JSON (texte action complet)
  • Détection contexte réseau (interne / externe via SSO)
  • Détection automatique des groupes EasyVista disponibles (depuis v2026.5.40) en parsant le <select id="plan_group_id"> de la page Planning EV
  • Détection automatique des membres d'un groupe (paramétrable par groupId depuis v2026.5.40)
  • Gestion du cache local (chrome.storage.local)
  • Nettoyage des vieux caches (cron)

viewer.js — Interface principale

  • Parsing du XML calendar_block
  • Construction des cards / rows par tech
  • Timeline visuelle avec segments (depuis v2026.5.40 : barre couleur catégorie + référence + ville en vue horizontale)
  • Tooltips détaillés (épinglables, draggables)
  • 2 modes d'affichage (classique / horizontale)
  • Panel admin (Apparence / Équipe / EasyVista / Diagnostics / À propos) — section "Statuts" retirée en v2026.5.40
  • Onglet Équipe : sélecteur de groupe EV (SI-CSS, SI-EXT, …) avec rafraîchissement auto de la liste de membres au changement de groupe (v2026.5.40)
  • Onglet EasyVista : édition manuelle des deux domaines interne / externe (v2026.5.40)
  • Module LOG unifié (debug toggle)
  • Handlers globaux d'erreur
  • sendMessage avec timeout

viewer.css

  • Variables CSS (:root) pour thème clair / sombre
  • Layout grille (vue classique) + flex (vue horizontale)
  • Animations transitions douces
  • Media queries (compact 24" / breakpoint étroit)

Flux principal

┌────────────────┐    clic icône    ┌─────────────────────┐
│   Navigateur   │ ───────────────> │  background.js      │
│   (Chrome /    │                  │  (service worker)   │
│   Firefox)     │ <─── viewer.html │                     │
└────────────────┘                  └──────────┬──────────┘
        │                                      │
        │ chrome.runtime.sendMessage           │ findEasyVistaSession()
        │ {type: "fetchPlanning"}              │ ↓
        ▼                                      ▼
┌────────────────────────┐            ┌────────────────────┐
│   viewer.js  (UI)      │            │ EasyVista server   │
│  ─ init()              │            │ itsma.vd.ch /      │
│  ─ loadForDate(iso)    │ ◄── XML ── │ etat-de-vaud.ch    │
│  ─ parseXML            │   fiches   │                    │
│  ─ buildCard() x N     │            │ planning_xhr.php   │
│  ─ buildTooltipHTML    │            │ index.php (fiches) │
│  ─ renderFromData      │            │ etc.               │
│  ─ writeCache          │            └────────────────────┘
└──────────┬─────────────┘
           │
           ▼
┌────────────────────────────────────────────────┐
│  chrome.storage.local                           │
│   ─ admin_config (groupe, équipe, domaines, …) │
│   ─ planning_cache_YYYY-MM-DD (par jour)       │
│  localStorage                                   │
│   ─ view_mode (classic / horizontal)           │
└────────────────────────────────────────────────┘

Étapes clés (de init() à l'affichage) :

  1. init() → charge le thème + la config admin + les bornes de la journée + l'équipe.
  2. refreshSessionAndLoad() → cherche la session EV via background.js.
  3. loadForDate(isoDate) → essaye d'abord le cache local (readCache), puis fetche le XML calendar_block via background.fetchPlanningXml.
  4. Pour chaque intervention présente dans le XML : on en buildCard() immédiatement avec les attributs déjà enrichis (contact/lieu/catégorie). Pour les statuts détaillés, on lance des fetchFiche séquentiels en arrière-plan qui mettent à jour le DOM au fur et à mesure.
  5. À la fin du fetch complet : writeCache() sauvegarde tout dans chrome.storage.local["planning_cache_YYYY-MM-DD"].

Stack technique

Élément Détail
Manifest V3
Browser support Chrome / Edge / Firefox 140+
Pas de framework JS vanilla
Pas de build step navigateur les fichiers sont chargés tels quels (un script build.sh empaquette les sources en .zip / .xpi)
Stockage chrome.storage.local (cache + admin_config) + localStorage (préfs UI)
Réseau fetch avec credentials (cookies EV)
Logs console (préfixé + timestamp + version)
Auto-update Firefox firefox-updates.json à la racine du repo, .xpi signé Mozilla AMO

Fonctions clés (viewer.js)

Fonction Introduite Rôle
loadForDate v1.0.0 Fetch + parse planning pour une date
buildCard v1.0.0 Construit la card HTML d'un technicien
buildTooltipHTML v1.0.0 HTML détaillé d'une intervention
pinTooltip v4.1.3 Épingle un tooltip
bindTimelinePopover v4.2.3 Lie segments timeline → popovers
_softUnpinPopup v4.3.3 Désépinglage mou (popup reste visible)
initAppClock v5.0.0 Horloge HH:MM topbar
initSessionTimer v5.0.0 Compteur session EV (tick 1s)
_applyViewMode v2026.5.32 Toggle vue classique/horizontale
positionTooltipAnchored v2026.5.34 Positionnement tooltip (4 candidats)
LOG.error/warn/info v2026.5.38 Logger unifié
_applyTextZoom v2026.5.39 Zoom global du texte (slider Apparence)
renderAdminSectionTeam v5.0.0 Onglet admin Équipe (sélecteur de groupe EV depuis v2026.5.40, tri inclus + alpha)
renderAdminSectionEV v5.0.0 Onglet admin EasyVista (édition domaines depuis v2026.5.40)
paintGroupOptions / refreshTeamForGroup v2026.5.40 Helpers du sélecteur de groupe (peuplement + refresh auto de la liste équipe)
_normalizeOrigin v2026.5.40 Normalisation des URLs des domaines EV éditées dans l'admin

Fonctions clés (background.js)

Fonction Rôle
findEasyVistaSession Trouve l'onglet EV ouvert + extrait PHPSESSID
fetchPlanningXml Fetch XML calendar_block du planning
fetchFicheHtml Fetch HTML d'une fiche EV (avec retry SSO)
fetchTimelineApi Fetch JSON timeline d'une fiche (texte action complet)
fetchCurrentUser Identifie l'user EV connecté
detectNetworkContext Interne (etat-de-vaud.ch) ou externe (vd.ch)
detectGroupsFromEV (v2026.5.40) Parse le <select id="plan_group_id"> de la page Planning → liste [{id, name}, …] des groupes disponibles
detectTeamFromEV (paramétrable depuis v2026.5.40) Liste les membres d'un groupe ; accepte un groupId en argument pour basculer entre SI-CSS / SI-EXT
evFetch Wrapper fetch avec headers EV (Referer, X-Requested-With)

Helpers de lecture de config (v2026.5.41)

Côté background.js, tous les hardcodes runtime ont été remplacés par des helpers qui lisent admin_config à chaque appel :

Helper Retourne Fallback
getAdminConfig() objet cfg complet {}
getEvOrigins() tableau d'origines à surveiller DEFAULT_EV_ORIGINS (2 défauts hardcodés)
getGroupId() string group_id DEFAULT_GROUP_ID = "191" (SI-CSS)
getSupportIds() CSV des clés de cfg.team "" (lève no_team_configured côté fetchPlanningXml)
getDayBounds() { start, end } heures { start: 8, end: 18 }

fetchPlanningXml() lève Error("no_team_configured") quand getSupportIds() retourne une chaîne vide → le viewer affiche un message invitant à configurer Paramètres → Équipe.

Configuration persistée

chrome.storage.local["admin_config"] :

Clé Type Rôle Introduite
team {id: name} Liste des techniciens inclus v5.0.0
recurringAbsences {id: [days]} Jours de la semaine d'absence récurrente v5.0.0
groupId string ID du groupe EV sélectionné v5.0.0
groupName string Nom du groupe sélectionné (cache d'affichage) v2026.5.40
detectedGroups [{id, name}] Cache de la dernière détection des groupes v2026.5.40
evOrigins [interne, externe] Domaines EasyVista éditables dans l'admin v2026.5.40
theme auto/light/dark Préférence thème v2026.5.39
textZoom -2..+2 Niveau de zoom du texte v2026.5.39
cacheDays number Durée de rétention du cache (défaut 7) v2026.5.39
dayStart / dayEnd number Plage horaire de la timeline (défaut 8-18) v2026.5.39
closedStatus / resolvedStatus / cancelledStatus / suspendedStatus [string] Statuts EV reconnus pour les verdicts (matching insensible casse/accents/conjugaisons) v2026.5.44
topbarColor string #rrggbb Couleur de fond de la topbar (vide = défaut thème) v2026.5.44
appFont string Identifiant de police (cf. APP_FONT_OPTIONS) appliquée à toute la page v2026.5.44

localStorage["view_mode"] : classic ou horizontal (depuis v2026.5.32).


Variables CSS dynamiques (v2026.5.44)

Posées sur :root ou html par JS, lues partout dans le CSS :

Variable Source Rôle
--topbar-bg _applyTopbarColor(color) depuis cfg.topbarColor Fond de la topbar (et de la sidebar en horizontal). Vide = --bg-elevated du thème
--topbar-text _applyTopbarColor calcule via _readableTextOn(color) (luminance pondérée 0.299·r + 0.587·g + 0.114·b) Couleur du texte topbar — blanc si fond foncé, foncé si fond clair
--app-font _applyAppFont(val) depuis cfg.appFont (mappe sur _appFontStack) Stack font-family appliquée à body (cascade vers toute la page)
--topbar-height _initTopbarHeightVar() via ResizeObserver sur header.topbar Hauteur réelle de la topbar — utilisée par les éléments sticky en dessous (.session-banner, .progress-bar, .session-slide-alert) pour leur top:
--text-scale / --zoom-factor _applyTextZoom(level) depuis cfg.textZoom Multiplicateurs du zoom texte (existants depuis v2026.5.39, listés ici pour complétude)

Classe html.has-topbar-color (toggle JS dans _applyTopbarColor) active les règles color-mix translucides sur les boutons / today-block / date-custom de la topbar et de la sidebar horizontale, pour qu'ils restent lisibles sur n'importe quel fond.


Helpers d'apparence (v2026.5.44)

Fonction (viewer.js) Rôle
APP_FONT_OPTIONS Tableau des 28 stacks de police proposés dans le panel admin
_appFontStack(val) Retourne le stack font-family correspondant à un identifiant
_hexToLuminance(hex) Calcule la luminance perçue (0..1) d'une couleur hex (#rgb ou #rrggbb)
_readableTextOn(hex) Retourne #1a1f2b ou #ffffff selon contraste optimal
_applyTopbarColor(color) Pose --topbar-bg, --topbar-text et toggle html.has-topbar-color
_applyAppFont(val) Pose --app-font sur :root
_initTopbarStyleFromConfig() Lit cfg.topbarColor + cfg.appFont (avec fallback cfg.topbarFont) au boot et applique
_initTopbarHeightVar() Démarre le ResizeObserver qui maintient --topbar-height à jour

Helpers de verdict ghost (v2026.5.44 — refonte)

Dans analyzeOneDisappearedIv() :

Verdict (iv._disappearStatus) Décision Rendu UI
terminated-clos KEEP Carte verte + ✓✓
terminated-pending KEEP ✓ gris « Fait »
terminated-suspended KEEP ✓ jaune « Suspendu » + barre verte (horizontal)
closed / terminated KEEP Carte verte + ✓✓
cancelled / cancelled-reservation / cancelled-absence REMOVE (retiré silencieusement en PROD ; loggé en mode diagnostic)
error KEEP (silent) Inchangé
fiche_tronquee KEEP (silent) Inchangé

Helpers de matching (_normalizeStatus, _stemStatus, _statusMatchesAny) : permettent à l'utilisateur de saisir ses statuts EV en français normal (« Clôturé », « clôture », « Clos », « clôs », « Suspendu : Attente info »…) avec matching tolérant aux variations casse/accents/conjugaisons via lowercase + suppression diacritiques + strip suffixes verbaux courants.


Refresh séquentiel + abort (v2026.5.44)

background.js :

const _evFetchControllers = new Set();        // registre global des AbortController en vol
function _abortAllEvFetches() {                // appelé sur message "abortAllFetches"
  for (const c of _evFetchControllers) {
    try { c.abort(); } catch (e) { /* ignore */ }
  }
  _evFetchControllers.clear();
}

async function evFetch(url, origin, opts = {}) {
  let controller = null;
  if (!opts.signal) {
    controller = new AbortController();
    _evFetchControllers.add(controller);
  }
  // ... fetch avec signal
  try { return await fetch(url, fetchOpts); }
  finally { if (controller) _evFetchControllers.delete(controller); }
}

viewer.js envoie {type: "abortAllFetches"} au service worker quand l'utilisateur clique « ✕ Arrêter ». Tous les fetch EV en vol sont avortés instantanément (avant la complétion). Le state state.refreshAborted interrompt aussi la boucle séquentielle au prochain await.