Version 5.0.9 — Stabilisation série 5.0

This commit is contained in:
2026-04-20 13:00:00 +02:00
parent 6ae440cbf1
commit 8435a2b77e
5 changed files with 1287 additions and 48 deletions
+368 -41
View File
@@ -3067,7 +3067,7 @@ async function refreshStatuses(techs, isoDate, opts = {}) {
updateProgressBar(i + 1, toFetch.length);
}
// Sauvegarde périodique du cache pendant le fetch
// Sauvegarde périodique du cache pdt le fetch
if (sinceLastCacheWrite >= CACHE_WRITE_EVERY) {
try {
await writeCache(isoDate, { techs });
@@ -3123,7 +3123,7 @@ async function fetchAndUpdateIntervention(iv, myToken) {
return;
}
// v4.1.2 : pour chaque intervention on fait xhr2 PUIS fiche.
// v4.1.2 : pour chaque interventoin on fait xhr2 PUIS fiche.
// - xhr2 : récupère les VRAIES infos contact/lieu (attr1/attr2 du XML
// sont parfois erronées si le tech a corrigé après planif).
// On met à jour la carte tout de suite avec les vraies infos.
@@ -3203,7 +3203,7 @@ async function fetchAndUpdateIntervention(iv, myToken) {
// ─── Étape 3 : API timeline → texte complet de l'action ─────────────
// Le HTML brut de la fiche ne contient PAS les valeurs d'action (elles
// sont injectées côté client par Angular via un appel REST). On appelle
// sont injectées côté client par Angular via un apel REST). On appelle
// donc le même endpoint REST qu'Angular pour récupérer la description
// complète, match par ACTION_ID === iv.actionId (fiable, numérique).
//
@@ -3297,6 +3297,61 @@ async function fetchAndUpdateIntervention(iv, myToken) {
* (venant du texte d'action validé par le tech) sont plus fiables que
* attr1/attr2 (planification initiale parfois erronée).
*/
// v4.3.2 : pré-fetch de tous les xhr2 en parallèle (batch).
// Objectif : avoir les VRAIES infos contact/lieu pour toutes les interventions
// AVANT que l'utilisateur se mette à les survoler. Comme le xhr2 est léger
// (2-5 KB), on peut en faire plusieurs en parallèle sans écrouler EasyVista.
//
// Params :
// techs : liste des techs avec leurs interventions
// myToken : jeton d'annulation (si l'user change de date, on s'arrête)
// forceAll : si true, re-fait le xhr2 même pour les inter déjà xhr2Fetched
// (utilisé par "Tout recharger")
async function prefetchAllXhr2(techs, myToken, forceAll) {
if (!techs) return;
// Lister les iv qui ont besoin d'un xhr2
const needed = [];
for (const tech of techs) {
for (const iv of tech.interventions || []) {
if (iv.type !== "AL-Intervention") continue;
if (!iv.actionId || iv.ghost) continue;
if (iv.xhr2Fetching) continue;
if (iv.xhr2Fetched && !forceAll) continue;
needed.push(iv);
}
}
if (needed.length === 0) return;
console.log(`[load] pré-fetch xhr2 batch : ${needed.length} interventoin(s)…`);
const t0 = performance.now();
// Si forceAll, reset le flag pour que ensureBulleDescription re-fetch
if (forceAll) {
for (const iv of needed) iv.xhr2Fetched = false;
}
// Batch en parallèle avec concurrency limitée (6) — assez rapide, pas trop
// aggressif sur EasyVista.
const concurrency = 6;
const queue = [...needed];
const workers = [];
for (let w = 0; w < concurrency; w++) {
workers.push((async () => {
while (queue.length > 0) {
if (isRefreshAborted(myToken)) return;
const iv = queue.shift();
try {
await ensureBulleDescription(iv);
} catch (err) {
console.warn("[prefetch xhr2] iv", iv.actionId, err);
}
}
})());
}
await Promise.all(workers);
console.log(`[load] pré-fetch xhr2 fini en ${Math.round(performance.now() - t0)} ms`);
}
async function ensureBulleDescription(iv) {
// Déjà chargé : rien à faire
if (iv.xhr2Fetched) return true;
@@ -3735,7 +3790,7 @@ function setRefreshing(on) {
refreshCounter++;
if (targetIcon) targetIcon.classList.add("spinning");
clearCheckMark();
// Afficher "Rafraîchissement en cours…" si on n'a pas déjà les données
// Afficher "rafraichissement en cours…" si on n'a pas déjà les données
updateCaptureInfoText();
} else {
refreshCounter = Math.max(0, refreshCounter - 1);
@@ -3748,7 +3803,7 @@ function setRefreshing(on) {
}
}
// Force le rafraîchissement du texte "MAJ HH:MM" ou "Rafraîchissement en cours…"
// Force le rafraichissement du texte "MAJ HH:MM" ou "rafraichissement en cours…"
// selon refreshCounter.
function updateCaptureInfoText() {
if (state.currentData) {
@@ -3816,7 +3871,7 @@ function updateProgressBar(done, total) {
label.textContent = `${prefix}${done} / ${total}`;
}
// Affiche/masque le bouton "Arrêter". N'est montré que pendant un refresh
// Affiche/masque le bouton "Arrêter". N'est montré que pdt un refresh
// manuel (clic utilisateur), pas pendant les chargements normaux ni les
// refresh auto 12h/15h.
function showAbortButton(on) {
@@ -3832,7 +3887,7 @@ function showAbortButton(on) {
/**
* Toast de feedback quand l'user clique Arrêter. Les fetches en cours peuvent
* encore prendre 1-2 secondes avant de se terminer (on ne peut pas vraiment
* encore prendre 1-2 secondes avant de se terminer (on ne peut pas vriament
* annuler un fetch() en cours), mais du point de vue de l'interface tout
* est arrêté : plus de mise à jour, plus de cache, plus rien.
*/
@@ -3869,7 +3924,12 @@ function detectOverlaps(techs) {
const ivs = (tech.interventions || []).filter(iv =>
iv && iv.startTime && iv.endTime &&
!iv._disappearRemove &&
iv.type !== "AL-Reservation"
iv.type !== "AL-Reservation" &&
// v4.3.2 : le pompier est une absence "tolérée" qui chevauche par
// nature les heures de travail (garde volontaire) — on l'exclut des
// conflits. En revanche les congés/maladies/formations restent
// détectés car une inter planifiée pdt une absence, c'est un vrai pb.
!iv.isPompier
);
// Reset flag sur toutes les inters du tech (y compris celles ignorées)
for (const iv of (tech.interventions || [])) {
@@ -3908,7 +3968,7 @@ function renderCaptureInfo(data, stats) {
if (refreshCounter > 0) {
// v4.1.20 : message différencié selon le type de refresh actif
// - partial (Actualiser) → "Actualisation en cours…"
// - total (Tout recharger) → "Rafraîchissement en cours…"
// - total (Tout recharger) → "rafraichissement en cours…"
if (activeRefreshButton === "partial") {
info.textContent = "Actualisation en cours…";
} else {
@@ -4123,6 +4183,24 @@ function buildCard(tech, isoDate) {
note.textContent = "Absent toute la journée";
}
body.appendChild(note);
// v5.0.4 : tooltip au hover sur toute la carte absent (pas juste un
// bouton visible). Contient : détail période + bouton supprimer si
// c'est une absence supprimable (actionId réel, pas pompier récurrent).
if (ab.actionId && !ab.isPompier && !ab._recurring) {
// On attache le tooltip sur la CARD ENTIÈRE (card) — comme ça
// survoler n'importe où sur la zone grisée "absent" le déclenche.
const ivCopy = {
...ab,
type: "AL-Absence" // force pour buildTooltipHTML
};
card.addEventListener("mouseenter", (e) => {
showTooltip(e, ivCopy, card);
});
card.addEventListener("mouseleave", () => {
hideTooltip();
});
}
}
// v4.1.20 : cas spécifique Pillonel Olivier, absent tous les vendredis.
@@ -4202,10 +4280,13 @@ function buildCard(tech, isoDate) {
// Timeline
// ============================================================================
// v5.0.0 : constantes timeline globales (avant : locales à buildTimeline),
// pour que updateNowLine puisse les utiliser aussi.
const DAY_START = 8 * 60; // 08:00 en minutes
const DAY_END = 18 * 60; // 18:00 en minutes
const DAY_LEN = DAY_END - DAY_START;
function buildTimeline(realInterventions, pompierBlocks, absenceBlocks, cardEl, isPompier, isAbsent) {
const DAY_START = 8 * 60;
const DAY_END = 18 * 60;
const DAY_LEN = DAY_END - DAY_START;
const wrap = document.createElement("div");
wrap.className = "timeline";
@@ -4346,7 +4427,7 @@ function bindTimelinePopover(el) {
// - Ctrl+clic (Cmd+clic sur Mac) : ouvre dans un onglet en arrière-plan
const kind = el.dataset.kind;
const ivIdxStr = el.dataset.ivIdx;
// Seulement sur les segments avec une intervention (pas les "hole" libres
// Seulement sur les segments avec une interventoin (pas les "hole" libres
// ni certaines absences sans ivIdx)
if (ivIdxStr === undefined) return;
@@ -4410,7 +4491,7 @@ function openInterventionFromTimeline(el, opts) {
if (!row) return;
const actionId = row.dataset.actionId;
if (!actionId) return;
// cupère l'iv depuis state
// recupere l'iv depuis state
const iv = findIvByActionId(actionId);
if (!iv) return;
openInterventionInNewTab(iv, opts || {});
@@ -4527,7 +4608,7 @@ function showTimelinePopover(e, el) {
}
// ============================================================================
// Ligne d'intervention
// Ligne d'interventoin
// ============================================================================
function buildInterventionRow(iv, cardEl) {
@@ -4535,7 +4616,9 @@ function buildInterventionRow(iv, cardEl) {
row.className = "intervention-v2";
row.dataset.actionId = iv.actionId;
if (iv.isPompier) row.classList.add("is-pompier-line");
if (iv.ghost) row.classList.add("is-ghost");
// v4.3.3 : on ne marque plus les ghosts visuellement (classe is-ghost
// retirée). Les tickets disparus sont soit retirés (_disappearRemove),
// soit affichés en vert (_disappearStatus). Plus de barrage.
// v4.2.5 : indicateur "en cours d'analyse" (ticket disparu, on re-fetch
// la fiche pour décider de le garder en vert ou le retirer).
if (iv._disappearChecking) row.classList.add("_checking");
@@ -5331,7 +5414,7 @@ async function copyRef(ref, btn) {
}
// ─── Rendu incrémental (v3.1) ───────────────────────────────────────────────
// Met à jour UNE ligne d'intervention dans le DOM (après qu'un fetch fiche
// Met à jour UNE ligne d'interventoin dans le DOM (après qu'un fetch fiche
// ait enrichi l'objet iv avec sa ref, son statut, etc.). Appelée par
// fetchAndUpdateIntervention pour afficher la ref dès qu'elle arrive, sans
// attendre que tous les workers aient fini ni re-rendre toute la vue.
@@ -5579,7 +5662,7 @@ function showTooltip(e, iv, rowEl) {
}
bulleState.hoveredInRow = true;
// v4.1.12 : positionner la bulle UNE SEULE FOIS au mouseenter, près de la
// carte (row) et pas du curseur. Elle ne bouge plus pendant le survol.
// carte (row) et pas du curseur. Elle ne bouge plus pdt le survol.
// v4.1.15 : si pinned, NE PAS repositionner (la bulle doit rester fixe).
if (!bulleState.pinned) {
positionTooltipAnchored(rowEl || (e && e.currentTarget));
@@ -5596,7 +5679,7 @@ function showTooltip(e, iv, rowEl) {
if (!ok) return;
const tip = tooltipEl();
if (!tip.classList.contains("visible")) return;
// Vérifie qu'on affiche toujours la même intervention (pas un autre hover
// Vérifie qu'on affiche toujours la même interventoin (pas un autre hover
// intervenu entretemps)
if (state.currentTooltipIv === iv) {
tip.innerHTML = buildTooltipHTML(iv);
@@ -5653,24 +5736,19 @@ function hasTextSelectionInTooltip() {
}
function moveTooltip(e) {
// v4.1.12 : la bulle est FIXE (positionnée une fois au mouseenter). Cette
// fonction est conservée pour compat mais ne fait plus rien.
// Historique : avant on suivait la souris. Maintenant la bulle est fixe
// (placée une seule fois au mouseenter). Cette fonction est là juste pour
// pas casser les appels existants.
}
// ============================================================================
// v4.2.4 : Positionnement du tooltip — refonte complète
//
// Stratégie :
// 1. On positionne TOUJOURS avec style.left/top en coordonnées VIEWPORT
// (comme un élément position:fixed).
// 2. Au 1er positionnement, on mesure si `position: fixed` marche vraiment
// sur ce tooltip (grâce à getBoundingClientRect). Si un ancêtre le
// casse (transform / filter / backdrop-filter / contain), le tooltip
// tombe en position:absolute calculée depuis le containing block.
// 3. Si `position: fixed` est cassée, on active un listener scroll qui
// recalcule la position pour qu'elle reste STABLE à l'écran (on traite
// alors style.left/top comme des coordonnées document et on ajoute
// window.scrollX/Y pour compenser).
// Positionnement du tooltip
// ============================================================================
// On positionne avec style.left/top en coords VIEWPORT (comme position:fixed).
// Si un ancêtre casse position:fixed (transform, filter, backdrop-filter ou
// contain), on détecte ça empiriquement au 1er placement via
// getBoundingClientRect — et on bascule en "abs" : mêmes coords mais on
// compense le scroll manuellement pour garder la bulle stable à l'écran.
// ============================================================================
// Position stockée : targetLeft / targetTop = coordonnées VIEWPORT désirées
@@ -5909,13 +5987,41 @@ function pinTooltip() {
closeBtn.type = "button";
closeBtn.className = "pinned-popup-close";
closeBtn.innerHTML = "×";
closeBtn.title = "Fermer";
closeBtn.title = "Désépingler (reste visible tant que la souris est dessus)";
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
_closePinnedPopup(popup);
// Désépinglage "mou" : on marque la popup comme non épinglée mais on la
// laisse visible tant que la souris est dessus. Elle disparaît quand la
// souris sort.
_softUnpinPopup(popup);
});
popup.appendChild(closeBtn);
// v4.3.3 : barre de drag en haut, pour déplacer la popup à la souris.
// Ancrée en haut à 22px de haut ; le padding-top de la popup est augmenté
// côté CSS pour ne pas que le contenu soit caché derrière.
const dragbar = document.createElement("div");
dragbar.className = "pinned-popup-dragbar";
dragbar.title = "Glissez pour déplacer";
popup.appendChild(dragbar);
_attachPopupDragHandler(popup, dragbar);
// v4.3.0 : le popup contient un clone du tooltip live, qui inclut le
// bouton 📌. Dans un popup déjà épinglé, ce bouton devient "désépingler".
// On intercepte le clic ici, avant qu'il remonte.
popup.addEventListener("click", (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
if (action === "pin") {
e.stopPropagation();
e.preventDefault();
_softUnpinPopup(popup);
}
// Les autres actions (reload, copy-ref, etc.) ne sont pas gérées ici ;
// on pourrait les ajouter plus tard si besoin.
});
// Placer en (0,0) temporairement pour mesurer la taille
popup.style.position = "absolute";
popup.style.left = "-9999px";
@@ -5969,12 +6075,167 @@ function _closePinnedPopup(el) {
el.remove();
}
/** Ferme tous les popups épinglés. */
/**
* Désépinglage "mou" : la popup n'est plus considérée épinglée (elle n'est
* plus dans pinnedPopups, donc le comptage pour Ctrl×2 etc. ignore) mais on
* la laisse visible. Elle disparait quand la souris sort.
*/
function _softUnpinPopup(el) {
// Retirer de la liste (pour le comptage Ctrl×2) mais garder le DOM
const idx = pinnedPopups.findIndex(p => p.el === el);
if (idx >= 0) pinnedPopups.splice(idx, 1);
// v4.3.3 corr : basculer visuellement en tooltip normal (retirer tous les
// attributs visuels du mode épinglé : bordure bleue, dragbar, bouton ×,
// padding-top, etc.). La classe .soft-unpinned fait ça côté CSS.
// On retire .pinned-popup pour que les règles visuelles lourdes
// disparaissent, tout en gardant la popup au même endroit (position
// absolute conservée).
el.classList.remove("pinned-popup");
el.classList.add("soft-unpinned");
// Icône 📌 → 📍 pour le clin d'œil (même si elle va bientôt disparaitre)
const pinBtn = el.querySelector('[data-action="pin"]');
if (pinBtn) pinBtn.textContent = "📍";
// Supprimer les éléments propres au mode épinglé : barre de drag et ×
const dragbar = el.querySelector(".pinned-popup-dragbar");
if (dragbar) dragbar.remove();
const closeBtn = el.querySelector(".pinned-popup-close");
if (closeBtn) closeBtn.remove();
// Helper qui joue l'animation de sortie puis supprime le DOM
const animateAndRemove = () => {
el.classList.add("unpinning");
setTimeout(() => el.remove(), 180);
};
if (!el.matches(":hover")) {
animateAndRemove();
return;
}
// Souris dessus : on ne supprime pas tout de suite. On attend mouseleave
// et à ce moment on joue l'animation de sortie et on supprime.
el.addEventListener("mouseleave", animateAndRemove, { once: true });
}
/** Ferme toutes les popups épinglées (appelé par Échap ou changement de date). */
/**
* v5.0.1 : helper pour déclencher la suppression d'une absence ou réservation.
* Affiche la modal de confirmation, puis appelle le background.
*/
function _triggerDeleteItem(actionId, kind, triggerBtn) {
if (!actionId) return;
const label = kind === "reservation" ? "cette réservation" : "cette absence";
showAlertModal({
title: "Confirmer la suppression",
message: `Voulez-vous vraiment supprimer ${label} ? Cette action est irréversible.`,
buttons: [
{ label: "Annuler", variant: "secondary", action: () => {} },
{
label: "Supprimer",
variant: "danger",
action: async () => {
if (triggerBtn) {
triggerBtn.disabled = true;
triggerBtn.textContent = "Suppression…";
}
try {
const resp = await sendMessage({
type: "deletePlanningItem",
actionId: actionId,
kind: kind
});
if (!resp || !resp.ok) {
throw new Error(resp && resp.error ? resp.error : "erreur inconnue");
}
showToast("Supprimé", "L'élément a été retiré du planning.");
unpinTooltip();
closeAllPinnedPopups();
if (state.session) {
await loadForDate(state.currentDate, { forceRefetch: true });
}
} catch (err) {
showAlertModal({
title: "Erreur lors de la suppression",
message: "Impossible de supprimer : " + (err.message || err),
buttons: [{ label: "OK", variant: "secondary", action: () => {} }]
});
if (triggerBtn) {
triggerBtn.disabled = false;
triggerBtn.textContent = "🗑 Supprimer l'absence";
}
}
}
}
]
});
}
function closeAllPinnedPopups() {
for (const p of pinnedPopups.slice()) {
p.el.remove();
}
pinnedPopups.length = 0;
// Fermer aussi les popups en état soft-unpinned qui trainent encore
document.querySelectorAll(".pinned-popup.soft-unpinned").forEach(el => el.remove());
}
/**
* v4.3.3 : permet de déplacer une popup épinglée à la souris via sa barre
* de drag. Met à jour les coords document (position absolute) et le rect
* mémorisé dans pinnedPopups pour que les nouvelles popups évitent bien
* la nouvelle position.
*/
function _attachPopupDragHandler(popup, dragbar) {
let dragging = false;
let startMouseX = 0, startMouseY = 0;
let startLeft = 0, startTop = 0;
const onMouseMove = (e) => {
if (!dragging) return;
const dx = e.clientX - startMouseX;
const dy = e.clientY - startMouseY;
let newLeft = startLeft + dx;
let newTop = startTop + dy;
// Clamper dans le document (pas sortir trop à gauche/haut)
if (newLeft < 4) newLeft = 4;
if (newTop < 4) newTop = 4;
popup.style.left = newLeft + "px";
popup.style.top = newTop + "px";
};
const onMouseUp = () => {
if (!dragging) return;
dragging = false;
popup.classList.remove("dragging");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
// Mettre à jour le rect mémorisé pour la détection de chevauchement
const entry = pinnedPopups.find(p => p.el === popup);
if (entry) {
const l = parseFloat(popup.style.left) || 0;
const t = parseFloat(popup.style.top) || 0;
const w = popup.offsetWidth;
const h = popup.offsetHeight;
entry.rect = { left: l, top: t, right: l + w, bottom: t + h };
}
};
dragbar.addEventListener("mousedown", (e) => {
// Seulement bouton gauche
if (e.button !== 0) return;
e.preventDefault();
dragging = true;
startMouseX = e.clientX;
startMouseY = e.clientY;
startLeft = parseFloat(popup.style.left) || 0;
startTop = parseFloat(popup.style.top) || 0;
popup.classList.add("dragging");
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
}
// v4.1.14/19 : recharger UNIQUEMENT cette intervention. Fetch DIRECT sans
@@ -6237,19 +6498,24 @@ function bindTooltipInteractions() {
}, 1200);
}).catch(() => {});
}
} else if (action === "delete-item") {
// v5.0.0 : supprimer absence/réservation (depuis tooltip)
const actionId = btn.dataset.actionId;
const kind = btn.dataset.kind || "absence";
_triggerDeleteItem(actionId, kind, btn);
}
});
// Clic hors bulle : unpin si épinglé.
// Attention : ne pas déclencher sur clic DANS la bulle (elle contient du
// texte sélectionnable), ni sur clic sur une intervention (qui ouvre la
// texte sélectionnable), ni sur clic sur une interventoin (qui ouvre la
// fiche — le user n'attend pas que la bulle reste épinglée dans ce cas
// mais le comportement "ouvrir la fiche" reste prioritaire).
document.addEventListener("mousedown", (e) => {
if (!bulleState.pinned) return;
// Clic dans la bulle → on laisse (sélection de texte)
if (el.contains(e.target)) return;
// Dans tous les autres cas (y compris clic sur une autre intervention),
// Dans tous les autres cas (y compris clic sur une autre interventoin),
// on désépingle. Si c'était un clic sur intervention, le handler
// d'ouverture de la fiche s'exécutera ensuite normalement.
unpinTooltip();
@@ -6268,6 +6534,23 @@ function buildTooltipHTML(iv) {
}
if (iv.reservationLabel) rows.push(row("Sujet", iv.reservationLabel));
if (iv.reservationCreator) rows.push(row("Par", iv.reservationCreator));
// v5.0.0 : bouton supprimer pour les réservations (avec confirmation)
rows.push(`<dt></dt><dd><button type="button" class="tooltip-delete-btn" data-action="delete-item" data-action-id="${escapeHtml(iv.actionId || "")}" data-kind="reservation">🗑 Supprimer cette réservation</button></dd>`);
return `<dl>${rows.join("")}</dl>`;
}
// v5.0.0 : cas spécial absence (congé, maladie, formation, pompier, ...)
if (iv.type === "AL-Absence") {
const label = iv.label || "Absence";
rows.push(`<dt>Type</dt><dd><span class="status-pill other">${escapeHtml(label)}</span></dd>`);
if (iv.startTime && iv.endTime) {
rows.push(row("Horaire", `${iv.startTime}${iv.endTime}`));
}
// Pour les absences récurrentes (Pillonel vendredi), pas d'actionId réel
// → pas de bouton supprimer. Pour les autres → oui.
if (iv.actionId) {
rows.push(`<dt></dt><dd><button type="button" class="tooltip-delete-btn" data-action="delete-item" data-action-id="${escapeHtml(iv.actionId)}" data-kind="absence">🗑 Supprimer cette absence</button></dd>`);
}
return `<dl>${rows.join("")}</dl>`;
}
@@ -6488,15 +6771,59 @@ function hideEvUnreachable() {
// que les mises à jour sont arrêtées.
function showSessionExpiredBanner() {
const b = document.getElementById("session-expired-banner");
if (b) b.classList.remove("hidden");
// Masquer la bannière EV si présente (on ne montre qu'une bannière à la fois)
if (b) {
b.classList.remove("hidden");
// v5.0.9 : s'assurer que la bannière contient le bouton "Me reconnecter"
// et qu'il appelle triggerReconnect (SSO Windows transparent).
if (!b.querySelector(".session-expired-reconnect-btn")) {
// Chercher le premier .banner-content ou injecter du contenu si vide
let content = b.querySelector(".banner-content") || b;
// Si déjà du contenu natif, on ajoute juste le bouton à la fin
const btn = document.createElement("button");
btn.type = "button";
btn.className = "session-expired-reconnect-btn";
btn.textContent = "🔄 Me reconnecter";
btn.addEventListener("click", () => triggerReconnect());
content.appendChild(btn);
}
}
hideEvUnreachableBanner();
hideReconnectingBanner();
}
function hideSessionExpiredBanner() {
const b = document.getElementById("session-expired-banner");
if (b) b.classList.add("hidden");
}
// v5.0.9 : bannière affichée pendant la reconnexion (remplace la bannière
// expirée après clic sur "Me reconnecter")
function showReconnectingBanner() {
let b = document.getElementById("session-reconnecting-banner");
if (!b) {
// Créer la bannière si elle n'existe pas (dans le topbar)
b = document.createElement("div");
b.id = "session-reconnecting-banner";
b.className = "banner-reconnecting";
b.innerHTML = `
<span class="banner-spinner"></span>
<span class="banner-text">Reconnexion à EasyVista en cours Connectez-vous dans l'onglet qui vient de s'ouvrir.</span>
`;
// L'insérer juste après la topbar
const topbar = document.querySelector(".topbar") || document.querySelector("header") || document.body;
if (topbar.nextSibling) {
topbar.parentNode.insertBefore(b, topbar.nextSibling);
} else {
document.body.insertBefore(b, document.body.firstChild);
}
}
b.classList.remove("hidden");
hideSessionExpiredBanner();
}
function hideReconnectingBanner() {
const b = document.getElementById("session-reconnecting-banner");
if (b) b.classList.add("hidden");
}
// v4.2.5 : bannière non bloquante "EasyVista inaccessible"
function showEvUnreachableBanner() {
const b = document.getElementById("ev-unreachable-banner");