Add VIO FC Status monitoring page and enhance server inventory filtering
- Introduced `Storage/VIO.php` for monitoring VIO Fibre Channel status with client balancing checks and table sorting. - Updated `/include/en.php` and `/include/fr.php` to include new translations for ServiceNow and VIO-related navigation elements. - Improved SQL query and filtering logic in `/X/Inventory.php` to exclude VIO entries and add XenSam version data. - Enhanced `/reports/heartbeat.php` with a new mechanism for handling server heartbeat status, allowing better granularity and readability.
This commit is contained in:
450
reports/Cluster_Compliance.php
Normal file
450
reports/Cluster_Compliance.php
Normal file
@@ -0,0 +1,450 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<title>Web Infra Reports OT</title>
|
||||
|
||||
<script src="/js/jquery-3.6.1.min.js"></script>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/bootstrap-icons/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="/css/preloader.css">
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<link rel="stylesheet" href="/css/bootstrap-table.min.css">
|
||||
<script src="/js/bootstrap-table.min.js"></script>
|
||||
<script src="/js/bootstrap-table-fr-FR.min.js"></script>
|
||||
<script src="/js/tableExport.min.js"></script>
|
||||
<script src="/js/bootstrap-table-export.min.js"></script>
|
||||
<script src="/js/libs/js-xlsx/xlsx.core.min.js"></script>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-row {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.left-menu {
|
||||
flex: 0 0 230px;
|
||||
height: 100vh;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.right-content {
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.sticky-dashboard-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: .75rem;
|
||||
}
|
||||
|
||||
.kpi-card { cursor: pointer; transition: transform .1s ease-in-out; }
|
||||
.kpi-card:hover { transform: translateY(-2px); }
|
||||
code.sig { font-size: .85em; }
|
||||
.table td { vertical-align: middle; }
|
||||
|
||||
/* Chevron rotation for the collapsible unreachable list */
|
||||
.collapse-toggle .bi-chevron-down { transition: transform .15s ease-in-out; }
|
||||
.collapse-toggle[aria-expanded="true"] .bi-chevron-down { transform: rotate(180deg); }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<?php include $_SERVER['DOCUMENT_ROOT']."/include/all.php" ; ?>
|
||||
<div class="container-fluid">
|
||||
<div class="row flex-nowrap main-row">
|
||||
<div class="col-auto px-sm-2 px-0 bg-dark left-menu">
|
||||
<?php include $_SERVER['DOCUMENT_ROOT']."/navbar.html" ; ?>
|
||||
</div>
|
||||
<div class="col right-content">
|
||||
<?php
|
||||
// -----------------------------------------------------------
|
||||
// Read the latest run from the compliance results table
|
||||
//
|
||||
// Cluster conformity is now keyed on the BUILD (BuildDrift):
|
||||
// two nodes sharing the same build but a different UBR/KB are
|
||||
// OK at cluster level. The UBR gap is kept as a severity /
|
||||
// priority indicator. Hardware facts (BIOS + model) are read
|
||||
// for display and light hardware-drift flagging.
|
||||
// -----------------------------------------------------------
|
||||
$conn = DB_INFRA();
|
||||
|
||||
$sql = "SELECT CONVERT(VARCHAR(36), RunId) AS RunId, ClusterName, NodeName, OsName,
|
||||
MsPatchLevel, MsBuild, MsUbr, UbrGap, LastHotfix,
|
||||
CONVERT(VARCHAR(10), LastHotfixOn, 120) AS LastHotfixOn,
|
||||
SppSignature,
|
||||
BiosVersion, CONVERT(VARCHAR(10), BiosDate, 120) AS BiosDate,
|
||||
ServerModel, Manufacturer,
|
||||
Reachable, MsDrift, BuildDrift, SppDrift, ErrorMessage,
|
||||
CONVERT(VARCHAR(16), RunDate, 120) AS RunDate
|
||||
FROM dbo.ClusterComplianceResults
|
||||
WHERE RunId = (SELECT TOP 1 RunId FROM dbo.ClusterComplianceResults ORDER BY RunDate DESC)
|
||||
AND (
|
||||
Reachable = 0
|
||||
OR ClusterName IN (
|
||||
SELECT ClusterName
|
||||
FROM dbo.ClusterComplianceResults
|
||||
WHERE RunId = (SELECT TOP 1 RunId FROM dbo.ClusterComplianceResults ORDER BY RunDate DESC)
|
||||
AND Reachable = 1
|
||||
GROUP BY ClusterName
|
||||
HAVING COUNT(DISTINCT NodeName) > 1
|
||||
)
|
||||
)
|
||||
ORDER BY ClusterName, NodeName";
|
||||
$rs = odbc_exec($conn, $sql);
|
||||
|
||||
$runId = "";
|
||||
$runDate = "-";
|
||||
$clusters = array();
|
||||
|
||||
if ($rs) {
|
||||
while ($row = odbc_fetch_array($rs)) {
|
||||
if ($runId === "") { $runId = $row['RunId']; $runDate = $row['RunDate']; }
|
||||
$c = $row['ClusterName'];
|
||||
if (!isset($clusters[$c])) {
|
||||
$clusters[$c] = array(
|
||||
'name'=>$c, 'nodes'=>array(),
|
||||
'build'=>false, 'ms'=>false, 'spp'=>false,
|
||||
'unreach'=>false, 'ubrgap'=>0
|
||||
);
|
||||
}
|
||||
$clusters[$c]['nodes'][] = $row;
|
||||
// Conformity fail = build drift. MS (Build.UBR) kept as info only.
|
||||
if ($row['BuildDrift'] == 1) { $clusters[$c]['build'] = true; }
|
||||
if ($row['MsDrift'] == 1) { $clusters[$c]['ms'] = true; }
|
||||
if ($row['SppDrift'] == 1) { $clusters[$c]['spp'] = true; }
|
||||
if ($row['Reachable'] == 0) { $clusters[$c]['unreach'] = true; }
|
||||
// UbrGap is stored identically on every node row of the run.
|
||||
$clusters[$c]['ubrgap'] = max($clusters[$c]['ubrgap'], (int)$row['UbrGap']);
|
||||
}
|
||||
}
|
||||
|
||||
// Classify each cluster: DRIFT > UNREACHABLE > OK
|
||||
// DRIFT is now build-drift OR SPP-drift (NOT a pure UBR/KB gap).
|
||||
$ok = array(); $drift = array(); $unreach = array(); $ubrgap_list = array();
|
||||
foreach ($clusters as $name => $cl) {
|
||||
if ($cl['build'] || $cl['spp']) {
|
||||
$clusters[$name]['status'] = 'DRIFT';
|
||||
if (count($cl['nodes']) > 1) {
|
||||
$drift[] = $name;
|
||||
}
|
||||
} elseif ($cl['unreach']) {
|
||||
$clusters[$name]['status'] = 'UNREACHABLE'; $unreach[] = $name;
|
||||
} else {
|
||||
if (count($cl['nodes']) > 1){
|
||||
// Build + SPP aligned and fully reachable. A non-zero
|
||||
// UBR gap means "conform at cluster level, but KB behind"
|
||||
// -> isolated in its own priority list, out of the OK set.
|
||||
if ((int)$clusters[$name]['ubrgap'] > 0) {
|
||||
$clusters[$name]['status'] = 'UBR_GAP';
|
||||
$ubrgap_list[] = $name;
|
||||
} else {
|
||||
$clusters[$name]['status'] = 'OK';
|
||||
$ok[] = $name;
|
||||
}
|
||||
} else {
|
||||
$clusters[$name]['status'] = 'OK';
|
||||
}
|
||||
}
|
||||
}
|
||||
$count_ok = count($ok);
|
||||
$count_drift = count($drift);
|
||||
$count_unreach = count($unreach);
|
||||
$count_ubrgap = count($ubrgap_list);
|
||||
|
||||
// Biggest gaps first, so the priority list is correct regardless
|
||||
// of any client-side sorting applied afterwards.
|
||||
usort($ubrgap_list, function($a, $b) use ($clusters) {
|
||||
return (int)$clusters[$b]['ubrgap'] <=> (int)$clusters[$a]['ubrgap'];
|
||||
});
|
||||
|
||||
// Per-cluster summary (distinct MS levels, builds, SPP signatures,
|
||||
// hardware facts, unreachable nodes, errors).
|
||||
function cluster_summary($cl) {
|
||||
$ms = array(); $builds = array(); $spp = array();
|
||||
$models = array(); $bios = array(); $biosdates = array();
|
||||
$unodes = array(); $errs = array();
|
||||
foreach ($cl['nodes'] as $n) {
|
||||
if ($n['Reachable'] == 1) {
|
||||
if ($n['MsPatchLevel'] !== '' && $n['MsPatchLevel'] !== null) { $ms[] = $n['MsPatchLevel']; }
|
||||
if ($n['MsBuild'] !== '' && $n['MsBuild'] !== null) { $builds[] = $n['MsBuild']; }
|
||||
if ($n['SppSignature'] !== '' && $n['SppSignature'] !== null) { $spp[] = $n['SppSignature']; }
|
||||
if ($n['ServerModel'] !== '' && $n['ServerModel'] !== null) { $models[] = $n['ServerModel']; }
|
||||
if ($n['BiosVersion'] !== '' && $n['BiosVersion'] !== null) { $bios[] = $n['BiosVersion']; }
|
||||
if ($n['BiosDate'] !== '' && $n['BiosDate'] !== null) { $biosdates[] = $n['BiosDate']; }
|
||||
} else {
|
||||
$unodes[] = $n['NodeName'];
|
||||
if (!empty($n['ErrorMessage'])) { $errs[] = $n['ErrorMessage']; }
|
||||
}
|
||||
}
|
||||
$bios_u = array_values(array_unique($bios));
|
||||
return array(
|
||||
'ms' => array_values(array_unique($ms)),
|
||||
'builds' => array_values(array_unique($builds)),
|
||||
'spp' => array_values(array_unique($spp)),
|
||||
'models' => array_values(array_unique($models)),
|
||||
'bios' => $bios_u,
|
||||
'biosdates' => array_values(array_unique($biosdates)),
|
||||
'bios_drift' => (count($bios_u) > 1),
|
||||
'ubrgap' => (int)$cl['ubrgap'],
|
||||
'unodes' => $unodes,
|
||||
'errs' => array_values(array_unique($errs)),
|
||||
'nodecount' => count($cl['nodes'])
|
||||
);
|
||||
}
|
||||
|
||||
// Small helper: a coloured badge for the UBR gap value.
|
||||
function ubrgap_badge($gap) {
|
||||
$gap = (int)$gap;
|
||||
if ($gap <= 0) { return "<span class='badge text-bg-light text-muted'>0</span>"; }
|
||||
$cls = ($gap >= 100) ? 'text-bg-danger' : 'text-bg-info';
|
||||
return "<span class='badge $cls'>".$gap."</span>";
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="sticky-dashboard-header bg-light">
|
||||
<h1>
|
||||
<span class="badge text-bg-secondary" style="width:100%;">
|
||||
Conformité des Clusters — Build MS / SPP HP / Matériel
|
||||
<small><small>(<?php echo htmlspecialchars($runDate); ?>)</small></small>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<!-- ================= KPI cards ================= -->
|
||||
<div class="row my-3">
|
||||
<div class="col-md-3">
|
||||
<a href="#sec_ok" class="text-decoration-none">
|
||||
<div class="card text-bg-success shadow-sm kpi-card">
|
||||
<div class="card-body py-2 d-flex justify-content-between align-items-center">
|
||||
<div><h6 class="card-title mb-0"><i class="bi bi-check-circle me-1"></i>Clusters OK</h6></div>
|
||||
<h2 class="fw-bold mb-0"><?php echo $count_ok; ?></h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="#sec_drift" class="text-decoration-none">
|
||||
<div class="card text-bg-warning shadow-sm kpi-card">
|
||||
<div class="card-body py-2 d-flex justify-content-between align-items-center">
|
||||
<div><h6 class="card-title mb-0"><i class="bi bi-exclamation-triangle me-1"></i>Clusters en drift</h6></div>
|
||||
<h2 class="fw-bold mb-0"><?php echo $count_drift; ?></h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="#sec_ubrgap" class="text-decoration-none">
|
||||
<div class="card text-bg-info shadow-sm kpi-card">
|
||||
<div class="card-body py-2 d-flex justify-content-between align-items-center">
|
||||
<div><h6 class="card-title mb-0"><i class="bi bi-arrow-left-right me-1"></i>Écart UBR (même build)</h6></div>
|
||||
<h2 class="fw-bold mb-0"><?php echo $count_ubrgap; ?></h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="#sec_unreach" class="text-decoration-none">
|
||||
<div class="card text-bg-danger shadow-sm kpi-card">
|
||||
<div class="card-body py-2 d-flex justify-content-between align-items-center">
|
||||
<div><h6 class="card-title mb-0"><i class="bi bi-plug me-1"></i>Clusters injoignables</h6></div>
|
||||
<h2 class="fw-bold mb-0"><?php echo $count_unreach; ?></h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ================= DRIFT table ================= -->
|
||||
<h4 id="sec_drift" class="mt-4"><i class="bi bi-exclamation-triangle text-warning me-1"></i>Clusters en drift</h4>
|
||||
<table class="table table-bordered table-hover table-sm" id="t_drift"
|
||||
data-toggle="table" data-search="true" data-show-columns="true"
|
||||
data-export-types="['xlsx','csv','json']" data-show-export="true"
|
||||
data-sortable="true" style="width:99%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-field="cluster" data-sortable="true">Cluster</th>
|
||||
<th data-field="ecart" data-sortable="false">Écart</th>
|
||||
<th data-field="noeuds" data-sortable="true">Nœuds</th>
|
||||
<th data-field="build" data-sortable="false">Build(s)</th>
|
||||
<th data-field="ubrgap" data-sortable="true">Gap UBR</th>
|
||||
<th data-field="ms" data-sortable="false">Niveau(x) MS</th>
|
||||
<th data-field="modele" data-sortable="false">Modèle(s)</th>
|
||||
<th data-field="bios" data-sortable="false">BIOS</th>
|
||||
<th data-field="biosdate" data-sortable="false">Date BIOS</th>
|
||||
<th data-field="spp" data-sortable="false">Signature(s) SPP</th>
|
||||
<th data-field="detail" data-sortable="false">Détail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($drift as $name): $cl = $clusters[$name]; $s = cluster_summary($cl); ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($name); ?></td>
|
||||
<td>
|
||||
<?php if ($cl['build']) { echo "<span class='badge text-bg-danger me-1'>BUILD</span>"; } ?>
|
||||
<?php if ($cl['spp']) { echo "<span class='badge text-bg-warning text-dark me-1'>SPP</span>"; } ?>
|
||||
<?php if ($s['bios_drift']) { echo "<span class='badge text-bg-secondary'>BIOS</span>"; } ?>
|
||||
</td>
|
||||
<td><?php echo $s['nodecount']; ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(', ', $s['builds'])); ?></td>
|
||||
<td><?php echo ubrgap_badge($s['ubrgap']); ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(', ', $s['ms'])); ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(' / ', $s['models'])); ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(' / ', $s['bios'])); ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(' / ', $s['biosdates'])); ?></td>
|
||||
<td><code class="sig"><?php echo htmlspecialchars(implode(' / ', $s['spp'])); ?></code></td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-outline-primary"
|
||||
href="cluster_compliance_detail.php?cluster=<?php echo urlencode($name); ?>&run=<?php echo urlencode($runId); ?>">
|
||||
<i class="bi bi-search"></i> Détail
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- ================= UBR GAP table (same build, KB behind) ================= -->
|
||||
<h4 id="sec_ubrgap" class="mt-4"><i class="bi bi-arrow-left-right text-info me-1"></i>Écart UBR — même build, KB à rattraper</h4>
|
||||
<p class="text-muted small mb-2">
|
||||
Ces clusters sont conformes au niveau cluster (build aligné), mais un ou plusieurs nœuds
|
||||
sont en retard de correctif. Triés par écart décroissant — les plus gros à corriger en priorité.
|
||||
</p>
|
||||
<table class="table table-bordered table-hover table-sm" id="t_ubrgap"
|
||||
data-toggle="table" data-search="true" data-show-columns="true"
|
||||
data-export-types="['xlsx','csv','json']" data-show-export="true"
|
||||
data-sortable="true" style="width:99%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-field="cluster" data-sortable="true">Cluster</th>
|
||||
<th data-field="ubrgap" data-sortable="true">Gap UBR</th>
|
||||
<th data-field="noeuds" data-sortable="true">Nœuds</th>
|
||||
<th data-field="build" data-sortable="true">Build</th>
|
||||
<th data-field="ms" data-sortable="false">Niveau(x) MS</th>
|
||||
<th data-field="modele" data-sortable="false">Modèle</th>
|
||||
<th data-field="bios" data-sortable="false">BIOS</th>
|
||||
<th data-field="detail" data-sortable="false">Détail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($ubrgap_list as $name): $cl = $clusters[$name]; $s = cluster_summary($cl); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php echo htmlspecialchars($name); ?>
|
||||
<?php if ($s['bios_drift']) { echo " <span class='badge text-bg-secondary'>BIOS</span>"; } ?>
|
||||
</td>
|
||||
<td><?php echo ubrgap_badge($s['ubrgap']); ?></td>
|
||||
<td><?php echo $s['nodecount']; ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(', ', $s['builds'])); ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(', ', $s['ms'])); ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(' / ', $s['models'])); ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(' / ', $s['bios'])); ?></td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-outline-primary"
|
||||
href="cluster_compliance_detail.php?cluster=<?php echo urlencode($name); ?>&run=<?php echo urlencode($runId); ?>">
|
||||
<i class="bi bi-search"></i> Détail
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- ================= OK table ================= -->
|
||||
<h4 id="sec_ok" class="mt-4"><i class="bi bi-check-circle text-success me-1"></i>Clusters OK</h4>
|
||||
<table class="table table-bordered table-hover table-sm" id="t_ok"
|
||||
data-toggle="table" data-search="true" data-show-columns="true"
|
||||
data-export-types="['xlsx','csv','json']" data-show-export="true"
|
||||
data-sortable="true" style="width:99%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-field="cluster" data-sortable="true">Cluster</th>
|
||||
<th data-field="noeuds" data-sortable="true">Nœuds</th>
|
||||
<th data-field="build" data-sortable="true">Build</th>
|
||||
<th data-field="ubrgap" data-sortable="true">Gap UBR</th>
|
||||
<th data-field="ms" data-sortable="true">Niveau(x) MS</th>
|
||||
<th data-field="modele" data-sortable="false">Modèle</th>
|
||||
<th data-field="bios" data-sortable="false">BIOS</th>
|
||||
<th data-field="spp" data-sortable="false">Signature SPP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($ok as $name): $cl = $clusters[$name]; $s = cluster_summary($cl); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php echo htmlspecialchars($name); ?>
|
||||
<?php if ($s['bios_drift']) { echo " <span class='badge text-bg-secondary'>BIOS</span>"; } ?>
|
||||
</td>
|
||||
<td><?php echo $s['nodecount']; ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(', ', $s['builds'])); ?></td>
|
||||
<td><?php echo ubrgap_badge($s['ubrgap']); ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(', ', $s['ms'])); ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(' / ', $s['models'])); ?></td>
|
||||
<td><?php echo htmlspecialchars(implode(' / ', $s['bios'])); ?></td>
|
||||
<td><code class="sig"><?php echo htmlspecialchars(implode(', ', $s['spp'])); ?></code></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- ================= UNREACHABLE (collapsed list, at the end) ================= -->
|
||||
<h4 id="sec_unreach" class="mt-4 mb-2">
|
||||
<a class="collapse-toggle text-decoration-none text-reset" data-bs-toggle="collapse"
|
||||
href="#collapseUnreach" role="button" aria-expanded="false" aria-controls="collapseUnreach">
|
||||
<i class="bi bi-plug text-danger me-1"></i>Clusters injoignables
|
||||
<span class="badge text-bg-danger align-middle"><?php echo $count_unreach; ?></span>
|
||||
<i class="bi bi-chevron-down small ms-1"></i>
|
||||
</a>
|
||||
</h4>
|
||||
<div class="collapse mb-4" id="collapseUnreach">
|
||||
<?php if ($count_unreach == 0): ?>
|
||||
<p class="text-muted small mb-0">Aucun cluster injoignable sur ce run.</p>
|
||||
<?php else: ?>
|
||||
<ul class="list-group">
|
||||
<?php foreach ($unreach as $name): $cl = $clusters[$name]; $s = cluster_summary($cl); ?>
|
||||
<li class="list-group-item">
|
||||
<i class="bi bi-plug text-danger me-1"></i><strong><?php echo htmlspecialchars($name); ?></strong>
|
||||
<?php if (!empty($s['unodes'])): ?>
|
||||
<span class="text-muted">— <?php echo htmlspecialchars(implode(', ', $s['unodes'])); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($s['errs'])): ?>
|
||||
<div class="small text-muted mt-1"><?php echo htmlspecialchars(implode(' | ', $s['errs'])); ?></div>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/switch.js"></script>
|
||||
<script>
|
||||
// Clicking the "Clusters injoignables" KPI card scrolls to the section
|
||||
// and expands the collapsed list.
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var card = document.querySelector('a[href="#sec_unreach"]');
|
||||
var box = document.getElementById('collapseUnreach');
|
||||
if (card && box) {
|
||||
card.addEventListener('click', function () {
|
||||
bootstrap.Collapse.getOrCreateInstance(box, { toggle: false }).show();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user