**
EDIT: 04-08-26 (passage en V2).
** Code revu, amélioré et plus rapide. Interface améliorée.
Bonjour,
J’ai un paquet d’esp avec espeasy chez moi et je remarque que parfois, sans qu’on ne sache pourquoi le wifi devient instable. De fait, l’esp reste en actif mais hors ligne. Pour ceux qui sont accessible physiquement, pas de soucis, mais pour les autres cela peut vite devenir la misère. J’en avais marre de bricoler pour accéder un esp afin de le relancer. J’ai donc chercher une solution pour que cela devienne automatique.
Ce n’est peut être pas la meilleure solution, programmée de la meilleure manière car je ne suis pas un spécialiste du PHP, elle existe peut-être déjà, mais ça fonctionne !
Il suffit de copier le code dans un script nommé espeasy.php (via le plugins script) et de faire les modifications en insérant le code indiqué dans les rules de vos esp.
Cela permet aux esp de faire un « ping » vers le script et le script renvoie un « pong » vers l’esp. Si rien n’est reçu après x sec, l’esp reboot.
<?php
// (c) Lenif -- V2.7 (04-08-2026)
// Keepalive script pour espeasy. Si network hors ligne pour x sec, espeasy reboot
//
// utilisation: placer ce code dans un script jeedom (ou ailleurs).
// http://ipserver/espeasy.php?mode=view pour afficher les espeasy
// http://ipserver/espeasy.php pour afficher l'ip du client
// Accès depuis jeedom:
// http://ipdejeedom/plugins/script/core/ressources/espeasy.php?mode=view
//
//
// Compatible avec le plugin CLINK.
// !!! Non compatible avec le bouton TESTER du plugin script !!!
//
//
// Dans l'exemple ci-dessous, un accès est réalisé toutes les deux minutes(120) vers le script sur Jeedom
// Si le script n'a rien envoyé après 1800s (30 minutes), espeasy reboot
//
//
// *******************************
// modification dans votre espeasy
// ajouter ceci dans les rules:
//
/*
on system#boot do
timerset,7,10
endon
on Rules#Timer=7 do //envoie un ping toutes les 120sec
SendToHTTP IP_JEEDOM,80,/plugins/script/core/ressources/espeasy.php?mode=update
timerset,7,120
endon
on Pong do // pong receive
timerset,8,1800
endon
on Rules#Timer=8 do // no pong, reboot
reboot
endon
*/
date_default_timezone_set('Europe/Brussels');
$space = ' * ';
$page = strtok("http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]", "?");
$savefile = "espList.json";
$sec = "10";
$Pong = '/control?cmd=event,Pong';
$mode = isset($_GET["mode"]) ? $_GET["mode"] : '';
$espIP = $_SERVER['REMOTE_ADDR'];
$espDateTime = date('d-m-Y H:i:s');
$nowTimestamp = time();
$todayStart = strtotime('today midnight'); // Début de la journée courante
function espFindValue($data, array $candidates, $default = 'N/A')
{
if (!is_array($data) && !is_object($data)) {
return $default;
}
$data = (array) $data;
foreach ($candidates as $key) {
foreach ($data as $k => $v) {
if (strcasecmp($k, $key) === 0 && !is_array($v) && !is_object($v)) {
return $v;
}
}
}
foreach ($data as $v) {
if (is_array($v) || is_object($v)) {
$result = espFindValue($v, $candidates, null);
if ($result !== null) {
return $result;
}
}
}
return $default;
}
function espSignalIcon($rssiStr)
{
if (strpos($rssiStr, 'N/A') !== false) {
return '<span style="color:#717597; font-weight: bold;">N/A</span>';
}
$num = intval(preg_replace('/[^0-9\-]/', '', $rssiStr));
if ($num >= -55) { $level = 4; $color = '#2e7d32'; }
elseif ($num >= -65) { $level = 3; $color = '#558b2f'; }
elseif ($num >= -75) { $level = 2; $color = '#ef6c00'; }
elseif ($num >= -85) { $level = 1; $color = '#d84315'; }
else { $level = 0; $color = '#c62828'; }
$heights = [3, 6, 9, 12];
$bars = '';
for ($i = 0; $i < 4; $i++) {
$h = $heights[$i];
$fill = ($i < $level) ? $color : '#c5c8e3';
$x = 1 + $i * 4;
$y = 13 - $h;
$bars .= '<rect x="'.$x.'" y="'.$y.'" width="3" height="'.$h.'" fill="'.$fill.'"></rect>';
}
return '<span title="'.htmlspecialchars($rssiStr, ENT_QUOTES, 'UTF-8').'"><svg width="15" height="13" viewBox="0 0 17 14">'.$bars.'</svg></span>';
}
// envoyer tout de suite le pong
if ($mode == 'update'){
echo 'Send back pong to:</br>';
$PongURL = 'http://'.$espIP.$Pong;
echo htmlspecialchars($PongURL, ENT_QUOTES, 'UTF-8');
$cURL = curl_init($PongURL);
curl_setopt($cURL, CURLOPT_PORT, 80);
curl_setopt($cURL, CURLOPT_CONNECTTIMEOUT, 1);
curl_setopt($cURL, CURLOPT_RETURNTRANSFER, true);
$PongResult = curl_exec($cURL);
if (strstr($PongResult, "OK"))
{
$espPong = 'OK';
}
else
{
$espPong = 'NOK';
}
echo '<p>'.$espPong.'</p>';
// Interrogation JSON uniquement lors d'un UPDATE
$cURL = curl_init('http://'.$espIP.'/json');
curl_setopt($cURL, CURLOPT_PORT, 80);
curl_setopt($cURL, CURLOPT_CONNECTTIMEOUT, 2);
curl_setopt($cURL, CURLOPT_RETURNTRANSFER, true);
$espJSON = json_decode(curl_exec($cURL));
if (is_null($espJSON))
{
$espName = 'N/A';
$espBuild = 'N/A';
$espSSID = 'N/A';
$espRSSI = 'N/A';
$espUptime = 0;
} else
{
$espName = espFindValue($espJSON, ['Unit Name', 'Hostname', 'Name']);
$espBuild = espFindValue($espJSON, ['Git Build'], '');
if ($espBuild === '' || $espBuild === null) {
$espBuild = espFindValue($espJSON, ['Build'], 'N/A');
}
$espSSID = espFindValue($espJSON, ['SSID', 'STA SSID']);
$espRSSI = espFindValue($espJSON, ['RSSI'], null);
$espRSSI = ($espRSSI !== null) ? $espRSSI.'db' : 'N/A';
$espUptime = espFindValue($espJSON, ['Uptime'], 0);
}
}
// charge le contenu du fichier
$espList = json_decode(@file_get_contents($savefile), true);
if (!is_array($espList))
{
$espList = array();
}
if (($mode == 'delete') and isset($_GET['ip'])){
$ipToDelete = str_replace("-",".",$_GET['ip']);
echo '<head><meta http-equiv="refresh" content="4;'.$page.'?mode=view"></head>';
foreach ($espList as $index => $esp)
{
if ($esp[0] == $ipToDelete) {
echo "Je supprime: ".htmlspecialchars($ipToDelete, ENT_QUOTES, 'UTF-8');
array_splice($espList, $index, 1);
$result = file_put_contents($savefile,json_encode($espList),LOCK_EX);
echo "<p>Modification enregistrée</p>";
exit();
}
}
echo "Pas trouvé !";
}
elseif ($mode == 'update')
{
$replaced = False;
foreach ($espList as $index => $esp)
{
if ($esp[0] == $espIP)
{
$espList[$index][1] = $espDateTime;
if ($espName != 'N/A') { $espList[$index][2] = $espName; }
$espList[$index][3] = $espBuild;
$espList[$index][4] = $espSSID;
$espList[$index][5] = $espRSSI;
$espList[$index][6] = $espUptime;
$espList[$index][7] = $espPong;
$replaced = true;
break;
}
}
if (!$replaced){
array_push($espList, array($espIP, $espDateTime, $espName, $espBuild, $espSSID, $espRSSI, $espUptime, $espPong));
} else
{
echo 'Update date/time for '.htmlspecialchars($espIP, ENT_QUOTES, 'UTF-8').'</br>';
}
$result = file_put_contents($savefile,json_encode($espList),LOCK_EX);
} elseif ($mode == 'clear')
{
echo '<head><meta http-equiv="refresh" content="2;'.$page.'?mode=view"></head>';
if (!unlink($savefile)) {
echo ("Erreur lors de l'effacement du fichier ".$savefile);
}
else {
echo ("Fichier supprimé");
}
} elseif ($mode == 'view') {
if (empty($espList))
{
$html = '<p>Vide</p>';
} else
{
$sortID = isset($_GET['sort']) ? (int)$_GET['sort'] : 0;
$sortDir = (isset($_GET['dir']) && strtolower($_GET['dir']) === 'desc') ? 'desc' : 'asc';
// Tri des colonnes (ASC / DESC)
if ($sortID == 0) {
usort($espList, function ($a, $b) use ($sortDir)
{
$aip = sprintf('%u', ip2long($a[0]));
$bip = sprintf('%u', ip2long($b[0]));
return ($sortDir === 'asc') ? ($aip <=> $bip) : ($bip <=> $aip);
});
} else {
$columns = array_column($espList, $sortID);
$dirFlag = ($sortDir === 'asc') ? SORT_ASC : SORT_DESC;
array_multisort($columns, $dirFlag, SORT_NATURAL|SORT_FLAG_CASE, $espList);
}
$totalCount = count($espList);
$offlineCount = 0;
// Calcul unifié du nombre d'appareils hors-ligne
foreach ($espList as $row) {
$ts = strtotime($row[1]);
if ($ts === false || (($nowTimestamp - $ts) / 60) > 10) {
$offlineCount++;
}
}
$onlineCount = $totalCount - $offlineCount;
// Génération des liens d'en-tête de tri
$getSortLink = function($colId, $label) use ($page, $sortID, $sortDir) {
if ($sortID == $colId) {
$nextDir = ($sortDir === 'asc') ? 'desc' : 'asc';
$arrow = ($sortDir === 'asc') ? ' ▲' : ' ▼'; // ▲ / ▼
return '<a href="'.$page.'?mode=view&sort='.$colId.'&dir='.$nextDir.'">'.$label.$arrow.'</a>';
} else {
return '<a href="'.$page.'?mode=view&sort='.$colId.'&dir=asc">'.$label.'</a>';
}
};
$html = '<html>
<head>
<meta http-equiv="refresh" content="10">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;600&family=Roboto:wght@500;700&display=swap" rel="stylesheet">
<style>
body {
font-family: "Roboto", sans-serif;
color: #1f212d;
margin: 0;
padding: 10px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.espContainer {
max-width: 1000px;
margin: 0 auto;
}
.espAlertBanner {
background-color: #c62828;
color: #ffffff;
padding: 8px 14px;
border-radius: 6px;
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 700;
font-size: 13.5px;
box-shadow: 0 4px 10px rgba(198, 40, 40, 0.3);
animation: alertPulse 2s infinite;
}
.espAlertBanner span.alertText {
display: flex;
align-items: center;
gap: 8px;
}
.espAlertBanner button {
background: #ffffff;
border: none;
color: #c62828;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 700;
}
@keyframes alertPulse {
0% { box-shadow: 0 0 0 0 rgba(198, 40, 40, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(198, 40, 40, 0); }
100% { box-shadow: 0 0 0 0 rgba(198, 40, 40, 0); }
}
.espHeader {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 12px;
}
.espBadge {
background: #3f4785;
color: #ffffff;
font-size: 13px;
font-weight: 700;
padding: 6px 10px;
border-radius: 5px;
white-space: nowrap;
font-family: "Fira Code", monospace;
}
.espSearchBox {
position: relative;
}
#espSearch {
width: 250px;
max-width: 60vw;
padding: 6px 30px 6px 10px;
border: 1.5px solid #5960ab;
border-radius: 5px;
font-family: "Roboto", sans-serif;
font-size: 13px;
font-weight: 500;
color: #1f212d;
outline: none;
}
#espClearSearch {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #5960ab;
font-size: 18px;
font-weight: bold;
line-height: 1;
visibility: hidden;
}
.espTableWrap {
overflow-x: auto;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(37, 42, 94, 0.15);
}
table {
border-collapse: collapse;
width: 100%;
min-width: 680px;
font-family: "Fira Code", monospace;
font-size: 12px;
font-weight: 400;
background: #ffffff;
letter-spacing: -0.3px;
}
tr.enhaut {
background-color: #3f4785;
color: #ffffff;
font-family: "Roboto", sans-serif;
font-size: 13px;
}
th {
padding: 8px 6px;
border-bottom: 2px solid #252a5e;
border-right: 1px solid rgba(255, 255, 255, 0.15);
white-space: nowrap;
font-weight: 700;
}
tfoot {
text-align: center;
font-family: "Roboto", sans-serif;
}
tr.offline { background: #ffcdd2; color: #b71c1c; font-weight: 600; } /* ROUGE : Critique / Hors ligne (>10 min) */
tr.gray { background: #cfd8dc; color: #37474f; font-weight: 600; } /* GRIS : Mineur / Pong NOK */
tr.yellow { background: #fff59d; color: #7f6000; font-weight: 600; } /* JAUNE : Wifi faible */
tr.orange { background: #ffe0b2; color: #e65100; font-weight: 600; } /* ORANGE : Reboot récent */
tr.green { background: #e8f5e9; color: #1b5e20; } /* VERT : Normal */
tr.enbas {
background: #5960ab;
color: #ffffff;
font-weight: 700;
}
tr.enbas td {
padding: 0;
}
tr.enbas td.clickableCell {
cursor: pointer;
padding: 8px;
}
tr.enbas td.clickableCell:hover {
background: #3f4785;
}
tr.enbas a {
text-decoration: none;
color: white;
display: block;
padding: 8px;
width: 100%;
box-sizing: border-box;
}
tr.enbas a:hover {
background: #3f4785;
}
td {
padding: 5px 6px;
text-align: left;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
border-right: 1px solid rgba(0, 0, 0, 0.05);
white-space: nowrap;
}
td[colspan="10"] {
text-align: center;
}
tbody tr:hover td {
filter: brightness(0.93);
}
th a:link, th a:visited {
text-decoration: none;
color: #ffffff;
}
td.lien {
text-align: center;
}
td.lien a {
color: #c62828;
display: inline-flex;
padding: 4px;
border-radius: 4px;
}
td.lien a:hover {
color: #ffffff;
background: #c62828;
}
tbody tr {
cursor: pointer;
}
td.colCenter, th.colCenter {
text-align: center;
}
.statusIcon {
display: inline-flex;
align-items: center;
justify-content: center;
}
.pongOk {
color: #2e7d32;
font-weight: 900;
}
.pongNok {
color: #546e7a;
font-weight: 900;
}
.espLegendRow {
display: none;
}
@media (max-width: 600px) {
.espHeader { flex-direction: column; align-items: flex-start; }
#espSearch { width: 100%; max-width: none; }
}
</style>
</head>
<body>';
$html .= '<div class="espContainer">';
// Bandeau d'alerte en haut si au moins un appareil est hors ligne
if ($offlineCount > 0) {
$pluralText = ($offlineCount > 1) ? $offlineCount.' appareils ne répondent plus' : '1 appareil ne répond plus';
$html .= '<div class="espAlertBanner">';
$html .= '<span class="alertText">';
$html .= '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>';
$html .= 'Attention : '.$pluralText.' !';
$html .= '</span>';
$html .= '<button onclick="espFilterOfflineOnly()">Filtrer les hors ligne</button>';
$html .= '</div>';
}
$html .= '<div class="espHeader">';
$html .= '<div class="espSearchBox">';
$html .= '<input type="text" id="espSearch" placeholder="Rechercher..." onkeyup="espFilterTable()">';
$html .= '<span id="espClearSearch" onclick="espClearFilter()">×</span>';
$html .= '</div>';
$html .= '<span id="espCountBadge" class="espBadge" data-online="'.$onlineCount.'" data-total="'.$totalCount.'">('.$onlineCount.'/'.$totalCount.')</span>';
$html .= '</div>';
$html .= '<div class="espTableWrap">';
$html .= '<table>';
$html .= '<thead><tr class="enhaut">';
$html .= '<th class="colCenter" style="width: 28px;">État</th>';
$html .= '<th>'.$getSortLink(0, 'IP').'</th>';
$html .= '<th>'.$getSortLink(1, 'Date').'</th>';
$html .= '<th>'.$getSortLink(2, 'Nom').'</th>';
$html .= '<th>'.$getSortLink(3, 'Build').'</th>';
$html .= '<th>'.$getSortLink(4, 'SSID').'</th>';
$html .= '<th>'.$getSortLink(5, 'RSSI').'</th>';
$html .= '<th>'.$getSortLink(6, 'UpTime').'</th>';
$html .= '<th class="colCenter">'.$getSortLink(7, 'Pong').'</th>';
$html .= '<th>Sup</th>';
$html .= '</tr></thead><tbody>';
foreach($espList as $rowkey=>$row)
{
$ts = strtotime($row[1]);
$timeDif = ($ts === false) ? 999 : ($nowTimestamp - $ts) / 60;
// Choix de la classe ET de l'icône selon la vraie gravité
if ($timeDif > 10) {
$rowClass = 'offline';
$statusIcon = '<span class="statusIcon" title="Hors ligne (>10 min)"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#b71c1c" stroke-width="2.5" stroke-linecap="round"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path><line x1="12" y1="2" x2="12" y2="12"></line></svg></span>';
} elseif ($row[7] == 'NOK') {
$rowClass = 'gray';
$statusIcon = '<span class="statusIcon" title="ESP n\'a pas reçu le PONG"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#455a64" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>';
} elseif (preg_replace('/[^0-9]/', '', $row[5]) > 75) {
$rowClass = 'yellow';
$statusIcon = '<span class="statusIcon" title="Signal wifi faible (1/5)"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke-linecap="round"><path d="M1.42 9a15.91 15.91 0 0 1 21.16 0" stroke="#b0bec5" stroke-width="2"></path><path d="M5 12.55a10.94 10.94 0 0 1 14 0" stroke="#b0bec5" stroke-width="2"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0" stroke="#b0bec5" stroke-width="2"></path><circle cx="12" cy="20" r="1.5" fill="#f57f17"></circle></svg></span>';
} elseif (preg_replace('/[^0-9]/', '', $row[6]) < 100) {
$rowClass = 'orange';
$statusIcon = '<span class="statusIcon" title="ESP a booté récemment"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#e65100" stroke-width="2.5" stroke-linecap="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg></span>';
} else {
$rowClass = 'green';
$statusIcon = '<span class="statusIcon" style="color:#2e7d32; font-weight:bold;" title="Fonctionnement normal">✓</span>';
}
$rowIP = htmlspecialchars($row[0], ENT_QUOTES, 'UTF-8');
$html .= '<tr class="'.$rowClass.'" onclick="if(event.target.closest(\'a\'))return;window.open(\'http://'.$rowIP.'\',\'_blank\')">';
$html .= '<td class="colCenter">'.$statusIcon.'</td>';
foreach ($row as $cellkey=>$cell) {
if ($cellkey == 1) {
if ($ts !== false) {
if ($ts >= $todayStart) {
$displayDate = date('H:i:s', $ts);
} else {
$displayDate = date('d/m H:i:s', $ts);
}
} else {
$displayDate = htmlspecialchars((string)$cell, ENT_QUOTES, 'UTF-8');
}
$html .= '<td>'.$displayDate.'</td>';
} elseif ($cellkey == 5) {
$html .= '<td class="colCenter">'.espSignalIcon($cell).'</td>';
} elseif ($cellkey == 6) {
$d = floor ($cell / 1440);
$h = floor (($cell - $d * 1440) / 60);
if ($h < 10) { $h = '0'.$h; }
$m = $cell - ($d * 1440) - ($h * 60);
if ($m < 10) { $m = '0'.$m;}
if ($d == 0) {
$espUptimeText = $h.':'.$m;
} else {
$espUptimeText = $d.'j. '.$h.':'.$m;
}
$html .= '<td>'.$espUptimeText.'</td>';
} elseif ($cellkey == 7) {
if ($cell == 'OK') {
$html .= '<td class="colCenter"><span class="pongOk">✓</span></td>';
} elseif ($cell == 'NOK') {
$html .= '<td class="colCenter"><span class="pongNok">✗</span></td>';
} else {
$html .= '<td class="colCenter">—</td>';
}
} else {
$html .= '<td>'.htmlspecialchars((string)$cell, ENT_QUOTES, 'UTF-8').'</td>';
}
}
$html .= '<td class="lien">';
$html .= '<a href="'.$page.'?mode=delete&ip='.urlencode(str_replace(".","-",$row[0])).'" title="Supprimer">';
$html .= '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"></path></svg>';
$html .= '</a>';
$html .= '</td></tr>';
}
$html .= '</tbody>';
$html .= '<tfoot>';
$html .= '<tr class="offline espLegendRow"><td colspan="10">Hors ligne (>10 min)</td></tr>';
$html .= '<tr class="gray espLegendRow"><td colspan="10">ESP n\'a pas reçu le PONG</td></tr>';
$html .= '<tr class="yellow espLegendRow"><td colspan="10">Signal wifi faible</td></tr>';
$html .= '<tr class="orange espLegendRow"><td colspan="10">ESP a booté récemment</td></tr>';
$html .= '<tr class="green espLegendRow"><td colspan="10">ESP normal</td></tr>';
$html .= '<tr class="enbas"><td colspan="10" class="clickableCell" onclick="espToggleLegend()"><span id="espLegendText">Afficher la légende</span></td></tr>';
$html .= '<tr class="enbas"><td colspan="10"><a href="'.$page.'?mode=clear">Supprimer l\'historique</a></td></tr>';
$html .= '</tfoot>';
$html .= '</table>';
$html .= '</div>';
$html .= '</div>';
$html .= '<script>
function espApplyLegendState(show) {
var rows = document.querySelectorAll(".espLegendRow");
var txt = document.getElementById("espLegendText");
rows.forEach(function(r) { r.style.display = show ? "table-row" : "none"; });
if (txt) {
txt.textContent = show ? "Masquer la légende" : "Afficher la légende";
}
}
function espToggleLegend() {
var rows = document.querySelectorAll(".espLegendRow");
var isShowing = rows.length > 0 && rows[0].style.display === "table-row";
var newState = !isShowing;
espApplyLegendState(newState);
sessionStorage.setItem("espLegendOpen", newState ? "true" : "false");
}
function espApplyFilter(query) {
var rows = document.querySelectorAll("table tbody tr");
var lower = query.toLowerCase();
var visibleCount = 0;
rows.forEach(function(row) {
var isOfflineOnly = (lower === ":offline");
var match = isOfflineOnly ? row.classList.contains("offline") : (row.textContent.toLowerCase().indexOf(lower) !== -1);
row.style.display = match ? "" : "none";
if (match) visibleCount++;
});
var badge = document.getElementById("espCountBadge");
if (query.length > 0) {
badge.textContent = "(" + visibleCount + ")";
} else {
badge.textContent = "(" + badge.dataset.online + "/" + badge.dataset.total + ")";
}
var clearBtn = document.getElementById("espClearSearch");
clearBtn.style.visibility = query.length > 0 ? "visible" : "hidden";
}
function espFilterTable() {
var input = document.getElementById("espSearch");
sessionStorage.setItem("espSearchQuery", input.value);
espApplyFilter(input.value);
}
function espClearFilter() {
var input = document.getElementById("espSearch");
input.value = "";
sessionStorage.removeItem("espSearchQuery");
espApplyFilter("");
input.focus();
}
function espFilterOfflineOnly() {
var input = document.getElementById("espSearch");
input.value = ":offline";
espFilterTable();
}
document.addEventListener("DOMContentLoaded", function() {
// Restauration du filtre
var savedSearch = sessionStorage.getItem("espSearchQuery");
if (savedSearch) {
document.getElementById("espSearch").value = savedSearch;
espApplyFilter(savedSearch);
}
// Restauration de l\'état de la légende
var savedLegend = sessionStorage.getItem("espLegendOpen");
if (savedLegend === "true") {
espApplyLegendState(true);
}
});
</script>';
$html .= '</body></html>';
}
echo $html;
} else
{
echo htmlspecialchars($espIP, ENT_QUOTES, 'UTF-8');
}
?>
a+
Aperçu des esp en ligne:






