v2026.5.44 — Refonte topbar, personnalisation Apparence, onboarding équipe, fix #1

Refresh / cache / verdicts ghost :
- Rafraîchissement séquentiel (1 fiche à la fois) avec arrêt instantané
  via AbortController.
- Re-fetch checksum frais (basicAutoComplete + redirectHeader).
- Cache merge robuste avec fallback cachedByRef ; cache écrit toutes les
  5 fiches (incrémental).
- Verdicts ghost unifiés : ✓✓ clos/résolu, ✓ Fait (pending), ✓ jaune
  Suspendu, retrait silencieux pour cancelled.
- Statuts EV configurables depuis Paramètres → EasyVista (matching
  insensible à la casse, accents, conjugaisons).
- Mode diagnostic optionnel (Diagnostics) qui logge tout sans rien retirer.

Topbar (vue classique) :
- Sélecteur de date du planning ancré au centre absolu (ne se décale
  plus quand le bouton Arrêter apparaît).
- Bouton Aujourd'hui en toutes lettres.
- Horloge contextuelle réduite à côté.

Personnalisation (Paramètres → Apparence) :
- Couleur de la topbar : 12 presets cliquables + picker custom + champ
  hex. Texte topbar adapté automatiquement (luminance) pour rester lisible.
- Police de l'application : 28 choix (Arial, Helvetica, Verdana, Tahoma,
  Trebuchet, Calibri, Segoe UI, Times New Roman, Georgia, Cambria,
  Garamond, Palatino, Courier, Consolas, Comic Sans, Impact, …) appliquée
  à toute la page (cards, popups, panel admin) avec preview live.
- Export / import du cache et de admin_config.

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

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

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

Bugfix :
- Issue #1 (Pompier + Absence) : les deux badges s'affichent désormais
  avec '/' au lieu de masquer l'absence.
- Absences récurrentes restaurées au switch de groupe (étaient invisibles
  alors qu'en storage).
- Barre de progression / bannière session expirée suivent la hauteur
  dynamique de la topbar (--topbar-height via ResizeObserver).
- STATUS_FR regex limite 30 → 200 chars.
- Description action décodée proprement (\u0022, <br>, HTML strippé) ;
  préfixe 'login:' retiré du commentaire technicien.
- Flèche '↗' retirée des références cliquables.
This commit is contained in:
FroSteel
2026-05-01 18:08:11 +02:00
parent 54b8f826df
commit 2d242d26ec
7 changed files with 3223 additions and 470 deletions
+69 -7
View File
@@ -299,14 +299,39 @@ async function fetchPlanningXml(origin, phpsessid, unixDate) {
* @param {string} origin - origine EasyVista (pour construire le Referer)
* @param {object} [opts] - options fetch (method, body, headers supplémentaires)
*/
// registre global des AbortController des fetchs EV en vol. Permet
// au foreground (viewer.js) d'envoyer un message "abortAllFetches" pour
// tuer instantanément les requêtes en cours quand l'user clique "Arrêter".
const _evFetchControllers = new Set();
function _abortAllEvFetches() {
for (const c of _evFetchControllers) {
try { c.abort(); } catch (e) { /* ignore */ }
}
_evFetchControllers.clear();
}
async function evFetch(url, origin, opts = {}) {
const defaultHeaders = {
"Referer": `${origin}/index.php?eventName=HelpDesk_PlanningItem`,
"X-Requested-With": "XMLHttpRequest"
};
const headers = Object.assign({}, defaultHeaders, opts.headers || {});
const fetchOpts = Object.assign({ credentials: "include" }, opts, { headers });
return await fetch(url, fetchOpts);
// on ne remplace pas un signal explicitement passé par l'appelant.
let controller = null;
if (!opts.signal) {
controller = new AbortController();
_evFetchControllers.add(controller);
}
const fetchOpts = Object.assign(
{ credentials: "include" },
opts,
{ headers, signal: opts.signal || (controller && controller.signal) }
);
try {
return await fetch(url, fetchOpts);
} finally {
if (controller) _evFetchControllers.delete(controller);
}
}
/**
@@ -376,10 +401,10 @@ async function fetchFicheHtml(origin, phpsessid, formLink) {
continue;
}
// Sinon : on retourne ce qu'on a
return html;
// on signale au foreground si la dernière réponse est tronquée pour
// qu'il puisse afficher un ⚠ et probe la session.
return { html, truncated: html.length < MIN_VALID_SIZE, size: html.length };
}
// Ne devrait pas arriver (la boucle fait return avant)
throw new Error("fetchFicheHtml: max retries reached");
}
@@ -1225,6 +1250,13 @@ async function detectTeamFromEV(origin, phpsessid, groupIdArg, supportIdsArg) {
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
(async () => {
try {
// abort de toutes les requêtes EV en vol (clic sur "Arrêter").
if (msg.type === "abortAllFetches") {
_abortAllEvFetches();
sendResponse({ ok: true });
return;
}
if (msg.type === "getSession") {
const session = await findEasyVistaSession();
sendResponse({ ok: true, session });
@@ -1282,12 +1314,14 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
return;
}
try {
const html = await fetchFicheHtml(session.origin, session.phpsessid, msg.formLink);
// fetchFicheHtml renvoie maintenant { html, truncated, size }.
const result = await fetchFicheHtml(session.origin, session.phpsessid, msg.formLink);
const html = result.html;
if (looksLikeLoginPage(html)) {
sendResponse({ ok: false, error: "session_expired" });
return;
}
sendResponse({ ok: true, html, session });
sendResponse({ ok: true, html, session, truncated: !!result.truncated, size: result.size });
} catch (err) {
sendResponse({
ok: false,
@@ -1299,6 +1333,34 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
return;
}
// probe rapide de session — fetch un endpoint léger pour vérifier
// que PHPSESSID est toujours valide. Renvoie ok=false/error=session_expired
// si la session est morte.
if (msg.type === "checkSession") {
const session = await findEasyVistaSession();
if (!session) {
sendResponse({ ok: false, error: "no_session" });
return;
}
try {
const url = `${session.origin}/index.php?eventName=HelpDesk_PlanningItem&PHPSESSID=${encodeURIComponent(session.phpsessid)}`;
const r = await evFetch(url, session.origin);
if (!r.ok) {
sendResponse({ ok: false, error: classifyHttpStatus(r.status), httpStatus: r.status });
return;
}
const txt = await r.text();
if (looksLikeLoginPage(txt) || txt.length < 5000) {
sendResponse({ ok: false, error: "session_expired" });
return;
}
sendResponse({ ok: true });
} catch (err) {
sendResponse({ ok: false, error: "fetch_failed", detail: err.message || String(err) });
}
return;
}
if (msg.type === "fetchTimelineApi") {
const session = await findEasyVistaSession();
if (!session) {