PS-PING.PS1

<#PSScriptInfo
.VERSION 4.79
.GUID caf09809-49a0-4e4c-b526-7444f4aa3a09
.AUTHOR Eric Guiffault
.COMPANYNAME CL SASU
.COPYRIGHT (c) 2026 Eric Guiffault - MIT License
.TAGS powershell ping monitoring latency icmp network networkmonitor uptime availability packetloss realtime dashboard graph chart html offline sysadmin windows PSEdition_Desktop PSEdition_Core
.LICENSEURI https://github.com/Vietnamix/PS-PING/blob/main/LICENSE
.PROJECTURI https://github.com/Vietnamix/PS-PING
.ICONURI https://raw.githubusercontent.com/Vietnamix/PS-PING/main/icon.png
.EXTERNALMODULEDEPENDENCIES
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES v4.79: Fixed launch via installed command from ISE (relaunch now uses PSCommandPath, the executing script path; psISE.CurrentFile pointed to the editor tab and relaunched the wrong file). Added "please wait" startup messages at all relaunch points and an "Initializing" overlay in the HTML page until first data arrives. v4.78: PSScriptInfo header, 0.25 px dot size steps (0-5), history 3000 -> 5000 points.
#>
 
<#
.DESCRIPTION
 PS-PING is a real-time ping/latency monitor for Windows. It graphs
 round-trip time, timeouts and packet loss per target in an interactive,
 100% offline HTML dashboard (Chart.js). Single self-contained PowerShell
 script, no external dependency, runs on Windows PowerShell 5.1 and
 PowerShell 7+ (strict ConstrainedLanguage compatible).
#>
 
# ============================================================
# SCRIPT : PS-PING - Ping Monitor
# VERSION : 4.79
# AUTEUR : Eric Guiffault
# DATE : 2026-06
# DESCRIPTION : Monitoring de ping - 100% offline.
# Chart.js embarque depuis cache local.
# Compatible ConstrainedLanguage (voir NOTE CL ci-dessous).
# ------------------------------------------------------------
# CHANGELOG v4.79 :
# - FIX ISE / Install-Script : la relance depuis l'ISE utilisait
# $psISE.CurrentFile.FullPath (= l'onglet OUVERT dans l'editeur).
# En tapant "PS-PING" dans la console ISE (script installe via
# Install-Script), c'etait le MAUVAIS fichier qui etait relance :
# powershell.exe fantome, rien ne s'ouvrait. Desormais
# $PSCommandPath (chemin du script en cours d'execution) est
# utilise en priorite ; $psISE.CurrentFile ne sert plus que de
# repli (code colle directement dans la console ISE).
# - MESSAGES D'ATTENTE : aux 3 points de relance (console cachee,
# ISE cache, ISE visible), un message invite a patienter le temps
# que la fenetre du navigateur s'ouvre. La console visible attend
# 2 s avant de rendre la main (message lisible en double-clic).
# - OVERLAY "Initialisation" cote HTML : affiche a l'ouverture de
# la page, retire a la premiere donnee acceptee par la garde de
# session. Couvre les 2-4 s sans aucun indice en console cachee ;
# le bandeau stale (v4.77) prend le relais ensuite.
# ------------------------------------------------------------
# CHANGELOG v4.78 :
# - PUBLICATION PowerShell Gallery : bloc PSScriptInfo + bloc
# .DESCRIPTION en tete de fichier (Test-ScriptFileInfo OK).
# - TAILLE DES POINTS : pas de 0.25 px (0 / 0.25 / 0.50 / 0.75 /
# 1 ... 5). Plafond du curseur ramene de 10 a 5 px. Lecture en
# parseFloat (l'ancien parseInt tronquait les decimales).
# Note : un TIMEOUT reste affiche a 3 px minimum quand les
# points sont actives (lisibilite des pertes inchangee).
# - HISTORIQUE : 3000 -> 5000 points ($MaxKeep, curseur "Points",
# garde MAXKEEP cote JS). A 1 ping/s : ~83 min glissantes.
# ------------------------------------------------------------
# CHANGELOG v4.77 :
# - AUTO-ARRET v2 (porte de PS-MRTG v1.28) : le PID RACINE de la
# fenetre est identifie une fois au demarrage (scan CIM unique :
# notre --user-data-dir + --app= present + PAS de --type=, donc
# le processus racine Chromium, dont la vie = celle de la fenetre).
# La surveillance devient un simple Get-Process toutes les 2 s ;
# arret ~6 s apres fermeture (3 echecs consecutifs, avec un
# re-scan de confirmation par echec). Corrige 3 failles latentes
# de la v4.74 :
# (a) hand-off Chromium -> HasExited vrai fenetre ouverte
# -> FAUX arret (bug observe sur PS-MRTG v1.22) ;
# (b) catch { closed=true } : un doute tuait le monitoring ;
# desormais, dans le doute, on RESTE en vie ;
# (c) handle perdu -> plus aucun arret possible : le scan CIM
# retrouve la fenetre meme sans handle.
# Repli sans CIM : titre de fenetre "*Ping Monitor*" OU handle,
# confirmation longue (~30 s). Le finally ferme aussi notre
# fenetre (PID connu d'abord, balayage CIM en secours).
# - BANDEAU "MONITORING STOPPED" : champ wallMs (heure d'ecriture)
# dans ping_data.js ; si les donnees ont plus de
# max(12 s, 3 x intervalle), bandeau rouge en haut de page.
# Complete la garde startMs v4.73 (qui couvre le cache perime,
# pas la mort du PowerShell - angle mort du mode console cachee).
# - JOURNAL DE DIAGNOSTIC (porte de PS-MRTG v1.20) : transcript
# %TEMP%\PingMonitor\PS-PING.log ; en console cachee, un echec
# silencieux devient lisible apres coup. Ligne console reduite a
# une toutes les 30 s (le transcript ne gonfle plus d'1 ligne/2 s).
# - PERF : construction LINEAIRE du tableau d'items dans
# Write-DataJs (capture de boucle for) ; l'ancien $parts +=
# recopiait le tableau a chaque iteration (~4,5 M de copies
# d'elements toutes les 2 s a 3000 points).
# - Ecriture conditionnee a un NOUVEAU ping (utile si $Interval
# depasse $WriteEvery : plus d'ecritures dupliquees).
# - Passe ASCII (lecon PS-MRTG v1.26) : tirets cadratins et accents
# residuels remplaces ; si le BOM disparait (copier/coller), le
# parseur PS 5.1 ne peut plus exploser sur un octet multi-byte.
# ------------------------------------------------------------
# CHANGELOG v4.76 :
# - Navigateur (Edge/Chrome mode app) : suppression des fenetres
# parasites au demarrage -
# * bulle "We are now syncing your browsing data" (connexion
# implicite au compte) -> --disable-sync + features
# msImplicitSignin / msEdgeSyncBubble desactivees ;
# * barre "Translate page from French?" -> features Translate*
# desactivees + balise <meta name="google" content="notranslate">.
# Si la bulle de compte persiste sur ta machine, decommente
# l'option --guest dans Open-Browser (session invitee, aucun compte ;
# note : --guest ignore --user-data-dir).
# ------------------------------------------------------------
# CHANGELOG v4.75 :
# - CONSOLE CACHEE depuis le script (sans lanceur externe) :
# interrupteur $HideConsole (en tete). Le script se relance dans
# un powershell.exe a fenetre cachee ; seule la fenetre du
# navigateur reste visible. Mettre $false pour le diagnostic.
# Rappel : sans console, pas de Ctrl+C -> fermez la fenetre du
# navigateur (auto-arret) ou tuez powershell.exe.
# ------------------------------------------------------------
# CHANGELOG v4.74 :
# - CORRIGE "interface affichee mais aucun ping" : la page HTML
# etait servie depuis le cache (vieille version). Les fichiers
# (page + donnees) portent desormais un nom UNIQUE par session
# (horodatage de lancement) -> le navigateur charge toujours la
# version courante. Le nettoyage supprime les fichiers des runs
# precedents.
# - AUTO-ARRET : Edge/Chrome sont lances en mode application avec un
# profil dedie ; PowerShell surveille ce processus toutes les 5 s
# et s'arrete automatiquement si la fenetre est fermee.
# (Navigateur par defaut / Firefox : surveillance indisponible.)
# ------------------------------------------------------------
# CHANGELOG v4.73 :
# - Corrige la regression v4.72 (ecran vide) : abandon du compteur
# "seq" cote PowerShell (fragile selon le scope/ConstrainedLanguage).
# - Anti-flicker base UNIQUEMENT sur l'identifiant de session
# (startMs, deja present et monotone d'un lancement a l'autre) :
# les copies en cache d'une ancienne session sont ignorees, et la
# garde ne peut jamais figer ni vider l'affichage.
# - Aucune modification de la logique PowerShell (moins de risque).
# ------------------------------------------------------------
# CHANGELOG v4.71 :
# - Nettoyage AU DEMARRAGE : ping_data.js (et son .tmp) de la
# session precedente sont supprimes avant de relancer, pour
# repartir sur un historique propre. Tout vieil onglet encore
# ouvert se reinitialise au cycle suivant (< 2 s).
# ------------------------------------------------------------
# CHANGELOG v4.7 :
# - Suppression du canal de commande mort (ping_cmd.js / Read-CmdFile).
# Le selecteur d'intervalle UI ne pouvait pas ecrire sur disque
# depuis file:// -> remplace par un affichage en lecture seule.
# - Ecriture ATOMIQUE de ping_data.js (.tmp + Move-Item) :
# plus de lecture partielle cote navigateur.
# - Trim allege : reslice tous les +200 pings au lieu de chaque tick.
# - NOTE CL clarifiee (voir ci-dessous) + alternative ping avec timeout.
# ------------------------------------------------------------
# INSTALLATION OFFLINE - Chart.js :
# Telecharger : chart.js@4.4.0/dist/chart.umd.min.js
# Copier dans : %USERPROFILE%\Documents\chartjs.min.js
# ou %TEMP%\PingMonitor\chartjs.min.js
# ------------------------------------------------------------
# NOTE ConstrainedLanguage :
# Le fallback de telechargement Chart.js utilise System.Net.WebClient,
# qui est BLOQUE en CL STRICT. Dans ce mode, le telechargement echoue
# proprement (try/catch) : deposez alors chartjs.min.js manuellement
# dans un des emplacements listes au demarrage. Le reste du script
# (ping, stats, generation HTML) reste compatible CL.
# ------------------------------------------------------------
# NAVIGATEUR : Edge ou Chrome recommandes
# ============================================================
 
param([switch]$Hidden)
 
# ============================================================
# --- Console cachee (sans fenetre PowerShell) ---
# ============================================================
# Mettre $true pour que le script se relance dans une fenetre
# PowerShell CACHEE (seule la fenetre du navigateur reste visible).
# Mettre $false pour garder la console (utile pour le diagnostic).
#
# IMPORTANT : sans console, l'arret par Ctrl+C n'est plus possible.
# L'auto-arret a la fermeture de la fenetre (Edge/Chrome, v4.74+)
# prend le relais. A defaut, terminez "powershell.exe" via le
# Gestionnaire des taches.
$HideConsole = $true
 
# Relance cachee (hors ISE) : si demande et qu'on n'est pas deja
# l'instance cachee, on relance le script dans un powershell.exe a
# fenetre cachee, puis on quitte l'instance visible. Le commutateur
# -Hidden evite toute boucle de relance.
if ($HideConsole -and -not $Hidden -and $Host.Name -ne "Windows PowerShell ISE Host") {
    $self = $PSCommandPath
    if ($self -and (Test-Path -Path $self -PathType Leaf)) {
        Write-Host ""
        Write-Host " PS-PING demarre en arriere-plan..." -ForegroundColor Cyan
        Write-Host " La fenetre du navigateur va s'ouvrir d'ici quelques secondes," -ForegroundColor Cyan
        Write-Host " merci de patienter." -ForegroundColor Cyan
        Start-Process -FilePath "powershell.exe" `
                      -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-File","`"$self`"","-Hidden" `
                      -WindowStyle Hidden
        Start-Sleep -Seconds 2 # v4.79 : laisse le message lisible (console lancee par double-clic)
        return
    }
    # Si le chemin du script est introuvable (ex. code colle dans la
    # console), on continue normalement, fenetre visible.
}
 
# Relance depuis l'ISE (qui gere mal le monitoring continu) : on
# bascule dans un powershell.exe - cache si $HideConsole, sinon visible.
if ($Host.Name -eq "Windows PowerShell ISE Host") {
    # v4.79 : $PSCommandPath EN PRIORITE - c'est le chemin du script en
    # cours d'EXECUTION, correct aussi bien pour F5 sur le fichier que
    # pour la commande installee (Install-Script puis "PS-PING" tapee
    # dans la console ISE). L'ancien $psISE.CurrentFile.FullPath
    # pointait vers l'onglet OUVERT dans l'editeur : en invocation par
    # commande, il relancait le MAUVAIS fichier (powershell.exe
    # fantome dans le Gestionnaire des taches, rien ne s'ouvre).
    # $psISE.CurrentFile ne sert plus que de repli (code colle
    # directement dans la console ISE, ou $PSCommandPath est vide).
    $scriptPath = $PSCommandPath
    if (-not ($scriptPath -and (Test-Path -Path $scriptPath -PathType Leaf))) {
        $scriptPath = $null
        if ($psISE -and $psISE.CurrentFile) { $scriptPath = $psISE.CurrentFile.FullPath }
    }
    if ($scriptPath -and (Test-Path -Path $scriptPath -PathType Leaf)) {
        if ($HideConsole) {
            Write-Host " ISE detecte - Lancement cache dans powershell.exe..." -ForegroundColor Cyan
            Write-Host " La fenetre du navigateur va s'ouvrir d'ici quelques secondes," -ForegroundColor Cyan
            Write-Host " merci de patienter." -ForegroundColor Cyan
            Start-Process -FilePath "powershell.exe" `
                          -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-File","`"$scriptPath`"","-Hidden" `
                          -WindowStyle Hidden
        } else {
            Write-Host " ISE detecte - Lancement dans powershell.exe..." -ForegroundColor Cyan
            Write-Host " La fenetre du navigateur va s'ouvrir d'ici quelques secondes," -ForegroundColor Cyan
            Write-Host " merci de patienter." -ForegroundColor Cyan
            Start-Process -FilePath "powershell.exe" `
                          -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-NoExit","-File","`"$scriptPath`"" `
                          -WindowStyle Normal
        }
    } else {
        Write-Host " Sauvegardez avec Ctrl+S puis relancez." -ForegroundColor Red
    }
    return
}
 
# ============================================================
# --- Configuration ---
# ============================================================
$Target = "8.8.8.8"
$Interval = 1000
$MaxKeep = 5000
$WriteEvery = 2
$TrimBuffer = 200 # marge avant reslice (perf : evite un reslice par tick)
 
# ============================================================
# --- Dossier temporaire ---
# ============================================================
$scriptTemp = "$env:TEMP\PingMonitor"
if (-not (Test-Path $scriptTemp)) {
    New-Item -ItemType Directory -Path $scriptTemp -Force | Out-Null
}
 
# --- Journal de diagnostic (v4.77, porte de PS-MRTG v1.20) ----------
# En console cachee, un echec silencieux ressemble a "rien ne se
# passe". Ce transcript enregistre tout ce que fait l'instance (test
# ping, lancement navigateur, lignes de log, erreurs, auto-arret)
# dans un fichier consultable apres coup :
# %TEMP%\PingMonitor\PS-PING.log
# -Force ecrase le log du run precedent (pas d'accumulation).
Start-Transcript -Path "$scriptTemp\PS-PING.log" -Force -ErrorAction SilentlyContinue | Out-Null
 
# Identifiant de session = heure de lancement (epoch ms). Calcule TOT
# car il sert a nommer les fichiers de maniere unique a chaque run.
$script:startMs = [long](((Get-Date).ToUniversalTime() - [datetime]"1970-01-01 00:00:00").TotalMilliseconds)
 
# NOMS UNIQUES PAR SESSION : empeche le navigateur de servir une
# ancienne page HTML / d'anciennes donnees depuis son cache file://.
# (C'etait la cause du "interface affichee mais aucun ping" : la page
# HTML, ouverte sans cache-buster, etait servie en version perimee.)
$htmlFile = "$scriptTemp\ping_monitor_$($script:startMs).html"
$dataFile = "$scriptTemp\ping_data_$($script:startMs).js"
$profileDir = "$scriptTemp\browser_profile_$($script:startMs)"
 
# --- Nettoyage des sessions precedentes ---
# Supprime les pages/donnees/profils des runs anterieurs. On NE touche
# PAS a chartjs.min.js (cache offline). Les profils encore verrouilles
# par une fenetre ouverte sont ignores (erreurs silencieuses).
Remove-Item -Path "$scriptTemp\ping_monitor_*.html" -Force -ErrorAction SilentlyContinue
Remove-Item -Path "$scriptTemp\ping_data_*.js","$scriptTemp\ping_data_*.js.tmp" -Force -ErrorAction SilentlyContinue
Remove-Item -Path "$scriptTemp\browser_profile_*" -Recurse -Force -ErrorAction SilentlyContinue
 
# ============================================================
# --- Variables ---
# ============================================================
$script:Target = $Target
$script:startTime = Get-Date
$script:pingTs = @()
$script:pingVs = @()
$script:pingCount = 0
$script:interval = $Interval
$script:appMode = $false # v4.77 : vrai si le navigateur est en mode app (surveillable)
 
# ============================================================
# Fonctions compatibles ConstrainedLanguage
# ============================================================
# ------------------------------------------------------------
# Round-Int : arrondi mathematique d'un double vers l'entier le
# plus proche (0.5 arrondi vers le haut). On n'utilise PAS
# [math]::Round() car ses surcharges/banker's rounding peuvent
# poser souci en ConstrainedLanguage ; cette version reste sure.
# Entree : $n (double). Sortie : [int].
# ------------------------------------------------------------
function Round-Int {
    param([double]$n)
    if (($n - [int]$n) -ge 0.5) { return [int]$n + 1 }
    return [int]$n
}
 
# ------------------------------------------------------------
# Get-NowMs : horodatage "epoch" en millisecondes (entier long).
# Calcule l'ecart entre maintenant (UTC) et le 1970-01-01.
# Le cast [long] force un entier propre : evite la notation
# scientifique (ex: 1.7e12) qui casserait le parsing JS.
# Sert d'axe temporel pour chaque point de ping (champ "t").
# ------------------------------------------------------------
function Get-NowMs {
    $epoch = [datetime]"1970-01-01 00:00:00"
    $span = (Get-Date).ToUniversalTime() - $epoch
    return [long]$span.TotalMilliseconds
}
 
# ------------------------------------------------------------
# Get-ElapsedSec : duree ecoulee depuis le lancement du script,
# en secondes entieres. Utilise pour l'affichage "00h 00m 00s".
# ------------------------------------------------------------
function Get-ElapsedSec {
    return [int]((Get-Date) - $script:startTime).TotalSeconds
}
 
