Compare commits

..

4 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
8 changed files with 1481 additions and 170 deletions
+1
View File
@@ -17,6 +17,7 @@ desktop.ini
*.old
# Build artifacts (les ZIP/XPI livrés ne sont pas dans le repo, ils sont buildés à la demande)
dist/
*.zip
*.xpi
*.crx
+82 -3
View File
@@ -1,7 +1,7 @@
# CHANGELOG — Extension Planification EasyVista Canton de Vaud
> Ce changelog documente l'évolution de l'extension Chrome/Firefox "Planification"
> développée par Quentin Rouiller pour les techniciens IT du Canton de Vaud.
> développée par Quentin Rouiller pour les techniciens DGNSI (Canton de Vaud).
>
> Les versions documentées ci-dessous sont celles dont les détails sont connus.
> Pour les versions plus anciennes, Claude Code se basera sur l'analyse du code
@@ -9,9 +9,88 @@
---
## v2026.5.37Refonte vue horizontale (sidebar complète)
## v2026.5.39Séparation Matin / Après-midi + Apparence (thème, zoom, cache)
**Branche** : current
### Séparation matin / après-midi
- Séparateur visuel "MATIN" / "APRÈS-MIDI" entre les interventions
dans la vue classique : pill grise neutre, ligne 3px épaisse.
- Affiché aussi entre les absences partielles (demi-journée).
- Si une période est vide, son séparateur n'est pas affiché.
- Caché en vue horizontale (les rows sont masquées de toute façon).
### Timeline — coupure midi très visible
- Bande verticale composée d'un trait massif central (couleur --text)
+ stripes diagonales en arrière-plan (effet "césure"). 6 px de large
(7 px en vue horizontale). Visible immédiatement, pas de label superflu.
### Vue horizontale (sidebar)
- Boutons (Absence, Douchette, Actualiser, Tout recharger, Vider cache,
Thème) maintenant **vraiment** poussés en bas via `min-height: 100vh`
sur la sidebar.
- Bouton "Aujourd'hui" : style cohérent avec les flèches ◀ ▶ (même
padding, font-size, hauteur), texte centré, libellé complet
"Aujourd'hui" (au lieu de "Auj.").
- Espace visuel entre `Actualisé à HH:MM` et le bouton Absence (fine
bordure top + padding).
### Vue classique (topbar)
- Ordre verrouillé via CSS `order` : badge user → titre → date-nav →
capture-info → refresh-check. Évite les déplacements au retour de
vue horizontale.
### Section Apparence (admin) — refondue + en première position
- **Thème** : sélecteur Auto / Clair / Sombre (s'enregistre direct).
- **Durée du cache (jours)** : configurable, défaut 7 jours, range 1-365.
Lue par viewer.js (purge auto) ET background.js (au boot).
- **Taille du texte** : 5 niveaux (-20%, -10%, 100%, +10%, +20%) via CSS
`zoom` sur body. Persisté dans admin_config.textZoom et appliqué dès
le boot.
- Section "Apparence" est maintenant **la première** dans le panel admin.
## v2026.5.38 — Attribution auteur + nettoyage code
**Branche** : current
### Attribution auteur
- Ajout en-têtes copyright dans tous les fichiers source
(viewer.js, viewer.html, viewer.css, background.js)
- Ajout `@author Quentin Rouiller` sur les fonctions principales
(loadForDate, buildCard, buildTooltipHTML, pinTooltip, _softUnpinPopup,
positionTooltipAnchored, _applyViewMode, _moveElementsToSidebar,
_restoreElementsToTopbar, fetchAndShowCurrentUser, _maybeRetryFetchUser,
initAppClock, initAppFooter, bindTimelinePopover,
openPersistentTimelinePopup, showTooltip, _findFreePopupPosition,
_clampPopupInSafeArea, findEasyVistaSession, fetchPlanningXml,
fetchCurrentUser, detectNetworkContext)
- Ajout signature "Développé par Quentin Rouiller" en bas du popup
user-badge (style cohérent avec footer version : 11px, italique,
gris atténué, séparateur fin)
- Mise à jour `description` du manifest pour mentionner DGNSI
### Nettoyage et optimisation
- Retrait fonction vide `initAdminMenu()` (inutile depuis v2026.5.25,
l'admin passe par le bouton ⚙ Paramètres du popup user-badge)
- Retrait classe CSS orpheline `.date-picker-day` (déjà remplacée par
`.date-custom` en v2026.5.17)
- Retrait anciens styles CSS `.intervention` (layout v1, jamais générés
depuis le passage à `.intervention-v2`)
- Retrait commentaire orphelin `.intervention-v2.is-ghost` (classe
retirée en v4.3.3)
- Retrait 14× `console.log("[viewMode]")` debug verbose (gardé
uniquement les `console.warn` utiles pour erreurs)
- Retrait 5× `console.log("[bg]")` debug verbose dans
fetchPlanningXml / fetchFicheHtml / fetchSessionTimeRemaining /
extendSessionKeepAlive (gardé warnings + logs critiques)
- Remplacement `extendBtn.onclick` par `addEventListener("click", ...)`
pour plus de cohérence
### Builds
- `dist/chromium/` et `dist/firefox/` prêts à charger en mode dev
- `planification-v2026.5.38-chromium.zip` (~144 Ko)
- `planification-v2026.5.38-firefox.xpi` (~144 Ko, à signer sur AMO)
## v2026.5.37 — Refonte vue horizontale (sidebar complète)
- Topbar en haut supprimée en vue horizontale
- User-badge + titre déplacés tout en haut de la sidebar
- Bouton "Aujourd'hui" pleine largeur avec icône ↺
@@ -175,5 +254,5 @@
## Auteur
**Quentin Rouiller** (QRO)
Canton de Vaud — Service IT
Technicien DGNSI — Canton de Vaud
Email pour commits Git : `quentin.rouiller@ikmail.com`
+19 -9
View File
@@ -1,11 +1,11 @@
# Planification — Extension EasyVista Canton de Vaud
Extension Chrome / Firefox pour visualiser de manière claire et rapide le planning des techniciens IT du Canton de Vaud dans EasyVista.
Extension Chrome / Firefox pour visualiser de manière claire et rapide le planning des techniciens DGNSI (Canton de Vaud) dans EasyVista.
## Aperçu rapide
- **Auteur** : Quentin Rouiller (QRO)
- **Cible** : techniciens IT 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
- **Version actuelle** : `v2026.5.37`
- **Manifest** : V3 (Chrome/Edge/Firefox)
@@ -60,14 +60,24 @@ L'extension a connu **3 systèmes de versionning successifs** :
|---|---|---|
| 16-17 avril 2026 | Versions de base | `1.0.0`, `2.0.0`, `3.0.0` |
| 18-20 avril 2026 | SemVer classique | `4.1.3`, `4.2.8`, `5.0.12` |
| 21 avril 2026 → maintenant | **Année + mois + patch** | `2026.5.16``2026.5.37` |
| 21 avril 2026 → maintenant | **`ANNÉE.MAJEURE.PATCH`** | `2026.5.16``2026.5.37` |
### Pourquoi le passage à `YYYY.M.PATCH` ?
### Format actuel : `ANNÉE.MAJEURE.PATCH`
À partir de la **v2026.5.16** (21 avril 2026), l'extension est passée au versionning par année :
- Plus lisible pour les utilisateurs (l'année indique immédiatement la fraîcheur)
- Plus de débat sur ce qui constitue un "majeur" vs "mineur"
- Bump du `PATCH` à chaque livraison
À partir de la **v2026.5.16** (21 avril 2026), l'extension utilise le schéma suivant :
| Position | Sens | Quand ça change |
|---|---|---|
| `2026` | **Année** | À chaque nouvelle année calendaire |
| `5` | **Majeure** | À chaque **gros changement / ajout important** (refonte, nouvelle feature majeure, bump volontaire) |
| `37` | **Patch** | À **chaque livraison** dans la majeure courante (corrections, ajustements, petites features) |
Exemples :
- `2026.5.16``2026.5.17` : petite correction ou ajustement (patch)
- `2026.5.37``2026.6.0` : refonte majeure (par exemple nouvelle vue, nouvelle architecture)
- `2026.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).
⚠️ **Important** : `v2026.5.16` succède chronologiquement à `v5.0.12`, malgré le numéro qui semble plus petit. Le préfixe `2026` indique l'année.
@@ -179,4 +189,4 @@ git push --tags
## Auteur
**Quentin Rouiller** (QRO)
Canton de Vaud — Service IT
Technicien DGNSI — Canton de Vaud
+152 -19
View File
@@ -1,3 +1,16 @@
/**
* Planification — Extension navigateur EasyVista (Canton de Vaud / DGNSI)
*
* Service worker (Manifest V3) : récupération session EV, fetch planning XML,
* fetch fiches détaillées, gestion cache.
*
* Copyright (c) 2026 Quentin Rouiller
* Licensed under the MIT License — see LICENSE file in the project root.
*
* @author Quentin Rouiller
* @repository https://gitea.netaplaid.ch/FroSteel/Planification
*/
// background.js — Service worker (Manifest V3) — v4
//
// Rôles :
@@ -14,6 +27,94 @@
// directement ref/contact/lieu/catégorie dans ses attributs attr1/attr2/attr3,
// donc on n'a plus besoin ni de xhr2 en masse, ni de l'API timeline.
// ============================================================================
// v2026.5.38 : Observabilité — logger unifié + handlers globaux SW
// ============================================================================
// Le service worker MV3 est endormi/relancé fréquemment, donc important d'avoir
// un format de log compact et reproductible. Les handlers `error` et
// `unhandledrejection` sur `self` capturent ce qui passe à travers les
// try/catch (ex: une promise oubliée dans un setTimeout).
// ============================================================================
// Clef chrome.storage.local pour le mode debug — toggle depuis le panel admin
// du viewer. Le viewer envoie un message {type:"setDebugLogs"} qu'on traite
// plus bas pour sync, et on lit aussi au boot.
const DEBUG_LOGS_KEY = "debug_logs";
const LOG = (() => {
let _version = "?";
try {
if (chrome && chrome.runtime && chrome.runtime.getManifest) {
_version = chrome.runtime.getManifest().version || "?";
}
} catch (e) {}
let _debug = false;
// Lecture initiale (asynchrone, mais on s'en fout : on commence muet
// et qd la valeur arrive on update). Le SW peut être tué/relancé donc
// on relit à chaque démarrage.
try {
chrome.storage.local.get([DEBUG_LOGS_KEY]).then(obj => {
_debug = !!obj[DEBUG_LOGS_KEY];
}).catch(() => {});
} catch (e) {}
const _stamp = () => new Date().toISOString().substring(11, 23);
const _format = (level, prefix, msg, ctx) => {
const head = `[${_stamp()}][v${_version}][bg/${prefix}][${level}]`;
if (ctx !== undefined) return [head, msg, ctx];
return [head, msg];
};
return {
info: (prefix, msg, ctx) => {
if (!_debug) return;
console.log(..._format("INFO", prefix, msg, ctx));
},
warn: (prefix, msg, ctx) => console.warn (..._format("WARN", prefix, msg, ctx)),
error:(prefix, msg, ctx) => console.error(..._format("ERROR", prefix, msg, ctx)),
exception: (prefix, msg, err, extra) => {
const ctx = {
name: err && err.name,
message: err && err.message,
stack: err && err.stack,
extra: extra
};
console.error(..._format("ERROR", prefix, msg, ctx));
},
setDebug: (on) => {
_debug = !!on;
try { chrome.storage.local.set({ [DEBUG_LOGS_KEY]: _debug }); } catch (e) {}
console.log(..._format("INFO", "logger", `mode debug = ${_debug ? "ON" : "OFF"}`));
},
isDebug: () => _debug,
version: () => _version
};
})();
// Si le viewer toggle pendant qu'on est endormi/relancé, on capte le
// changement chrome.storage et on update le flag local.
chrome.storage.onChanged.addListener((changes, area) => {
if (area === "local" && changes[DEBUG_LOGS_KEY]) {
const newVal = !!changes[DEBUG_LOGS_KEY].newValue;
LOG.setDebug(newVal);
}
});
self.addEventListener("error", (event) => {
LOG.exception("global", "uncaught error in service worker",
event.error || new Error(event.message || "unknown"),
{ filename: event.filename, lineno: event.lineno, colno: event.colno });
});
self.addEventListener("unhandledrejection", (event) => {
const reason = event.reason;
LOG.exception("global", "unhandled promise rejection in service worker",
reason instanceof Error ? reason : new Error(String(reason)));
});
LOG.info("boot", "service worker démarré", { version: LOG.version() });
// Domaines EasyVista reconnus (interne d'abord, externe en fallback)
const EV_ORIGINS = [
"https://itsma.etat-de-vaud.ch",
@@ -40,6 +141,11 @@ chrome.action.onClicked.addListener(async () => {
// Trouver l'onglet EasyVista actif et en extraire le PHPSESSID
// ============================================================================
/**
* Trouve l'onglet EasyVista ouvert et récupère phpsessid + origin.
*
* @author Quentin Rouiller
*/
async function findEasyVistaSession() {
// Chercher tous les onglets sur un domaine EasyVista
for (const origin of EV_ORIGINS) {
@@ -65,6 +171,8 @@ async function findEasyVistaSession() {
*
* Ce n'est PAS le HTML de la page Planning — le serveur ne rend pas les données
* dans le HTML, elles arrivent via cet endpoint AJAX.
*
* @author Quentin Rouiller
*/
async function fetchPlanningXml(origin, phpsessid, unixDate) {
const techIds = "76272,83725,66635,92235,90070,40944,72485,86874";
@@ -84,9 +192,9 @@ async function fetchPlanningXml(origin, phpsessid, unixDate) {
`&mail_title=mail` +
`&day_start_hour=8` +
`&day_end_hour=19`;
console.log("[bg] fetchPlanningXml →", url.substring(0, 140));
// 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.
const r = await evFetch(url, origin);
console.log("[bg] status =", r.status);
if (!r.ok) {
// v4.2 : classifier l'erreur HTTP pour que le viewer affiche le bon
// écran (session expirée vs EV inaccessible).
@@ -95,9 +203,7 @@ async function fetchPlanningXml(origin, phpsessid, unixDate) {
err.status = r.status;
throw err;
}
const xml = await r.text();
console.log("[bg] taille XML =", xml.length);
return xml;
return await r.text();
}
/**
@@ -156,7 +262,6 @@ async function fetchXhr2(origin, phpsessid, actionId) {
async function fetchFicheHtml(origin, phpsessid, formLink) {
const url = `${origin}/index.php?${formLink}&PHPSESSID=${encodeURIComponent(phpsessid)}`;
console.log("[bg] fetchFicheHtml →", url.substring(0, 120));
// v2026.5.16 : juste après une reconnexion SSO, EasyVista retourne parfois
// une page intermédiaire tronquée (~8 Ko au lieu de ~250 Ko), le temps que
@@ -175,7 +280,11 @@ async function fetchFicheHtml(origin, phpsessid, formLink) {
throw err;
}
const html = await r.text();
console.log(`[bg] fiche status = ${r.status} | taille = ${html.length}${attempt > 1 ? ` (tentative ${attempt}/${MAX_RETRIES})` : ""}`);
// v2026.5.38 : on log seulement les retries (utile en cas de pb SSO),
// pas chaque tentative normale qui réussit du premier coup.
if (attempt > 1) {
console.log(`[bg] fiche tentative ${attempt}/${MAX_RETRIES} (taille = ${html.length})`);
}
// Si réponse clairement une redirection courte → login expiré, inutile de retry
if (html.length < 500) {
@@ -273,7 +382,7 @@ async function fetchSessionTimeRemaining(origin, phpsessid) {
const url = `${origin}/timeout_ajax.php`
+ `?PHPSESSID=${encodeURIComponent(phpsessid)}`
+ `&__AJAX_TIMEOUT_FCT__=session_time`;
console.log("[bg] fetchSessionTimeRemaining →", url.substring(0, 120));
// v2026.5.38 : log retiré (appelé toutes les minutes, polluait la console).
const r = await evFetch(url, origin);
if (!r.ok) {
throw new Error("HTTP " + r.status);
@@ -288,9 +397,7 @@ async function fetchSessionTimeRemaining(origin, phpsessid) {
}
throw new Error("invalid_response");
}
const ms = parseInt(body, 10);
console.log(`[bg] session_time = ${ms} ms = ${Math.round(ms/60000)} min`);
return ms;
return parseInt(body, 10);
}
/**
@@ -302,7 +409,7 @@ async function extendSessionKeepAlive(origin, phpsessid) {
const url = `${origin}/timeout_ajax.php`
+ `?PHPSESSID=${encodeURIComponent(phpsessid)}`
+ `&__AJAX_TIMEOUT_FCT__=keep_connection`;
console.log("[bg] extendSessionKeepAlive →", url.substring(0, 120));
// v2026.5.38 : log retiré (déclenché par bouton "prolonger", l'UI affiche déjà un toast).
const r = await evFetch(url, origin);
if (!r.ok) {
throw new Error("HTTP " + r.status);
@@ -312,9 +419,7 @@ async function extendSessionKeepAlive(origin, phpsessid) {
if (looksLikeLoginPage(body)) throw new Error("session_expired");
throw new Error("invalid_response");
}
const ms = parseInt(body, 10);
console.log(`[bg] keep_connection → session prolongée à ${ms} ms`);
return ms;
return parseInt(body, 10);
}
// ============================================================================
@@ -337,6 +442,8 @@ async function extendSessionKeepAlive(origin, phpsessid) {
*
* @param {boolean} force - si true, ignore le cache et refait le test
* @returns {Promise<"internal"|"external">}
*
* @author Quentin Rouiller
*/
async function detectNetworkContext(force = false) {
const CACHE_KEY = "network_context_v2"; // v5.0.12 : nouvelle clé pour invalider le cache fautif v5.0.11
@@ -475,6 +582,8 @@ function watchReconnectTabForIamLogin(tabId) {
* - champ "Bienvenue Nom Prénom"
* Retourne { name: "Nom Prénom" | null, login: "..." | null } ou null si
* tout a échoué.
*
* @author Quentin Rouiller
*/
async function fetchCurrentUser(origin, phpsessid) {
const url = `${origin}/index.php?PHPSESSID=${encodeURIComponent(phpsessid)}`;
@@ -1213,6 +1322,13 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
return;
}
// v2026.5.38 : toggle debug logs (depuis le panel admin du viewer)
if (msg.type === "setDebugLogs") {
LOG.setDebug(!!msg.on);
sendResponse({ ok: true, debug: LOG.isDebug() });
return;
}
sendResponse({ ok: false, error: "unknown_message" });
} catch (err) {
console.error("background error:", err);
@@ -1264,13 +1380,30 @@ async function cleanupOldCaches(daysToKeep) {
return toRemove.length;
}
// v2026.5.39 : on lit admin_config pour récupérer cacheDays. Si pas dispo,
// fallback sur 7 jours.
async function _getCacheDays() {
try {
const o = await chrome.storage.local.get("admin_config");
const cfg = o && o.admin_config;
if (cfg && typeof cfg.cacheDays === "number" && cfg.cacheDays > 0) {
return cfg.cacheDays;
}
} catch (e) {
LOG.warn("cache", "lecture admin_config échouée, fallback 7 jours", { err: e && e.message });
}
return 7;
}
// Au démarrage, nettoyer les anciennes alarmes et les anciens caches
chrome.runtime.onInstalled.addListener(() => {
chrome.runtime.onInstalled.addListener(async () => {
clearLegacyRefreshAlarms();
cleanupOldCaches(7).catch(err => console.warn("cleanup:", err));
const days = await _getCacheDays();
cleanupOldCaches(days).catch(err => LOG.warn("cleanup", "échec onInstalled", { err: err && err.message }));
});
chrome.runtime.onStartup.addListener(() => {
chrome.runtime.onStartup.addListener(async () => {
clearLegacyRefreshAlarms();
cleanupOldCaches(7).catch(err => console.warn("cleanup:", err));
const days = await _getCacheDays();
cleanupOldCaches(days).catch(err => LOG.warn("cleanup", "échec onStartup", { err: err && err.message }));
});
+2 -2
View File
@@ -1,8 +1,8 @@
{
"manifest_version": 3,
"name": "Planification",
"version": "2026.5.37",
"description": "Vue claire et rapide du planning des techniciens EasyVista. Regroupe interventions et réservations par tech, affiche horaires, contact, lieu, catégorie et statut en un coup d'œil.",
"version": "2026.5.39",
"description": "Vue claire et rapide du planning des techniciens EasyVista. Développé par Quentin Rouiller — DGNSI, Canton de Vaud.",
"permissions": [
"activeTab",
"scripting",
+399 -56
View File
@@ -1,3 +1,13 @@
/**
* Planification — Extension navigateur EasyVista (Canton de Vaud / DGNSI)
*
* Copyright (c) 2026 Quentin Rouiller
* Licensed under the MIT License — see LICENSE file in the project root.
*
* @author Quentin Rouiller
* @repository https://gitea.netaplaid.ch/FroSteel/Planification
*/
/* ==========================================================================
Thème clair (défaut)
========================================================================== */
@@ -134,6 +144,17 @@ html, body {
min-width: 0;
}
/* v2026.5.39 r2 : on verrouille l'ordre des élements de la topbar gauche en
vue classique. Le badge user reste à l'extrême gauche, le titre suit, puis
la nav date et enfin la capture-info. Ce verrou évite que des composants
restaurés depuis la sidebar (vue horizontale → classique) finissent dans
le mauvais ordre. */
html.view-classic .topbar-left #user-badge { order: 1; }
html.view-classic .topbar-left #app-title { order: 2; }
html.view-classic .topbar-left .date-nav { order: 3; }
html.view-classic .topbar-left .capture-info { order: 4; }
html.view-classic .topbar-left #refresh-check { order: 5; }
.topbar h1 {
margin: 0;
font-size: 18px;
@@ -368,8 +389,8 @@ html, body {
pointer-events: none;
}
/* v2026.5.17 : masquer l'ancien date-picker-day s'il traîne (compat) */
.date-picker-day { display: none; }
/* v2026.5.38 : ancienne classe .date-picker-day retirée (orpheline depuis le
nouveau picker .date-custom de la v2026.5.17). */
.btn-nav {
padding: 6px 10px;
@@ -751,14 +772,40 @@ html, body {
z-index: 2;
}
/* v2026.5.39 r3 : séparateur "midi" — vraie coupure visuelle.
La timeline-bar a overflow:hidden donc les chevrons qui dépassaient en
haut/bas étaient coupés. On reste DANS la bar mais on rend le trait
massif et fortement contrasté + un effet "encoche" via box-shadow latéral
(pour bien détacher du fond), et on superpose une bande à stripes
diagonales sur 6px de large pour signaler la pause de midi.
Z-index élevé pour passer au-dessus des segments. */
.timeline-noon {
position: absolute;
top: -2px;
bottom: -2px;
width: 1px;
background: var(--border-strong);
z-index: 1;
top: 0;
bottom: 0;
width: 6px;
z-index: 3;
pointer-events: none;
transform: translateX(-3px); /* centrer la bande sur 12h */
background:
/* trait central massif : */
linear-gradient(to right,
transparent 0%, transparent 30%,
var(--text) 30%, var(--text) 70%,
transparent 70%, transparent 100%),
/* fond stripes diagonales pour signaler "césure" : */
repeating-linear-gradient(
45deg,
var(--bg-muted) 0 3px,
var(--text-muted) 3px 4px
);
opacity: 0.95;
}
/* En vue horizontale, timeline-bar est plus haute (22px au lieu de 20px),
on garde le même trait mais légèrement plus large pour la visibilité. */
html.view-horizontal .timeline-noon {
width: 7px;
transform: translateX(-3.5px);
}
.timeline-scale {
@@ -838,10 +885,6 @@ html, body {
opacity: 0.7;
}
.intervention.highlight {
background: var(--bg-hover);
}
/* v2026.5.29 : highlight visible sur les rows .intervention-v2 quand on
survole le segment timeline correspondant (ou que l'user survole la row) */
.intervention-v2.highlight {
@@ -966,10 +1009,6 @@ html, body {
to { transform: rotate(360deg); }
}
/* .intervention-v2.is-ghost : retirée en v4.3.3 — on ne barre plus les
cartes. La gestion des tickets disparus se fait via _disappearStatus
(vert ✓/✓✓) ou _disappearRemove (retrait total). */
/* Ligne 1 : REF en titre centré gros gras */
.iv-ref-header {
grid-area: ref;
@@ -1212,28 +1251,8 @@ html, body {
font-style: italic;
}
/* ──────────────────────────────────────────────────────────────────────────
Anciens styles .intervention (v1) — gardés pour ne pas casser le reste
────────────────────────────────────────────────────────────────────────── */
.intervention {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px 8px 10px;
border-top: 1px solid var(--border);
cursor: default;
transition: background 0.08s;
position: relative;
}
.intervention:first-child { border-top: none; }
.intervention:hover { background: var(--bg-hover); }
.intervention-dot {
flex-shrink: 0;
width: 4px;
align-self: stretch;
margin: 2px 4px 2px 0;
border-radius: 2px;
}
/* v2026.5.38 : anciens styles .intervention (layout v1) supprimés — le HTML
ne génère plus que .intervention-v2 depuis longtemps. */
/* ==========================================================================
Tooltip
@@ -2554,7 +2573,6 @@ header.topbar::before {
.app-clock-date { display: none; }
.topbar-left { flex-wrap: wrap; }
.date-nav { margin-top: 4px; }
.date-picker-day { min-width: 46px; font-size: 12px; }
.topbar-right { flex-wrap: wrap; justify-content: flex-end; }
}
@@ -3868,9 +3886,10 @@ html.view-horizontal body > header.topbar {
}
/* 2. Sidebar : structure verticale avec section fixe en haut (user+titre+date)
et section "boutons" en bas poussée via margin-top: auto. */
et section "boutons" en bas poussée via margin-top: auto.
v2026.5.39 r8 : max-height retiré (était en conflit avec min-height
compensé par --zoom-inv quand le user dézoom le texte). */
html.view-horizontal .horizontal-sidebar {
max-height: 100vh !important;
padding-top: 12px !important;
}
@@ -3898,23 +3917,14 @@ html.view-horizontal .horizontal-sidebar #app-title {
html.view-horizontal .horizontal-sidebar .date-nav {
display: contents;
}
/* Le bouton Aujourd'hui devient prominent */
/* Bouton Aujourd'hui — v2026.5.39 r5 : MÊME style que Absence/Douchette
en sidebar. Hérite de la règle générique button.btn (padding/font-size).
On force juste centrage du texte. */
html.view-horizontal .horizontal-sidebar .btn-today {
order: 1; /* tout en haut après titre */
order: 1; /* tout en haut après titre */
width: 100% !important;
justify-content: center !important; /* centrage du label */
text-align: center !important;
padding: 8px 12px !important;
font-size: 13px !important;
font-weight: 600 !important;
background: var(--bg) !important;
border: 1px solid var(--border) !important;
border-radius: 6px !important;
color: var(--text) !important;
justify-content: center !important;
}
html.view-horizontal .horizontal-sidebar .btn-today::before {
content: "↺ ";
margin-right: 4px;
}
/* 5. App-clock (date + heure) centré sous le bouton "Aujourd'hui" */
@@ -3976,10 +3986,24 @@ html.view-horizontal .horizontal-sidebar .capture-info {
}
/* 10. Boutons poussés en bas via margin-top: auto sur le premier d'entre eux
(Absence, qui a order:7) */
(Absence, qui a order:7).
v2026.5.39 r2 : ajout d'une fine bordure top + padding pour visualiser
clairement la séparation entre la zone "info" (capture-info, stats) et
la zone "actions" (boutons). margin-top: auto pousse en bas. */
html.view-horizontal .horizontal-sidebar #absence-btn {
order: 7;
margin-top: auto !important; /* pousse tout ce qui suit en bas */
padding-top: 8px !important; /* respiration au-dessus du bouton */
position: relative;
}
html.view-horizontal .horizontal-sidebar #absence-btn::before {
content: "";
position: absolute;
top: 0;
left: 4px;
right: 4px;
height: 1px;
background: var(--border);
}
html.view-horizontal .horizontal-sidebar #douchette-btn { order: 8; }
html.view-horizontal .horizontal-sidebar #refresh-partial-btn { order: 9; }
@@ -4001,11 +4025,39 @@ html.view-horizontal .horizontal-sidebar #theme-toggle {
justify-content: center !important;
}
/* 12. Sidebar doit être flex column pour que margin-top:auto fonctionne */
/* 12. Sidebar doit être flex column pour que margin-top:auto fonctionne.
v2026.5.39 r3 : on force min-height: 100vh.
v2026.5.39 r5 : tout est centré horizontalement (text-align + align-items).
v2026.5.39 r8 : on compense le body.zoom via --zoom-inv. À 75% de zoom,
100vh ne couvre que 75% de la viewport effective ; on multiplie donc par
l'inverse du zoom pour que la sidebar atteigne TOUJOURS le bas de l'écran. */
html.view-horizontal .horizontal-sidebar {
display: flex !important;
flex-direction: column !important;
align-items: center !important; /* centrage horizontal des enfants */
text-align: center !important; /* centrage des textes */
gap: 6px !important;
min-height: calc(100vh * var(--zoom-inv, 1)) !important;
box-sizing: border-box !important;
}
/* Tous les enfants directs reçoivent text-align: center par héritage,
mais on force aussi sur les boutons (qui ont parfois justify-content:
flex-start) et les blocs de stats. */
html.view-horizontal .horizontal-sidebar > * {
text-align: center !important;
}
html.view-horizontal .horizontal-sidebar button.btn {
justify-content: center !important;
}
html.view-horizontal .horizontal-sidebar #stats .global-stat {
text-align: center !important;
}
html.view-horizontal .horizontal-sidebar .app-clock {
align-items: center !important;
text-align: center !important;
}
html.view-horizontal .horizontal-sidebar .date-custom {
justify-content: center !important;
}
/* 13. Barre de rafraîchissement en vue horizontale : overlay par-dessus
@@ -4032,3 +4084,294 @@ html.view-horizontal #progress-bar {
html.view-horizontal .card-status-note.pompier {
display: none !important;
}
/* ==========================================================================
v2026.5.38 : Signature auteur en bas du popup user-badge
Style cohérent avec le footer "QRO/vX.Y.Z" en bas à droite : petit, gris
atténué, séparé par une fine ligne supérieure.
========================================================================== */
.user-name-popup-author {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border);
font-size: 11px;
color: var(--text-faint);
text-align: center;
font-style: italic;
letter-spacing: 0.2px;
user-select: none;
}
/* ==========================================================================
v2026.5.39 : Séparateur Matin / Après-midi entre les interventions
Affiché entre les rows .intervention-v2 dans la vue classique. La ligne
est marquée (pas un simple text), avec un label dans une "pill" centrée
sur une fine barre horizontale qui traverse la card.
Caché automatiquement en vue horizontale (les rows .intervention-v2 sont
masquées dans cette vue, donc le séparateur le serait visuellement seul).
========================================================================== */
.day-period-sep {
display: flex;
align-items: center;
gap: 10px;
margin: 14px 0 8px 0;
padding: 0 6px;
position: relative;
user-select: none;
}
/* v2026.5.39 r2 : ligne plus épaisse (3px), continue, sans dégradé. */
.day-period-sep::before,
.day-period-sep::after {
content: "";
flex: 1 1 auto;
height: 3px;
background: var(--border-strong);
border-radius: 2px;
}
/* v2026.5.39 r2 : style neutre — pas de couleur ambre/bleu. */
.day-period-sep .day-period-label {
flex: 0 0 auto;
font-size: 12px;
font-weight: 700;
letter-spacing: 1.4px;
text-transform: uppercase;
color: var(--text-muted);
background: var(--bg-muted);
border: 1px solid var(--border-strong);
border-radius: 14px;
padding: 4px 14px;
white-space: nowrap;
}
/* Premier séparateur : pas de marge top excessive (juste après les stats) */
.card-stats + .day-period-sep {
margin-top: 8px;
}
/* Vue horizontale : la liste détaillée est masquée donc le séparateur aussi */
html.view-horizontal .day-period-sep {
display: none !important;
}
/* ==========================================================================
v2026.5.39 : Admin — section Apparence (rows label/control + select +
groupe boutons zoom).
========================================================================== */
.admin-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 14px 0;
border-bottom: 1px solid var(--border);
}
.admin-row:last-child {
border-bottom: none;
}
.admin-row-label {
flex: 1 1 auto;
min-width: 0;
}
.admin-row-label strong {
font-size: 14px;
color: var(--text);
}
.admin-row-desc {
font-size: 12px;
color: var(--text-faint);
margin-top: 4px;
line-height: 1.4;
}
.admin-row-control {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 6px;
}
.admin-select {
padding: 6px 10px;
font-size: 13px;
background: var(--bg-elevated);
color: var(--text);
border: 1px solid var(--border-strong);
border-radius: 4px;
cursor: pointer;
min-width: 200px;
}
.admin-input-num {
width: 80px !important;
text-align: center;
}
.admin-zoom-group {
display: flex;
gap: 4px;
}
.btn-zoom {
min-width: 56px;
padding: 6px 10px;
font-size: 12px;
font-weight: 500;
background: var(--bg-elevated);
color: var(--text-muted);
border: 1px solid var(--border-strong);
border-radius: 4px;
cursor: pointer;
transition: background 0.1s, color 0.1s, border-color 0.1s;
}
.btn-zoom:hover {
background: var(--bg-hover);
color: var(--text);
}
.btn-zoom.active {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
/* ==========================================================================
v2026.5.39 r5 : zoom texte via variable --text-scale (0.8 à 1.2).
Modifie uniquement les font-size, pas le layout. Stockée par JS dans
admin_config.textZoom et appliquée sur <html> au boot.
On utilise un selecteur `*` ciblant tous les éléments qui ont
`font-size` défini en `px` ou `rem` — nope c'est impossible. Donc on
applique sur :root un font-size inheritable, et on bump les tailles
spécifiques via calc(). Pour simplicité on cible juste body+composants
principaux. Les éléments restants (avec font-size en px hardcodé) seront
inchangés, mais comme la majorité des textes hérite de body, ça suffit
en pratique.
========================================================================== */
/* v2026.5.39 r7 : zoom GLOBAL sur le body — fait scaler TOUS les textes
visibles (interventions, popups, absences, réservations, tooltips, cards,
sidebar, etc.). Application via body.style.zoom = "<pct>" depuis JS.
On garde aussi --text-scale pour la date/heure (qui restent à la même
taille entre elles, même règle calc()). */
:root {
--text-scale: 1;
--zoom-factor: 1;
--zoom-inv: 1;
}
.app-clock-time,
.app-clock-date { font-size: calc(18px * var(--text-scale)) !important; }
/* v2026.5.39 r12 : le panel admin suit le zoom comme tout le reste.
Pas d'effet yo-yo car le zoom n'est plus appliqué pendant le drag du
slider (seulement au release) — voir _applyTextZoom dans viewer.js. */
/* Slider taille texte dans Apparence — v2026.5.39 r6 : 5 dots sur la piste */
.admin-zoom-slider-wrap {
display: flex;
align-items: center;
gap: 12px;
}
.admin-zoom-slider {
width: 220px;
height: 26px;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
background: transparent;
}
.admin-zoom-slider::-webkit-slider-runnable-track {
height: 4px;
background:
radial-gradient(circle at 0% 50%, var(--text-muted) 0 4px, transparent 5px),
radial-gradient(circle at 25% 50%, var(--text-muted) 0 4px, transparent 5px),
radial-gradient(circle at 50% 50%, var(--text-muted) 0 4px, transparent 5px),
radial-gradient(circle at 75% 50%, var(--text-muted) 0 4px, transparent 5px),
radial-gradient(circle at 100% 50%, var(--text-muted) 0 4px, transparent 5px),
var(--border-strong);
border-radius: 2px;
}
.admin-zoom-slider::-moz-range-track {
height: 4px;
background:
radial-gradient(circle at 0% 50%, var(--text-muted) 0 4px, transparent 5px),
radial-gradient(circle at 25% 50%, var(--text-muted) 0 4px, transparent 5px),
radial-gradient(circle at 50% 50%, var(--text-muted) 0 4px, transparent 5px),
radial-gradient(circle at 75% 50%, var(--text-muted) 0 4px, transparent 5px),
radial-gradient(circle at 100% 50%, var(--text-muted) 0 4px, transparent 5px),
var(--border-strong);
border-radius: 2px;
}
.admin-zoom-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--accent);
border: 2px solid var(--bg-elevated);
box-shadow: 0 1px 3px rgba(0,0,0,0.25);
cursor: pointer;
margin-top: -7px;
}
.admin-zoom-slider::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--accent);
border: 2px solid var(--bg-elevated);
box-shadow: 0 1px 3px rgba(0,0,0,0.25);
cursor: pointer;
}
.admin-zoom-value {
min-width: 48px;
text-align: right;
font-size: 13px;
font-weight: 600;
color: var(--text);
font-variant-numeric: tabular-nums;
}
/* ==========================================================================
v2026.5.39 r6 : section "À propos" du panel admin
========================================================================== */
.admin-about-card {
display: flex;
flex-direction: column;
gap: 0;
background: var(--bg-muted);
border: 1px solid var(--border);
border-radius: 6px;
padding: 16px 20px;
margin-top: 8px;
}
.admin-about-row {
display: flex;
gap: 16px;
padding: 8px 0;
border-bottom: 1px solid var(--border);
}
.admin-about-row:last-child {
border-bottom: none;
}
.admin-about-label {
flex: 0 0 130px;
font-size: 13px;
color: var(--text-muted);
font-weight: 500;
}
.admin-about-value {
flex: 1 1 auto;
font-size: 13px;
color: var(--text);
word-break: break-word;
}
.admin-about-value a {
color: var(--accent);
text-decoration: none;
}
.admin-about-value a:hover {
text-decoration: underline;
}
.admin-about-desc {
margin: 12px 0;
padding: 12px 14px;
background: var(--bg-elevated);
border-left: 3px solid var(--accent);
border-radius: 0 4px 4px 0;
font-size: 13px;
line-height: 1.55;
color: var(--text);
}
+9
View File
@@ -1,4 +1,13 @@
<!doctype html>
<!--
Planification — Extension navigateur EasyVista (Canton de Vaud / DGNSI)
Copyright (c) 2026 Quentin Rouiller
Licensed under the MIT License — see LICENSE file in the project root.
@author Quentin Rouiller
@repository https://gitea.netaplaid.ch/FroSteel/Planification
-->
<html lang="fr">
<head>
<meta charset="utf-8">
+817 -81
View File
File diff suppressed because it is too large Load Diff