# ------------------------------------------------------------
# Get-PingTime : effectue UN ping vers $TargetHost et renvoie la
# latence en ms (entier). Renvoie -1 en cas de timeout/erreur.
# La convention "-1 = timeout" est utilisee partout (stats + JS)
# pour distinguer un echec d'une vraie latence de 0 ms.
# ResponseTime (PS 5.1) ou Latency (PS 7+) selon la version.
# ------------------------------------------------------------
function Get-PingTime {
    param([string]$TargetHost)
    # NOTE timeout : Test-Connection (PS 5.1) n'expose pas de parametre
    # de timeout. Sur une cible injoignable, l'attente repose sur le
    # comportement par defaut (~quelques secondes max).
    # Alternative AVEC timeout precis (NECESSITE FullLanguage, pas CL) :
    # $p = New-Object System.Net.NetworkInformation.Ping
    # $r = $p.Send($TargetHost, 1000) # 1000 ms
    # if ($r.Status -eq 'Success') { return [int]$r.RoundtripTime }
    # return -1
    try {
        $r = Test-Connection -ComputerName $TargetHost -Count 1 `
                             -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
        if ($null -ne $r) {
            $x = $r | Select-Object -First 1
            if ($null -ne $x.ResponseTime) { return [int]$x.ResponseTime }
            if ($null -ne $x.Latency) { return [int]$x.Latency }
        }
        return -1
    } catch { return -1 }
}
 
# ------------------------------------------------------------
# Calc-Stats : parcourt l'historique des pings et calcule les
# indicateurs affiches dans le bandeau. Un seul passage O(n).
# Retourne une table de hachage :
# loss = % de paquets perdus (timeouts / total)
# uptime = 100 - loss
# avgV = latence moyenne (sur les pings valides uniquement)
# lastV = dernier ping (-1 si timeout)
# minStr / maxStr = min / max en chaine ("-1" si aucun valide)
# Les valeurs < 0 (timeouts) sont exclues des min/max/moyenne.
# ------------------------------------------------------------
function Calc-Stats {
    $timeouts=0; $sumV=0; $minV=99999; $maxV=0; $validCnt=0
    # Un seul parcours : on compte timeouts, somme, min et max.
    for ($i=0; $i -lt $script:pingCount; $i++) {
        $v = $script:pingVs[$i]
        if ($v -lt 0) { $timeouts++ } # -1 = timeout
        else {
            $validCnt++; $sumV += $v
            if ($v -lt $minV) { $minV = $v }
            if ($v -gt $maxV) { $maxV = $v }
        }
    }
    # Taux de perte (protege contre division par zero)
    $lossRaw = 0
    if ($script:pingCount -gt 0) { $lossRaw = ($timeouts * 100) / $script:pingCount }
    $loss = Round-Int $lossRaw
    # Moyenne sur les pings valides seulement
    $avgV = 0
    if ($validCnt -gt 0) { $avgV = Round-Int ($sumV / $validCnt) }
    # Dernier ping (pour la pastille de statut live)
    $lastV = -1
    if ($script:pingCount -gt 0) { $lastV = $script:pingVs[$script:pingCount - 1] }
    # Min/Max en chaine : "-1" signale "aucune donnee valide"
    $minStr = "-1"; if ($validCnt -gt 0) { $minStr = "$minV" }
    $maxStr = "-1"; if ($validCnt -gt 0) { $maxStr = "$maxV" }
    return @{ loss=$loss; uptime=(100-$loss); avgV=$avgV; lastV=$lastV; minStr=$minStr; maxStr=$maxStr }
}
 
# ------------------------------------------------------------
# Write-DataJs : genere le fichier ping_data.js que le navigateur
# recharge en boucle. C'est le PONT PowerShell -> page HTML.
# Format : window.PD = { items:[{t,v},...], target, stats... }
# - items : tableau des points (t = timestamp ms, v = latence/-1)
# - les autres champs sont les stats deja calculees (bandeau)
# Les timestamps sont forces en [long] et les latences en [int]
# pour eviter tout NaN / notation scientifique cote JS.
# ECRITURE ATOMIQUE : on ecrit dans un .tmp puis on renomme
# (Move-Item -Force). Le rename est instantane sur un meme
# volume, donc le navigateur ne lit jamais un fichier tronque.
# ------------------------------------------------------------
function Write-DataJs {
    # Construit le tableau d'items {t,v} sous forme de chaine JS.
    # v4.77 : construction LINEAIRE par capture de boucle. L'ancien
    # motif `$parts += "..."` recopiait tout le tableau a chaque
    # iteration (O(n^2) par appel : ~4,5 M de copies d'elements
    # toutes les 2 s a 3000 points). La capture affecte une seule
    # fois et reste compatible ConstrainedLanguage.
    $parts = for ($i=0; $i -lt $script:pingCount; $i++) {
        $ts = [long]$script:pingTs[$i] # timestamp -> entier long
        $v = [int]$script:pingVs[$i] # latence -> entier
        "{t:$ts,v:$v}"
    }
    # Stats + duree formatee pour le bandeau
    $st = Calc-Stats
    $elapsed = Get-ElapsedSec
    $hh = [int]($elapsed/3600); $mm = [int](($elapsed%3600)/60); $ss = $elapsed%60
    # Assemble l'objet global window.PD lu par loadData() cote JS.
    # v4.77 : wallMs = heure murale de CETTE ecriture. La page la
    # compare a sa propre horloge (meme machine) pour detecter un
    # PowerShell arrete/mort -> bandeau "MONITORING STOPPED".
    $js = "window.PD={items:[" + ($parts -join ",") + "]," +
          "target:`"$($script:Target)`",startMs:$($script:startMs)," +
          "total:$($script:pingCount),loss:$($st.loss),uptime:$($st.uptime)," +
          "lastV:$($st.lastV),minV:$($st.minStr),maxV:$($st.maxStr),avgV:$($st.avgV)," +
          "interval:$($script:interval)," +
          "wallMs:$(Get-NowMs)," +
          "dur:`"$("{0:D2}h {1:D2}m {2:D2}s"-f $hh,$mm,$ss)`"," +
          "now:`"$(Get-Date -Format "HH:mm:ss")`"};"
    # Ecriture atomique : .tmp puis rename (Move-Item -Force) sur meme volume.
    # Empeche le navigateur de lire un fichier a moitie ecrit.
    $tmp = "$dataFile.tmp"
    try {
        $js | Set-Content -Path $tmp -Encoding UTF8 -ErrorAction Stop
        Move-Item -Path $tmp -Destination $dataFile -Force -ErrorAction Stop
    } catch {
        # Si le rename echoue (verrou AV, etc.), repli en ecriture directe
        try { $js | Set-Content -Path $dataFile -Encoding UTF8 -ErrorAction SilentlyContinue } catch {}
    }
}
 
# ------------------------------------------------------------
# Get-ChartJsTag : produit la balise <script> de Chart.js a
# injecter dans le HTML. Strategie 100% offline d'abord :
# 1) cherche un fichier local chartjs.min.js / chart.min.js
# dans %TEMP%\PingMonitor, Documents et Desktop ;
# 2) s'il est trouve (>10 Ko = fichier reel), l'embarque INLINE
# dans la page et le recopie dans %TEMP% pour les fois suivantes ;
# 3) sinon, tente un telechargement CDN (echoue proprement en CL) ;
# 4) en dernier recours, renvoie une balise CDN distante (online).
# Retour : une chaine "<script>...</script>".
# ------------------------------------------------------------
function Get-ChartJsTag {
    # Emplacements ou l'on accepte de trouver Chart.js (ordre de priorite)
    $candidates = @(
        "$scriptTemp\chartjs.min.js",
        "$scriptTemp\chart.min.js",
        "$env:USERPROFILE\Documents\chartjs.min.js",
        "$env:USERPROFILE\Documents\chart.min.js",
        "$env:USERPROFILE\Desktop\chartjs.min.js",
        "$env:USERPROFILE\Desktop\chart.min.js"
    )
    Write-Host " Chart.js : recherche locale..." -ForegroundColor Cyan
    foreach ($c in $candidates) {
        if (-not $c) { continue }
        if (Test-Path $c) {
            $src = Get-Content -Path $c -Raw -ErrorAction SilentlyContinue
            if ($src -and $src.Length -gt 10000) {
                Write-Host " Chart.js : OK - $c ($([int]($src.Length/1024)) KB)" -ForegroundColor Green
                if ($c -ne "$scriptTemp\chartjs.min.js") {
                    try { $src | Set-Content -Path "$scriptTemp\chartjs.min.js" -Encoding UTF8 -ErrorAction SilentlyContinue } catch {}
                }
                return "<script>`n$src`n</script>"
            }
        }
    }
    # NOTE CL : WebClient est bloque en ConstrainedLanguage STRICT.
    # Le try/catch garantit un echec propre (depot manuel requis alors).
    Write-Host " Chart.js : tentative telechargement (FullLanguage requis)..." -ForegroundColor Yellow
    try {
        $wc = New-Object System.Net.WebClient
        $wc.Headers.Add("User-Agent","PowerShell/5.1")
        $src = $wc.DownloadString("https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js")
        if ($src -and $src.Length -gt 10000) {
            $src | Set-Content -Path "$scriptTemp\chartjs.min.js" -Encoding UTF8 -ErrorAction SilentlyContinue
            Write-Host " Chart.js : telecharge OK" -ForegroundColor Green
            return "<script>`n$src`n</script>"
        }
    } catch { Write-Host " Chart.js : telechargement impossible (CL strict ou hors ligne)" -ForegroundColor Red }
    Write-Host " >> Deposez chartjs.min.js dans : $scriptTemp" -ForegroundColor Yellow
    return '<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>'
}
 
# ------------------------------------------------------------
# Open-Browser : ouvre le rapport HTML et RENVOIE le processus du
# navigateur (ou $null si non surveillable).
# - Edge / Chrome : lances en mode APPLICATION (--app) avec un
# profil dedie (--user-data-dir). Cela cree une instance isolee
# et un processus PROPRE que l'on peut surveiller : quand la
# fenetre est fermee, le processus se termine, et le script
# PowerShell peut s'auto-arreter (voir boucle MAIN).
# - Autres navigateurs / defaut : ouverture simple, sans
# surveillance possible -> renvoie $null (pas d'auto-arret).
# Le profil dedie isole aussi le cache : plus de vieille page servie.
#
# v4.76 - suppression des fenetres parasites au demarrage :
# --disable-sync + features msImplicitSignin/msEdgeSyncBubble
# -> coupe la bulle "We are now syncing your browsing data" ;
# features Translate/TranslateUI/msTranslateBubble
# -> coupe la barre "Translate page from French?".
# (La balise <meta name="google" content="notranslate"> dans le
# HTML renforce la desactivation de la traduction.)
# Si la bulle de compte persiste, decommentez "--guest" ci-dessous
# (session invitee = aucun compte ; note : --guest ignore
# --user-data-dir, mais le processus reste surveillable).
# ------------------------------------------------------------
function Open-Browser {
    param([string]$FilePath)
    # Liste des navigateurs preferes (premier trouve = utilise)
    $browsers = @(
        "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
        "C:\Program Files\Microsoft\Edge\Application\msedge.exe",
        "C:\Program Files\Google\Chrome\Application\chrome.exe",
        "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
        "C:\Program Files\Mozilla Firefox\firefox.exe",
        "C:\Program Files (x86)\Mozilla Firefox\firefox.exe"
    )
    $found = $null
    foreach ($b in $browsers) { if (Test-Path $b) { $found = $b; break } }
 
    # Construit une URL file:// (Chromium --app exige une URL)
    $fileUrl = "file:///" + ($FilePath -replace '\\','/')
 
    if ($found -and ($found -like "*msedge.exe" -or $found -like "*chrome.exe")) {
        Write-Host " Navigateur : $(Split-Path $found -Leaf) (mode app - surveille)" -ForegroundColor Green
 
        # Arguments du mode application + neutralisation des pop-ups (v4.76)
        $args = @(
            "--app=$fileUrl",
            "--user-data-dir=$profileDir",
            "--no-first-run",
            "--no-default-browser-check",
            "--disable-sync",
            "--disable-features=msEdgeSyncBubble,msImplicitSignin,Translate,TranslateUI,msTranslateBubble"
            # "--guest" # <-- decommentez si la bulle de compte persiste
        )
 
        try {
            $proc = Start-Process -FilePath $found -PassThru -ArgumentList $args
            $script:appMode = $true # v4.77 : mode app OK -> surveillance armable
            return $proc
        } catch {
            # Repli : ouverture simple si le mode app echoue
            Start-Process -FilePath $found -ArgumentList "`"$FilePath`""
            return $null
        }
    }
    elseif ($found) {
        Write-Host " Navigateur : $(Split-Path $found -Leaf) (auto-arret indisponible)" -ForegroundColor Yellow
        Start-Process -FilePath $found -ArgumentList "`"$FilePath`""
        return $null
    }
    else {
        Write-Host " Navigateur : defaut (auto-arret indisponible ; si IE : 'Allow blocked content')" -ForegroundColor Yellow
        Start-Process $FilePath
        return $null
    }
}
 
# ------------------------------------------------------------
# Find-BrowserWindowPid (v4.77, porte de PS-MRTG v1.28) :
# identifie le VRAI processus de la fenetre, une seule fois.
# Chromium engendre un arbre (gpu, renderer, reseau...) dont les
# enfants portent tous --type=... ; le processus RACINE - celui
# dont la duree de vie = celle de la fenetre - porte notre
# argument --app= et PAS de --type=. Le filtre sur notre
# --user-data-dir dedie garantit que c'est NOTRE fenetre, meme si
# le lanceur a fait un hand-off (le handle renvoye par
# Start-Process ne vaut alors plus rien). Renvoie 0 si introuvable
# ou si CIM est indisponible.
# ------------------------------------------------------------
function Find-BrowserWindowPid {
    param([string]$ProfileDir)
    try {
        $procs = Get-CimInstance Win32_Process `
                 -Filter "Name='msedge.exe' OR Name='chrome.exe'" -ErrorAction Stop
        foreach ($p in $procs) {
            $cl = "$($p.CommandLine)"
            if ($cl -like "*$ProfileDir*" -and
                $cl -like "*--app=*" -and
                $cl -notlike "*--type=*") {
                return [int]$p.ProcessId
            }
        }
    } catch { }
    return 0
}
 
# ------------------------------------------------------------
# Test-BrowserOpenFallback (v4.77) : repli quand CIM n'a pas pu
# identifier le PID racine. Renvoie $true si N'IMPORTE QUEL signal
# dit que la fenetre est ouverte ; $false seulement si TOUS la
# disent fermee. Direction sure : dans le doute, on reste en vie
# (l'inverse de l'ancien catch { closed = $true } de la v4.74,
# qui tuait le monitoring au moindre doute).
# Signal 1 : une fenetre de navigateur dont le TITRE contient
# "Ping Monitor" (independant du modele de processus).
# Signal 2 : le handle du processus lance encore vivant.
# ------------------------------------------------------------
function Test-BrowserOpenFallback {
    # Signal 1 : titre de fenetre
    try {
        $w = Get-Process -Name msedge, chrome, firefox -ErrorAction SilentlyContinue |
             Where-Object { $_.MainWindowTitle -like "*Ping Monitor*" }
        if ($w) { return $true }
    } catch { }
    # Signal 2 : handle du processus lance
    try { if ($script:browserProc -and -not $script:browserProc.HasExited) { return $true } } catch { }
    return $false
}
 
# ============================================================
# Generation HTML
# ============================================================
# Write-HtmlOnce : ecrit ping_monitor.html UNE SEULE FOIS au
# demarrage. La page est statique ; seules les DONNEES changent
# ensuite (via ping_data.js recharge en boucle). On evite ainsi
# de regenerer / recharger toute la page a chaque ping.
# - $dataFileJs : chemin du fichier de donnees, antislashs
# doubles pour etre une chaine JS valide ('C:\\...').
# - $chartJsTag : balise <script> Chart.js (inline ou CDN).
# Tout le bloc @"..."@ ci-dessous est du HTML/CSS/JS pur.
# ============================================================
function Write-HtmlOnce {
    param([string]$target)
    $dataFileJs = $dataFile -replace '\\','\\\\' # echappe \ pour le JS
    $chartJsTag = Get-ChartJsTag
 
    $html = @"
<!DOCTYPE html>
<html lang="fr" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="google" content="notranslate">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>NN Ping Monitor v4.79</title>
$chartJsTag
<style>
:root{
  --bg:#1a1a2e;--surface:#16213e;--surface2:#0d1b2a;
  --border:#0f3460;--border2:#1e3050;--border3:#0f2040;
  --text:#e0e0e0;--text2:#a0b0c0;--text3:#556;--text4:#889;
  --accent:#4cc9f0;--accent2:#0f3460;
  --ok:#4ade80;--warn:#fb923c;--danger:#f87171;
  --live-bg:#143a20;--live-c:#4ade80;
  --hist-bg:#3a2a08;--hist-c:#fb923c;
  --apply-bg:#0f3460;--apply-c:#4cc9f0;
  --reset-bg:#5a1515;--reset-c:#f87171;
  --tog-bg:transparent;--tog-c:#4466aa;--tog-bd:#1e3050;
  --sb-track:#111927;--sb-bg:#0d1117;
  --sb-live:#1a5a2a;--sb-hist:#1e4a7a;
  --inp-bg:#0d1b2a;--inp-bd:#0f3460;
  --grid:rgba(80,80,120,.15);--grid2:rgba(80,80,120,.2);
  --tick:#556;--tickY:#ccc;
  --cline:rgba(80,255,100,.9);--cfill:rgba(50,220,80,.08);
  --ctobg:rgba(255,60,60,.2);--ctobd:rgba(255,60,60,.6);
  --ptok:rgba(120,255,140,.95);--ptokb:rgba(80,255,100,1);
  --ptto:rgba(255,50,50,.95);--pttob:rgba(255,50,50,1);
  --mbar-c:rgba(160,190,255,.6);
  --mbar-lbl:rgba(180,210,255,.9);
  --tobar-c:rgba(255,80,80,.75);
}
[data-theme="light"]{
  --bg:#f0f4f8;--surface:#ffffff;--surface2:#e8edf5;
  --border:#b0c0d8;--border2:#c8d4e8;--border3:#c0cfe0;
  --text:#0f1923;--text2:#3a5070;--text3:#8090a8;--text4:#607090;
  --accent:#0063be;--accent2:#dde9f8;
  --ok:#007a5e;--warn:#b86000;--danger:#c0202e;
  --live-bg:#d4f0e0;--live-c:#007a5e;
  --hist-bg:#fdefd4;--hist-c:#b86000;
  --apply-bg:#dde9f8;--apply-c:#0063be;
  --reset-bg:#fde8e8;--reset-c:#c0202e;
  --tog-bg:#eef2f8;--tog-c:#3a5070;--tog-bd:#b0c0d8;
  --sb-track:#dde4ee;--sb-bg:#e8edf5;
  --sb-live:#007a5e;--sb-hist:#0063be;
  --inp-bg:#ffffff;--inp-bd:#b0c0d8;
  --grid:rgba(100,120,160,.12);--grid2:rgba(100,120,160,.18);
  --tick:#8090a8;--tickY:#3a5070;
  --cline:rgba(0,130,70,.9);--cfill:rgba(0,180,100,.08);
  --ctobg:rgba(192,32,46,.15);--ctobd:rgba(192,32,46,.5);
  --ptok:rgba(0,140,80,.95);--ptokb:rgba(0,120,60,1);
  --ptto:rgba(200,30,40,.95);--pttob:rgba(200,30,40,1);
  --mbar-c:rgba(40,80,180,.5);
  --mbar-lbl:rgba(20,60,160,.85);
  --tobar-c:rgba(192,32,46,.65);
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:'Segoe UI',Consolas,monospace;
     display:flex;flex-direction:column;height:100vh;overflow:hidden;
     transition:background .25s,color .25s}
.hdr{background:var(--surface);padding:8px 16px;border-bottom:2px solid var(--border);
     display:flex;align-items:center;justify-content:space-between;gap:8px;
     min-height:44px;flex-shrink:0}
.hl{display:flex;align-items:center;gap:10px}
.hdr h1{font-size:1.05em;font-weight:bold;color:var(--text);white-space:nowrap}
.ts{color:var(--accent)}
.vtag{background:var(--accent2);color:var(--accent);font-size:.72em;
      padding:2px 7px;border-radius:10px;font-weight:bold}
.hr2{display:flex;gap:10px;align-items:center;flex-shrink:0}
.dur{color:var(--warn);font-size:.85em;font-weight:bold;white-space:nowrap}
.ub{font-size:.95em;font-weight:bold;padding:3px 10px;
    border-radius:20px;border:2px solid currentColor;white-space:nowrap}
.ug{color:var(--ok)}.uo{color:var(--warn)}.ur{color:var(--danger)}
.ibar{background:var(--surface2);padding:0 16px;border-bottom:1px solid var(--border);
      display:flex;align-items:center;font-size:.81em;height:26px;flex-shrink:0;overflow:hidden}
.sp{width:1px;height:13px;background:var(--border2);margin:0 9px;flex-shrink:0}
.si{display:flex;gap:3px;align-items:center;flex-shrink:0;white-space:nowrap}
.sl{color:var(--text3)}.sv{font-weight:bold}
.ok{color:var(--ok)}.t2{color:var(--danger)}.wn{color:var(--warn)}.nt{color:var(--text2)}
.il{display:flex;align-items:center;gap:5px;flex-shrink:0}
.iv{color:var(--text3);font-size:.88em}
.mv{color:var(--ok);font-size:.88em;font-weight:600}
.mv.h{color:var(--warn)}
.cw{flex:1;position:relative;padding:8px 14px 4px;overflow:hidden;min-height:60px}
.hb{display:none;position:absolute;top:10px;right:16px;
    background:var(--hist-bg);color:var(--hist-c);font-size:.76em;font-weight:bold;
    padding:2px 9px;border-radius:5px;border:1px solid var(--hist-c);
    pointer-events:none;z-index:10}
.hb.v{display:block}
.sbw{background:var(--sb-bg);height:16px;flex-shrink:0;
     border-top:1px solid var(--border3);border-bottom:1px solid var(--border3);
     position:relative;cursor:pointer;user-select:none}
.sbt{position:absolute;top:3px;bottom:3px;left:0;right:0;
     background:var(--sb-track);border-radius:5px;margin:0 4px}
.sbh{position:absolute;top:0;bottom:0;border-radius:5px;min-width:20px;
     cursor:grab;transition:filter .15s}
.sbh:hover,.sbh.d{filter:brightness(1.3)}
.sbh.lm{background:var(--sb-live)}
.sbh.hm{background:var(--sb-hist)}
.sbd{position:absolute;right:4px;top:50%;transform:translateY(-50%);
     width:5px;height:5px;border-radius:50%;background:var(--ok);animation:pl 1.2s infinite}
@keyframes pl{0%,100%{opacity:1}50%{opacity:.2}}
.cb{background:var(--surface);border-top:1px solid var(--border);flex-shrink:0}
.ch{display:flex;align-items:center;justify-content:space-between;
    padding:5px 16px;gap:8px;min-height:36px}
.chl{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.chr{display:flex;align-items:center;gap:6px;flex-shrink:0}
.co{display:flex;gap:10px;align-items:center;flex-wrap:wrap;
    padding:6px 16px 8px;border-top:1px solid var(--border)}
.co.hid{display:none}
.og{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.og-sep{width:1px;height:24px;background:var(--border2);flex-shrink:0}
.og-title{color:var(--text3);font-size:.7em;font-weight:700;
          text-transform:uppercase;letter-spacing:.5px;white-space:nowrap}
.og-note{color:var(--text4);font-size:.68em;font-style:italic;white-space:nowrap}
.ci{display:flex;align-items:center;gap:5px}
.ci label{color:var(--text4);font-size:.78em;white-space:nowrap}
input[type=text]{background:var(--inp-bg);border:1px solid var(--inp-bd);
                 color:var(--accent);padding:3px 8px;border-radius:4px;
                 font-size:.82em;width:165px;font-family:Consolas,monospace;font-weight:bold}
input[type=text]:focus{outline:2px solid var(--accent)}
.sg{display:flex;align-items:center;gap:5px}
.sg label{color:var(--text3);font-size:.75em;white-space:nowrap;min-width:52px;text-align:right}
.sg .sv2{color:var(--accent);font-weight:bold;font-size:.78em;min-width:42px}
input[type=range]{width:100px;accent-color:var(--accent);cursor:pointer}
.btn{padding:4px 11px;border:none;border-radius:4px;cursor:pointer;
     font-size:.78em;font-weight:bold;white-space:nowrap;transition:opacity .15s}
.btn:hover{opacity:.8}
.ba{background:var(--apply-bg);color:var(--apply-c)}
.br{background:var(--reset-bg);color:var(--reset-c)}
.bl{background:var(--live-bg);color:var(--live-c);min-width:78px}
.bh2{background:var(--hist-bg);color:var(--hist-c);min-width:78px}
.bt{background:var(--tog-bg);border:1px solid var(--tog-bd);
    color:var(--tog-c);font-size:.72em;padding:2px 8px;border-radius:4px;cursor:pointer}
.bt:hover,.thbtn:hover{filter:brightness(1.15)}
.thbtn{background:var(--tog-bg);border:1px solid var(--tog-bd);
       color:var(--tog-c);font-size:.72em;padding:2px 10px;
       border-radius:4px;cursor:pointer;white-space:nowrap}
.dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0}
.do{background:var(--ok);animation:pl 1.2s infinite}
.dt{background:var(--danger)}
/* -- Bandeau donnees perimees (v4.77) ----------------------- */
#staleBar{
  display:none; position:fixed; top:0; left:0; right:0; z-index:9999;
  background:#c0392b; color:#fff; text-align:center;
  font:bold 14px Consolas,monospace; padding:7px 10px;
  letter-spacing:.5px; box-shadow:0 2px 8px rgba(0,0,0,.4);
}
/* -- Overlay d'initialisation (v4.79) ------------------------ */
/* Affiche au chargement, masque a la premiere donnee acceptee
   par la garde de session. Couvre les 2-4 s entre l'ouverture de
   la fenetre et les premieres mesures (console cachee = aucun
   autre indice que la page elle-meme). */
#initOv{
  position:fixed; inset:0; z-index:9998;
  display:flex; align-items:center; justify-content:center;
  background:var(--bg); color:var(--text2);
  font:bold 15px Consolas,monospace; letter-spacing:.5px;
}
#initOv .dot{margin-right:10px}
</style>
</head>
<body>
 
<!-- -- BANDEAU DONNEES PERIMEES (v4.77) -----------------------
     Affiche quand ping_data.js n'est plus rafraichi : le
     PowerShell s'est arrete ou est mort. Sans lui, la page
     gelerait en silence (console cachee = aucun indice). -->
<div id="staleBar">&#9888; MONITORING ARRETE - les donnees ne sont plus rafraichies</div>
 
<!-- -- OVERLAY D'INITIALISATION (v4.79) -----------------------
     Visible au chargement de la page, masque des la premiere
     donnee acceptee (voir loadData). -->
<div id="initOv"><span class="dot do"></span>Initialisation &mdash; en attente des premieres mesures...</div>
 
<div class="hdr">
  <div class="hl">
    <span class="dot do" id="sd"></span>
    <h1>PS-PING - Ping Monitor &mdash; <span class="ts" id="tl">$target</span></h1>
    <span class="vtag">v4.79</span>
  </div>
  <div class="hr2">
    <span class="dur" id="dl">--</span>
    <span class="ub ug" id="ul">UpTime : --%</span>
  </div>
</div>
 
<div class="ibar">
  <div class="il">
    <span id="nl" class="iv">--:--:--</span>
    <span style="color:var(--border2);font-size:.8em">|</span>
    <span id="ml" class="mv">Live</span>
  </div>
  <div class="sp"></div>
  <div class="si"><span class="sl">Dernier</span><span class="sv ok" id="s1">--</span></div>
  <div class="sp"></div>
  <div class="si"><span class="sl">Min</span><span class="sv nt" id="s2">--</span></div>
  <div class="sp"></div>
  <div class="si"><span class="sl">Max</span><span class="sv nt" id="s3">--</span></div>
  <div class="sp"></div>
  <div class="si"><span class="sl">Moy</span><span class="sv nt" id="s4">--</span></div>
  <div class="sp"></div>
  <div class="si"><span class="sl">Perte</span><span class="sv ok" id="s5">--%</span></div>
  <div class="sp"></div>
  <div class="si"><span class="sl">Total</span><span class="sv nt" id="s6">0</span></div>
</div>
 
<div class="cw">
  <div id="hb" class="hb">Molette ou scrollbar pour naviguer</div>
  <canvas id="pc"></canvas>
</div>
 
<div class="sbw">
  <div class="sbt" id="sbt">
    <div class="sbh lm" id="sbh">
      <div class="sbd" id="sbdot"></div>
    </div>
  </div>
</div>
 
<div class="cb">
  <div class="ch">
    <div class="chl">
      <div class="ci">
        <label>Cible :</label>
        <input type="text" id="iT" value="$target"
               onkeydown="if(event.key==='Enter')this.blur();"
               onfocus="pr=true" onblur="pr=false">
        <button class="btn ba" onclick="applyT()" onmousedown="pr=false">Appliquer</button>
      </div>
      <button class="btn bl" id="bL" onclick="togLive()">&#9654; Live</button>
      <button class="btn br" onclick="doReset()">&#8635; Vue</button>
    </div>
    <div class="chr">
      <button class="thbtn" id="thBtn" onclick="togTheme()">&#9728; Light</button>
      <button class="bt" id="bto" onclick="togOpt()">&#9660; Options</button>
    </div>
  </div>
  <div class="co hid" id="co">
    <div class="og">
      <span class="og-title">Affichage</span>
      <div class="sg">
        <label>Points :</label>
        <input type="range" id="sP" min="10" max="5000" value="120"
          oninput="document.getElementById('vP').textContent=this.value+' pts';uc();usb()">
        <span class="sv2" id="vP">120 pts</span>
      </div>
      <div class="sg">
        <label>Taille pts :</label>
        <input type="range" id="sD" min="0" max="5" step="0.25" value="0"
          oninput="updDL(this.value);uc()">
        <span class="sv2" id="vD">0 (ligne)</span>
      </div>
      <div class="og-sep"></div>
      <span class="og-title">Intervalle ping</span>
      <div class="sg">
        <label>Intervalle :</label>
        <span class="sv2" id="sInt">-- s</span>
      </div>
      <div class="sg" style="gap:4px">
        <span style="color:var(--text3);font-size:.75em">Prochain :</span>
        <span class="sv2" id="sNext">--</span>
      </div>
      <span class="og-note">(reglable via \$Interval dans le script)</span>
    </div>
  </div>
</div>
 
<script>
// ============================================================
// ETAT GLOBAL JavaScript
// ------------------------------------------------------------
// DJP : chemin du fichier de donnees (injecte par PowerShell)
// aD : tableau des points charges {t,v} (cache local navigateur)
// live : true = suit le temps reel ; false = mode historique
// vo : "view offset" - index du 1er point affiche dans aD
// pr : "pause refresh" - true quand l'utilisateur tape (saisie cible)
// ldc : "last data count" - nb d'items deja charges (delta loading)
// oo : panneau Options ouvert ou non
// sbdrag/sbsx/sbsvo : etat du glisser-deposer de la scrollbar
// darkMode : theme courant
// curInterval/nextIn/cdTimer : intervalle de ping + compte a rebours
// ============================================================
var DJP='$dataFileJs';
var aD=[],live=true,vo=0,pr=false,ldc=0,oo=false;
var sbdrag=false,sbsx=0,sbsvo=0;
var darkMode=true;
var curInterval=1000,nextIn=1000,cdTimer=null;
// Anti-cache/anti-flicker : identifiant de la session de monitoring.
// startMs (heure de lancement, en ms epoch) augmente a chaque relance
// du script. On l'utilise pour rejeter une ancienne copie de
// ping_data.js servie par le cache du navigateur en file://.
var curSession=0;
var lastWall=0; // v4.77 : wallMs de la derniere ecriture vue (horloge PS, meme machine)
var MAXKEEP=5000; // doit correspondre a \$MaxKeep cote PowerShell
 
// ============================================================
// togTheme : bascule sombre <-> clair. Change l'attribut
// data-theme sur <html> (les variables CSS suivent), met a
// jour le libelle du bouton et redessine le graphe (uc) car
// ses couleurs sont lues depuis les variables CSS courantes.
// ============================================================
function togTheme(){
  darkMode=!darkMode;
  document.documentElement.setAttribute('data-theme',darkMode?'dark':'light');
  document.getElementById('thBtn').innerHTML=darkMode?'&#9728; Light':'&#9790; Dark';
  uc();
}
 
// ============================================================
// Intervalle (lecture seule - pilote par le script via ping_data.js)
// ------------------------------------------------------------
// startCD : (re)demarre le compte a rebours "Prochain ping".
// Decremente nextIn de 200 ms et boucle a curInterval.
// updNxt : affiche le compte a rebours en secondes.
// showInt : affiche l'intervalle courant ("1 s", "500 ms"...).
// L'intervalle reel vient du script (champ interval de window.PD) ;
// l'UI ne fait que le refleter - elle ne peut pas le modifier.
// ============================================================
function startCD(){
  if(cdTimer)clearInterval(cdTimer); // evite les timers empiles
  nextIn=curInterval;
  updNxt();
  cdTimer=setInterval(function(){
    nextIn-=200;
    if(nextIn<=0)nextIn=curInterval; // recommence un cycle
    updNxt();
  },200);
}
function updNxt(){
  document.getElementById('sNext').textContent=(nextIn/1000).toFixed(1)+'s';
}
function showInt(){
  var siv=(curInterval>=1000)?(curInterval/1000)+' s':curInterval+' ms';
  document.getElementById('sInt').textContent=siv;
}
 
// ============================================================
// Helpers timestamps - protection NaN et notation scientifique
// ------------------------------------------------------------
// ft : timestamp ms -> "HH:mm:ss" (label complet d'un point)
// fm : timestamp ms -> "HH:mm" (cle de minute, pour les barres)
// parseInt force l'entier ; le garde-fou isNaN/<=0 renvoie un
// placeholder plutot qu'une "Invalid Date".
// ============================================================
function ft(ms){
  var n=parseInt(ms,10);
  if(isNaN(n)||n<=0)return '--:--:--';
  var d=new Date(n);
  return('0'+d.getHours()).slice(-2)+':'+
         ('0'+d.getMinutes()).slice(-2)+':'+
         ('0'+d.getSeconds()).slice(-2);
}
function fm(ms){
  var n=parseInt(ms,10);
  if(isNaN(n)||n<=0)return '--:--';
  var d=new Date(n);
  return('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2);
}
 
// ============================================================
// Plugin Chart.js "vlines" - dessine des barres verticales
// PAR-DESSUS le graphe, dans le hook afterDraw (= apres que
// Chart.js a trace lignes et points). Deux types de barres :
// - toLines : rouge epais, marque chaque timeout
// - minLines : pointilles + label vertical "HH:mm", marque
// chaque changement de minute (reperes temporels)
// Les positions sont lues dans chart.config._vlines, rempli
// par uc() a chaque cycle. Les couleurs viennent des variables
// CSS (donc s'adaptent au theme clair/sombre).
// IMPORTANT : Chart.register() doit etre appele AVANT new Chart().
// ============================================================
Chart.register({
  id:'vlines',
  afterDraw:function(chart){
    if(!chart.config._vlines)return; // rien a dessiner
    var data=chart.config._vlines;
    var hasTo =(data.toLines && data.toLines.length >0);
    var hasMin=(data.minLines && data.minLines.length>0);
    if(!hasTo&&!hasMin)return;
 
    var ctx2=chart.ctx;
    var xs=chart.scales.x,ys=chart.scales.y;
    if(!xs||!ys)return;
    var top=ys.top,bot=ys.bottom;
    var s=getComputedStyle(document.documentElement);
 
    ctx2.save();
 
    // ---- Barres timeout : rouge epais visible ----
    if(hasTo){
      ctx2.strokeStyle=s.getPropertyValue('--tobar-c').trim()||'rgba(255,80,80,.75)';
      ctx2.lineWidth=2;
      ctx2.setLineDash([]);
      ctx2.globalAlpha=0.85;
      data.toLines.forEach(function(idx){
        try{
          var x=xs.getPixelForValue(idx);
          if(x>=xs.left&&x<=xs.right){
            ctx2.beginPath();ctx2.moveTo(x,top);ctx2.lineTo(x,bot);ctx2.stroke();
          }
        }catch(e){}
      });
    }
 
    // ---- Barres minutes : bleu/gris avec label ----
    if(hasMin){
      var mColor=s.getPropertyValue('--mbar-c').trim()||'rgba(160,190,255,.6)';
      var mText =s.getPropertyValue('--mbar-lbl').trim()||'rgba(180,210,255,.9)';
      ctx2.strokeStyle=mColor;
      ctx2.lineWidth=1.5;
      ctx2.setLineDash([4,3]);
      ctx2.globalAlpha=0.8;
 
      // Deduplication : une seule barre par label "HH:mm"
      var drawn={};
      data.minLines.forEach(function(ml){
        if(drawn[ml.label])return;
        drawn[ml.label]=true;
        try{
          var x=xs.getPixelForValue(ml.x);
          if(x>=xs.left&&x<=xs.right){
            ctx2.beginPath();ctx2.moveTo(x,top);ctx2.lineTo(x,bot);ctx2.stroke();
            if(bot-top>50){
              ctx2.save();
              ctx2.setLineDash([]);
              ctx2.globalAlpha=1;
              ctx2.fillStyle=mText;
              ctx2.font='bold 10px Consolas,monospace';
              ctx2.translate(x+5,Math.min(top+90,bot-8));
              ctx2.rotate(-Math.PI/2);
              ctx2.fillText(ml.label,0,0);
              ctx2.restore();
            }
          }
        }catch(e){}
      });
    }
 
    ctx2.restore();
  }
});
 
// ============================================================
// Chart.js - cree APRES Chart.register
// ------------------------------------------------------------
// Deux datasets superposes :
// [0] "ms" : courbe de latence (ligne + points colores)
// [1] "TO" : barres pour les timeouts (valeur 1, axe partage)
// animation:false -> redessins instantanes (live fluide).
// tooltip personnalise -> affiche "Timeout" ou "<n> ms".
// ============================================================
var cx=document.getElementById('pc').getContext('2d');
var ch=new Chart(cx,{
  type:'line',
  data:{labels:[],datasets:[
    {label:'ms',data:[],
     borderColor:'rgba(80,255,100,.9)',backgroundColor:'rgba(50,220,80,.08)',
     borderWidth:2,pointRadius:[],pointBackgroundColor:[],pointBorderColor:[],
     fill:true,tension:.2,spanGaps:false,order:2},
    {label:'TO',data:[],type:'bar',
     backgroundColor:'rgba(255,60,60,.2)',borderColor:'rgba(255,60,60,.6)',
     borderWidth:1,barPercentage:.4,categoryPercentage:1,order:1}
  ]},
  options:{
    responsive:true,maintainAspectRatio:false,animation:false,
    interaction:{mode:'index',intersect:false},
    plugins:{
      legend:{display:false},
      tooltip:{callbacks:{label:function(c){
        return c.datasetIndex===0?(c.parsed.y===null?'Timeout':c.parsed.y+' ms'):'';
      }}}
    },
    scales:{
      x:{ticks:{color:'#556',maxTicksLimit:12,font:{size:9,family:'Consolas'},maxRotation:0},
         grid:{color:'rgba(80,80,120,.15)'}},
      y:{min:0,
         ticks:{color:'#ccc',font:{size:11,family:'Consolas',weight:'bold'},
                callback:function(v){return v+'ms';}},
         grid:{color:'rgba(80,80,120,.2)'}}
    }
  }
});
 
// Conteneur des barres verticales, lu par le plugin "vlines"
ch.config._vlines={toLines:[],minLines:[]};
 
// ============================================================
// Helpers d'affichage
// ------------------------------------------------------------
// gmp : "get max points" - nb de points a afficher (slider).
// gds : "get dot size" - rayon des points. 0 = ligne seule ;
// valeur > 0 = taille fixe ; (mode auto desactive ici car
// le slider commence a 0). Garde la logique densite au cas ou.
// updDL: met a jour le libelle du slider taille de points.
// ============================================================
function gmp(){return parseInt(document.getElementById('sP').value,10);}
function gds(n,w){
  // v4.78 : parseFloat (le slider va de 0 a 5 par pas de 0.25 ;
  // parseInt tronquait 0.25/0.50/0.75 en 0 -> points invisibles).
  var s=parseFloat(document.getElementById('sD').value);
  if(s===0)return 0; // 0 = pas de point (ligne pure)
  if(s>0)return s; // taille fixe demandee
  // (auto) rayon selon la densite de points a l'ecran
  var p=w/Math.max(n,1);
  if(p>=12)return 5;if(p>=7)return 3;if(p>=4)return 2;if(p>=2)return 1;return 0;
}
function updDL(v){
  document.getElementById('vD').textContent=(+v===0)?'0 (ligne)':v+'px';
}
 
// ============================================================
// Scrollbar de navigation dans l'historique
// ------------------------------------------------------------
// usb : "update scrollbar" - positionne et dimensionne le
// curseur (sbh) selon vo / mp / total. Vert = live,
// bleu = historique. La pastille clignotante n'apparait
// qu'en live.
// Les listeners gerent : clic sur la piste (saut), glisser du
// curseur, et relacher (fin du glisser).
// ============================================================
function usb(){
  var th=document.getElementById('sbh'),dt=document.getElementById('sbdot');
  var tot=aD.length,mp=gmp();
  if(tot<=mp){ // tout tient a l'ecran -> curseur plein
    th.style.left='0%';th.style.width='100%';
    th.className='sbh lm';dt.style.display='block';return;
  }
  var r=mp/tot,p=vo/tot; // largeur = fraction visible ; position = offset
  th.style.width=(r*100).toFixed(2)+'%';
  th.style.left=(p*100).toFixed(2)+'%';
  if(live){th.className='sbh lm';dt.style.display='block';}
  else{th.className='sbh hm';dt.style.display='none';}
}
// Clic sur la piste (hors curseur) = saut a cette position
document.getElementById('sbt').addEventListener('mousedown',function(e){
  if(e.target===document.getElementById('sbh'))return;
  var rect=document.getElementById('sbt').getBoundingClientRect();
  var r=(e.clientX-rect.left)/rect.width;
  var tot=aD.length,mp=gmp();
  var nv=Math.round(r*tot-mp/2); // centre la vue sur le clic
  nv=Math.max(0,Math.min(nv,Math.max(0,tot-mp))); // borne dans les limites
  vo=nv;sl(vo>=Math.max(0,tot-mp));uc();usb();
});
// Debut du glisser du curseur : memorise position souris + offset
document.getElementById('sbh').addEventListener('mousedown',function(e){
  e.preventDefault();e.stopPropagation();
  sbdrag=true;sbsx=e.clientX;sbsvo=vo;
  document.getElementById('sbh').classList.add('d');
});
// Glisser en cours : convertit le deplacement souris en offset
document.addEventListener('mousemove',function(e){
  if(!sbdrag)return;
  var rect=document.getElementById('sbt').getBoundingClientRect();
  var tot=aD.length,mp=gmp();
  var dv=Math.round(((e.clientX-sbsx)/rect.width)*tot); // delta pixels -> delta index
  var nv=Math.max(0,Math.min(sbsvo+dv,Math.max(0,tot-mp)));
  vo=nv;sl(vo>=Math.max(0,tot-mp));uc();usb();
});
// Fin du glisser
document.addEventListener('mouseup',function(){
  if(sbdrag){sbdrag=false;document.getElementById('sbh').classList.remove('d');}
});
 
// ============================================================
// uc() = "update chart" - LE COEUR DU RENDU.
// Appelee a chaque cycle (et sur chaque interaction). Elle :
// 1) determine la fenetre visible [vo .. vo+mp] dans aD ;
// en live, se cale toujours sur les derniers points ;
// 2) parcourt cette fenetre pour construire en UN passage :
// - vs : latences (null pour un timeout)
// - tv : dataset barres timeout (1 ou null)
// - pr2/pb/pd : rayon et couleurs de chaque point
// - lb : labels d'axe X (affiches au changement de minute)
// - toLines : index des timeouts (barres rouges)
// - minLines : reperes de minute (barres + label)
// 3) applique les couleurs du theme (variables CSS) ;
// 4) ajuste l'echelle Y au max visible (+marge) ;
// 5) fait UN SEUL ch.update('none') (pas d'animation).
// Tout est fait en un seul redraw pour rester fluide en live.
// ============================================================
function uc(){
  var mp=gmp(),tot=aD.length,cw=cx.canvas.offsetWidth;
  var mv=Math.max(0,tot-mp); // offset max possible
  if(live){vo=mv;}else{vo=Math.max(0,Math.min(vo,mv));} // live = colle a droite
 
  var sl2=aD.slice(vo,vo+mp); // fenetre visible
  var n=sl2.length,ds=gds(n,cw);
  var lb=[],vs=[],tv=[],pr2=[],pb=[],pd=[];
  var toLines=[],minLines=[];
 
  var prevMinKey='__INIT__'; // pour detecter le changement de minute
 
  // Couleurs des points lues dans le theme courant
  var s=getComputedStyle(document.documentElement);
  var ptok=s.getPropertyValue('--ptok').trim()||'rgba(120,255,140,.95)'; // point OK
  var ptokb=s.getPropertyValue('--ptokb').trim()||'rgba(80,255,100,1)';
  var ptto=s.getPropertyValue('--ptto').trim()||'rgba(255,50,50,.95)'; // point timeout
  var pttob=s.getPropertyValue('--pttob').trim()||'rgba(255,50,50,1)';
 
  sl2.forEach(function(d,i){
    var ts=parseInt(d.t,10);
    var timeStr=ft(ts); // "HH:mm:ss"
    var minKey=fm(ts); // "HH:mm" (cle de minute)
 
    // Repere vertical au changement de minute
    if(i>0 && minKey!=='--:--' && minKey!==prevMinKey){
      minLines.push({x:i, label:minKey});
    }
    prevMinKey=minKey;
 
    // Label d'axe X : seulement quand la minute change (sinon vide)
    if(i>0 && minKey!==fm(parseInt(sl2[i-1].t,10))){
      lb.push(timeStr);
    } else {
      lb.push('');
    }
 
    var v=parseInt(d.v,10);
    if(v<0){ // TIMEOUT
      vs.push(null);tv.push(1); // trou dans la ligne + barre
      pr2.push(ds>0?Math.max(ds,3):0);
      pb.push(ptto);pd.push(pttob);
      toLines.push(i);
    } else { // PING OK
      vs.push(v);tv.push(null);
      pr2.push(ds);
      pb.push(ptok);pd.push(ptokb);
    }
  });
 
  // Injection des donnees dans les datasets Chart.js
  ch.data.labels=lb;
  ch.data.datasets[0].data=vs;
  ch.data.datasets[0].pointRadius=pr2;
  ch.data.datasets[0].pointBackgroundColor=pb;
  ch.data.datasets[0].pointBorderColor=pd;
  ch.data.datasets[1].data=tv;
 
  // Couleurs (ligne, remplissage, grille, ticks) selon le theme
  ch.data.datasets[0].borderColor=s.getPropertyValue('--cline').trim();
  ch.data.datasets[0].backgroundColor=s.getPropertyValue('--cfill').trim();
  ch.data.datasets[1].backgroundColor=s.getPropertyValue('--ctobg').trim();
  ch.data.datasets[1].borderColor=s.getPropertyValue('--ctobd').trim();
  ch.options.scales.x.ticks.color=s.getPropertyValue('--tick').trim();
  ch.options.scales.x.grid.color=s.getPropertyValue('--grid').trim();
  ch.options.scales.y.ticks.color=s.getPropertyValue('--tickY').trim();
  ch.options.scales.y.grid.color=s.getPropertyValue('--grid2').trim();
 
  // Transmet les barres verticales au plugin "vlines"
  ch.config._vlines={toLines:toLines,minLines:minLines};
 
  // Echelle Y dynamique : max visible arrondi a la dizaine + marge
  var vv=vs.filter(function(v2){return v2!==null;});
  if(vv.length>0)ch.options.scales.y.max=Math.ceil(Math.max.apply(null,vv)/10)*10+10;
 
  ch.update('none'); // UN seul redraw, sans animation
}
 
// ============================================================
// UI
// ============================================================
// uu() = "update UI" - met a jour le BANDEAU (hors graphe) a
// partir des stats de window.PD : pastille de statut, dernier
// ping, min/max/moyenne, % de perte, uptime (couleur selon
// seuils), duree, heure, cible. Synchronise aussi l'intervalle
// affiche (lecture seule) si le script l'a change.
// ============================================================
function uu(d){
  var lv=d.lastV,lo=d.loss,up=d.uptime;
  // Pastille + dernier ping : vert si OK, rouge si timeout
  var dot=document.getElementById('sd'),s1=document.getElementById('s1');
  if(lv<0){dot.className='dot dt';s1.textContent='Timeout';s1.className='sv t2';}
  else{dot.className='dot do';s1.textContent=lv+' ms';s1.className='sv ok';}
  // Min / Max / Moyenne
  document.getElementById('s2').textContent=d.minV>=0?d.minV+' ms':'--';
  document.getElementById('s3').textContent=d.maxV>=0?d.maxV+' ms':'--';
  document.getElementById('s4').textContent=d.avgV>0?d.avgV+' ms':'--';
  // Taux de perte : vert (0) / orange (<=10) / rouge (>10)
  var e5=document.getElementById('s5');
  e5.textContent=lo+'%';e5.className='sv '+(lo>10?'t2':lo>0?'wn':'ok');
  document.getElementById('s6').textContent=d.total;
  // Uptime : vert >=99 / orange >=90 / rouge sinon
  var ub=document.getElementById('ul');
  ub.textContent='UpTime : '+up+'%';
  ub.className='ub '+(up>=99?'ug':up>=90?'uo':'ur');
  document.getElementById('dl').textContent=d.dur; // duree totale
  document.getElementById('nl').textContent=d.now; // heure courante
  if(d.target)document.getElementById('tl').textContent=d.target;
  // Intervalle pilote par le script (lecture seule cote UI).
  // Si le script l'a modifie, on resynchronise le compte a rebours.
  if(d.interval){
    if(d.interval!==curInterval){
      curInterval=d.interval;
      startCD();
    }
    showInt();
  }
}
 
// ============================================================
// Live / Historique
// ------------------------------------------------------------
// sl(v) : "set live" - bascule l'etat live/historique et met
// a jour les indicateurs visuels (bouton, badge, banner).
// togLive : bouton "Live" - sort en historique, ou revient au
// temps reel (recale la vue tout a droite).
// wheel : molette sur le graphe = navigation dans l'historique
// (sortie auto du live si on remonte dans le passe).
// ============================================================
function sl(v){
  live=v;
  var btn=document.getElementById('bL'),hb=document.getElementById('hb'),ml=document.getElementById('ml');
  if(live){
    btn.innerHTML='&#9654; Live';btn.className='btn bl';
    hb.classList.remove('v');ml.textContent='Live';ml.className='mv';
  } else {
    btn.innerHTML='&#9646;&#9646; Hist.';btn.className='btn bh2';
    hb.classList.add('v');ml.textContent='Historique';ml.className='mv h';
  }
  usb();
}
function togLive(){
  var tot=aD.length,mp=gmp();
  if(live){sl(false);}else{vo=Math.max(0,tot-mp);sl(true);uc();}
}
document.getElementById('pc').addEventListener('wheel',function(e){
  e.preventDefault();
  var mp=gmp(),tot=aD.length,st=Math.max(1,Math.floor(mp/8)); // pas = 1/8 de fenetre
  var nv=vo+(e.deltaY>0?st:-st); // molette bas = avancer
  nv=Math.max(0,Math.min(nv,Math.max(0,tot-mp)));
  vo=nv;sl(vo>=Math.max(0,tot-mp));uc();usb();
},{passive:false});
 
// ============================================================
// Options / Cible / Reset
// ------------------------------------------------------------
// togOpt : ouvre/ferme le panneau d'options.
// applyT : la cible se change dans le SCRIPT (file:// ne peut
// pas piloter PowerShell) -> simple rappel a l'utilisateur.
// doReset: recale la vue en live tout a droite.
// ============================================================
function togOpt(){
  oo=!oo;
  var co=document.getElementById('co'),bt=document.getElementById('bto');
  if(oo){co.classList.remove('hid');bt.innerHTML='&#9650; Options';}
  else{co.classList.add('hid');bt.innerHTML='&#9660; Options';}
}
function applyT(){
  var t=document.getElementById('iT').value.trim();if(!t)return;
  document.getElementById('tl').textContent=t;
  alert('Pour changer la cible, modifiez Target dans le script PowerShell et relancez.');
}
function doReset(){vo=Math.max(0,aD.length-gmp());sl(true);uc();usb();}
 
// ============================================================
// loadData() = LE PONT page <- fichier.
// Recharge ping_data.js toutes les 2 s en injectant une balise
// <script> (avec ?_=timestamp pour aider a casser le cache).
// Quand il s'execute, il (re)definit window.PD.
//
// GARDE ANTI-FLICKER (file://) : le navigateur peut ressortir une
// ancienne copie en cache du fichier. On se sert de startMs, l'heure
// de lancement du script (donc plus grande a chaque relance), comme
// identifiant de session :
// - sid > session courante -> NOUVELLE session : on repart de zero ;
// - sid < session courante -> copie d'une ANCIENNE session (cache) :
// on ignore completement la lecture (pas de retour en arriere) ;
// - sid == session courante -> on synchronise normalement.
// On rejette aussi une lecture intra-session qui REGRESSE en nombre
// de points alors qu'on n'a pas atteint la zone de purge (= copie
// perimee). Cette garde ne depend d'AUCUN compteur, donc elle ne peut
// jamais figer ni vider l'affichage.
//
// Synchronisation de aD une fois la lecture acceptee :
// - plus d'items -> ajoute seulement le DELTA ;
// - moins (purge legitime au-dela de MAXKEEP) -> recharge tout.
// Ne fait rien si l'utilisateur tape (pr=true).
// ============================================================
function loadData(){
  if(pr)return; // ne pas rafraichir pendant la saisie
  var old=document.getElementById('ds');if(old)old.parentNode.removeChild(old);
  var s=document.createElement('script');
  s.id='ds';s.src=DJP+'?_='+Date.now(); // aide anti-cache (best effort)
  s.onload=function(){
    if(!window.PD)return;
    var d=window.PD;
    var sid=parseInt(d.startMs,10)||0;
 
    // --- Garde de session ---
    if(sid<curSession)return; // ancienne session (cache) -> ignore
    if(sid>curSession){ // nouvelle session du script
      curSession=sid;
      aD=[];ldc=0; // repart de zero
    }
    // (sid == curSession : meme session, on continue)
 
    // --- Fraicheur (v4.77) : memorise l'heure de la derniere ecriture
    // ACCEPTEE par la garde de session. Math.max protege contre une
    // copie intra-session perimee (son wallMs, plus ancien, ne doit
    // pas faire reculer l'horloge de fraicheur).
    if(d.wallMs){lastWall=Math.max(lastWall,parseInt(d.wallMs,10)||0);}
 
    // --- Rejet d'une regression de points hors zone de purge ---
    // (une copie perimee a moins de points ; un vrai trim n'arrive
    // qu'une fois MAXKEEP atteint, donc on ne le bloque pas.)
    if(d.items.length<ldc && ldc<MAXKEEP)return;
 
    // --- Synchronisation du cache local aD ---
    if(d.items.length>=ldc){ // ajout du delta (cas normal)
      for(var i=ldc;i<d.items.length;i++){
        aD.push({t:parseInt(d.items[i].t,10),v:parseInt(d.items[i].v,10)});
      }
    } else { // purge cote script -> recharge
      aD=d.items.map(function(p){
        return {t:parseInt(p.t,10),v:parseInt(p.v,10)};
      });
    }
    ldc=d.items.length;
    if(aD.length>MAXKEEP)aD=aD.slice(aD.length-MAXKEEP); // plafond cote client
 
    // v4.79 : premiere donnee acceptee par la garde de session ->
    // l'initialisation est finie, on retire l'overlay (definitif :
    // le bandeau stale prend le relais pour la suite).
    if(aD.length>0){
      var ov=document.getElementById('initOv');
      if(ov&&ov.parentNode)ov.parentNode.removeChild(ov);
    }
 
    uu(d);
    usb();
    if(live)uc(); // un seul uc() par cycle
  };
  s.onerror=function(){};
  document.head.appendChild(s);
}
 
// --- Chien de garde de fraicheur (v4.77) ---
// Le script ecrit toutes les WriteEvery s (2 s) ; le seuil s'adapte a
// l'intervalle de ping exporte (PD.interval) pour rester valable si
// l'ecriture est conditionnee a un nouveau ping (intervalle long) :
// perime = age > max(12 s, 3 x intervalle). Meme machine -> Date.now()
// et wallMs partagent la meme horloge.
function checkStale(){
  var b=document.getElementById('staleBar');
  if(!b)return;
  if(pr){b.style.display='none';lastWall=Date.now();return;} // saisie : loadData en pause ; rebase pour eviter un flash a la reprise
  var thr=12000;
  if(window.PD&&PD.interval){thr=Math.max(thr,3*(parseInt(PD.interval,10)||0));}
  var stale=(lastWall>0)&&((Date.now()-lastWall)>thr);
  b.style.display=stale?'block':'none';
}
 
// --- Demarrage de la boucle d'affichage ---
loadData(); // premier chargement immediat
setInterval(loadData,2000); // puis toutes les 2 s
setInterval(checkStale,2000); // v4.77 : surveillance de fraicheur
startCD(); // compte a rebours du prochain ping
showInt(); // affiche l'intervalle courant
</script>
</body>
</html>
"@
 
    try { $html | Set-Content -Path $htmlFile -Encoding UTF8 -ErrorAction Stop }
    catch { Write-Host " ERREUR HTML : $_" -ForegroundColor Red }
}
 
# ============================================================
# --- MAIN ---
# ------------------------------------------------------------
# Sequence : (1) banniere console, (2) ping de test, (3) generation
# du HTML + premieres donnees, (4) ouverture navigateur (surveille),
# (5) BOUCLE de monitoring avec auto-arret si la fenetre est fermee.
# Le bloc finally garantit une derniere ecriture propre a l'arret.
# NB : $script:startMs a deja ete calcule tot (section dossier
# temporaire) car il sert a nommer les fichiers de session.
# ============================================================
 
Write-Host ""
Write-Host " Mode : $($ExecutionContext.SessionState.LanguageMode)" -ForegroundColor Cyan
Write-Host " ============================================" -ForegroundColor Cyan
Write-Host " PS-PING - Ping Monitor v4.79" -ForegroundColor White
Write-Host " Cible : $Target" -ForegroundColor Yellow
Write-Host " Intervalle: $Interval ms" -ForegroundColor Yellow
Write-Host " Rapport : $htmlFile" -ForegroundColor Gray
Write-Host " Arret : Ctrl+C (ou fermeture de la fenetre)" -ForegroundColor Red
Write-Host " ============================================" -ForegroundColor Cyan
Write-Host ""
Write-Host " Chart.js - emplacements acceptes :" -ForegroundColor Cyan
Write-Host " $scriptTemp\chartjs.min.js" -ForegroundColor Gray
Write-Host " $env:USERPROFILE\Documents\chartjs.min.js" -ForegroundColor Gray
Write-Host ""
 
# Ping de test (informatif) : le monitoring continue meme si timeout
Write-Host " Test ping $Target ..." -ForegroundColor Cyan
$testPing = Get-PingTime -TargetHost $Target
if ($testPing -ge 0) {
    Write-Host " Ping OK : $testPing ms" -ForegroundColor Green
} else {
    Write-Host " Ping : Timeout (monitoring quand meme)" -ForegroundColor Yellow
}
Write-Host ""
 
# Genere la page UNE fois, ecrit un premier jeu de donnees, ouvre le
# navigateur et RECUPERE son processus (pour l'auto-arret).
Write-HtmlOnce -target $script:Target
Write-DataJs
$script:browserProc = Open-Browser -FilePath $htmlFile
 
Write-Host " Navigateur ouvert." -ForegroundColor Green
if ($script:appMode) {
    Write-Host " Auto-arret : actif (fermez la fenetre pour stopper)" -ForegroundColor Gray
} else {
    Write-Host " Auto-arret : inactif (navigateur non surveillable)" -ForegroundColor Gray
}
Write-Host " Ctrl+C pour arreter" -ForegroundColor Gray
Write-Host ""
 
# Horloges de cadence : ping, ecriture, log et surveillance fenetre
$lastWrite = Get-Date
$lastPing = Get-Date
$lastAlive = Get-Date
$lastLog = Get-Date
$startWatch = Get-Date
$AliveEvery = 2 # s entre deux verifications fenetre (test PID = peu couteux)
$WatchGrace = 10 # grace (s) : laisse au navigateur le temps de s'installer
$LogEvery = 30 # s entre deux lignes de log console (v4.77)
$MissNeeded = 3 # mode PID : 3 echecs x 2 s = arret ~6 s apres fermeture
$MissNeededFallback = 15 # repli sans CIM : 15 x 2 s = ~30 s de confirmation
$missStreak = 0 # verifications consecutives avec fenetre absente
$wasAlive = $false # (repli) fenetre confirmee ouverte au moins une fois
$script:windowPid = 0 # PID racine de la fenetre (0 = pas encore identifie)
$pidTries = 0 # tentatives d'identification
$PidTriesMax = 15 # abandon de l'identification apres ~30 s -> mode repli
$pingTick = 0 # incremente a chaque ping (conditionne l'ecriture, v4.77)
$writtenTick = -1 # dernier pingTick ecrit dans ping_data.js
 
try {
    # ----- BOUCLE PRINCIPALE -----
    # Tourne toutes les 100 ms. Trois cadences independantes :
    # - ping : declenche tous les $script:interval ms
    # - ecriture ping_data.js : tous les $WriteEvery secondes
    # - surveillance fenetre : tous les $AliveEvery secondes
    while ($true) {
 
        # --- Surveillance de la fenetre (auto-arret v2, v4.77) ---
        # Armee seulement en mode app (sinon : pas de fenetre dediee a
        # surveiller, comme avant). Deux modes :
        # PID : le processus RACINE de la fenetre a ete identifie
        # (scan CIM unique au demarrage). Chaque verification
        # n'est qu'un Get-Process -Id ; sa disparition = la
        # fenetre est fermee. 3 echecs (~6 s) suffisent, avec
        # un RE-SCAN de confirmation par echec (si le
        # navigateur a relance son processus racine, on
        # adopte le nouveau au lieu de s'arreter a tort).
        # REPLI : CIM indisponible -> Test-BrowserOpenFallback
        # (titre OU handle ; ferme seulement si TOUS d'accord)
        # avec une confirmation longue (~30 s). Dans le doute,
        # on reste en vie - jamais l'inverse.
        if ($script:appMode) {
            $sinceAlive = ((Get-Date) - $lastAlive).TotalSeconds
            if ($sinceAlive -ge $AliveEvery -and
                ((Get-Date) - $startWatch).TotalSeconds -ge $WatchGrace) {
                $lastAlive = Get-Date
 
                # Phase d'identification : trouver le PID racine (qq essais)
                if ($script:windowPid -eq 0 -and $pidTries -lt $PidTriesMax) {
                    $pidTries++
                    $script:windowPid = Find-BrowserWindowPid -ProfileDir $profileDir
                    if ($script:windowPid -gt 0) {
                        Write-Host " Processus fenetre identifie (PID $($script:windowPid)) - auto-arret rapide arme." -ForegroundColor Green
                    } elseif ($pidTries -eq $PidTriesMax) {
                        Write-Host " Processus fenetre non identifie - surveillance de repli (arret plus lent)." -ForegroundColor Yellow
                    }
                }
 
                if ($script:windowPid -gt 0) {
                    # Mode PID : verification peu couteuse du processus racine
                    $alive = $false
                    try {
                        if (Get-Process -Id $script:windowPid -ErrorAction SilentlyContinue) { $alive = $true }
                    } catch { }
                    if ($alive) {
                        $missStreak = 0
                    } else {
                        # Re-scan par echec : adopter un racine relance s'il existe
                        $np = Find-BrowserWindowPid -ProfileDir $profileDir
                        if ($np -gt 0) {
                            $script:windowPid = $np
                            $missStreak = 0
                        } else {
                            $missStreak++
                            if ($missStreak -ge $MissNeeded) {
                                Write-Host " Fenetre fermee -> arret automatique." -ForegroundColor Yellow
                                break
                            }
                        }
                    }
                } else {
                    # Mode repli (sans CIM) : signaux titre/handle, direction sure
                    if (Test-BrowserOpenFallback) {
                        $wasAlive = $true; $missStreak = 0
                    } elseif ($wasAlive) {
                        $missStreak++
                        if ($missStreak -ge $MissNeededFallback) {
                            Write-Host " Fenetre fermee -> arret automatique." -ForegroundColor Yellow
                            break
                        }
                    }
                }
            }
        }
 
        $now = Get-Date
 
        # --- Cadence PING ---
        $sinceLastPing = ($now - $lastPing).TotalMilliseconds
        if ($sinceLastPing -ge $script:interval) {
            $val = Get-PingTime -TargetHost $script:Target # latence ou -1
            $ts = Get-NowMs
            $script:pingTs += $ts # historique timestamps
            $script:pingVs += $val # historique latences
            $script:pingCount++
            $pingTick++ # v4.77 : signale une nouvelle donnee
            $lastPing = $now
 
            # Trim allege : on laisse grossir jusqu'a MaxKeep + TrimBuffer,
            # puis on coupe d'un coup (1 reslice / TrimBuffer pings).
            # Evite un reslice O(n) a chaque ping une fois le plafond atteint.
            if ($script:pingCount -gt ($MaxKeep + $TrimBuffer)) {
                $cut = $script:pingCount - $MaxKeep
                $script:pingTs = $script:pingTs[$cut..($script:pingCount - 1)]
                $script:pingVs = $script:pingVs[$cut..($script:pingCount - 1)]
                $script:pingCount = $MaxKeep
            }
        }
 
        # --- Cadence ECRITURE + log console ---
        # v4.77 : on n'ecrit que s'il y a une NOUVELLE donnee depuis la
        # derniere ecriture (pingTick). Si $Interval > $WriteEvery, plus
        # d'ecritures dupliquees du meme contenu.
        $sinceLW = ($now - $lastWrite).TotalSeconds
        if ($pingTick -ne $writtenTick -and $sinceLW -ge $WriteEvery) {
            Write-DataJs # rafraichit le fichier lu par le navigateur
            $lastWrite = $now
            $writtenTick = $pingTick
 
            # Ligne de log console synthetique - v4.77 : une toutes les
            # 30 s au lieu d'une par ecriture (sinon le transcript de
            # diagnostic gonfle d'une ligne toutes les 2 s).
            if (($now - $lastLog).TotalSeconds -ge $LogEvery) {
                $lastLog = $now
                $st = Calc-Stats
                $elapsed = Get-ElapsedSec
                $hh = [int]($elapsed / 3600)
                $mm = [int](($elapsed % 3600) / 60)
                $ss = $elapsed % 60
                $lv = if ($script:pingCount -gt 0 -and $script:pingVs[$script:pingCount-1] -ge 0) {
                              "$($script:pingVs[$script:pingCount-1]) ms"
                           } else { "Timeout" }
                $col = if ($lv -ne "Timeout") { "Green" } else { "Red" }
 
                Write-Host (" [{0:D2}h{1:D2}m{2:D2}s] {3,-12} | Total:{4,5} | Perte:{5,3}% | Int:{6}ms" -f `
                    $hh, $mm, $ss, $lv, $script:pingCount, $st.loss, $script:interval) -ForegroundColor $col
            }
        }
 
        Start-Sleep -Milliseconds 100 # respiration : ~10 boucles/s (CPU bas)
    }
}
finally {
    # Execute meme sur Ctrl+C : derniere ecriture pour que le rapport
    # reflete l'etat final, puis rappel du chemin du fichier.
    Write-Host ""
    Write-Host " Arret..." -ForegroundColor Yellow
    Write-DataJs
    Write-Host " Rapport : $htmlFile" -ForegroundColor Gray
    # v4.77 : ferme NOTRE fenetre si elle est encore ouverte (ex. arret
    # par Ctrl+C en mode console visible), pour ne rien laisser
    # d'orphelin. PID connu d'abord (gratuit) ; balayage CIM sur notre
    # profil dedie en secours (enfants / PID inconnu). Si l'arret vient
    # de la fermeture de la fenetre, ces appels ne trouvent rien : sans
    # effet, silencieux.
    if ($script:windowPid -gt 0) {
        Stop-Process -Id $script:windowPid -Force -ErrorAction SilentlyContinue
    }
    try {
        Get-CimInstance Win32_Process -Filter "Name='msedge.exe' OR Name='chrome.exe'" -ErrorAction Stop |
            Where-Object { $_.CommandLine -like "*$profileDir*" } |
            ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
    } catch { }
    Write-Host " Arrete." -ForegroundColor Green
    Write-Host ""
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
}