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:
2026-09-14 10:32:15 +02:00
parent 1f1b7dcd22
commit c185d099d6
25 changed files with 6646 additions and 437 deletions

410
reports/Backups.php Normal file
View File

@@ -0,0 +1,410 @@
<!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 IT</title>
<link rel="shortcut icon" type="image/png" href="/include/favicon-32x32.png">
<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">
<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>
/* ---- Theme --------------------------------------------------------
switch.js only swaps -light/-dark classes and tags tables with
.table-dark. The accordion paints its own background, so we make it
transparent and let it inherit whatever the body is wearing. */
.accordion,
.accordion-item,
.accordion-button,
.accordion-button:not(.collapsed),
.accordion-body { background-color: transparent; color: inherit; }
.accordion-item { border-color: rgba(128, 128, 128, .35); }
.accordion-button:not(.collapsed) { box-shadow: none; background-color: rgba(128, 128, 128, .12); }
.accordion-button:focus{ box-shadow: none; border-color: rgba(128, 128, 128, .35); }
.accordion-button::after { filter: grayscale(1) opacity(.75); }
body.bg-dark .accordion-button::after { filter: invert(1) grayscale(1) opacity(.85); }
/* Sticky needs a non-clipping, non-scrolling ancestor chain. bootstrap-table
wraps the table in .fixed-table-body (overflow:auto), which would capture
the sticky thead and push it down inside the table instead of the page. */
.accordion, .accordion-item { overflow: visible; }
.accordion-body,
.accordion-body .fixed-table-container,
.accordion-body .fixed-table-body,
.accordion-body .fixed-table-toolbar { overflow: visible !important; }
.accordion-body .fixed-table-container { border: 0; }
/* ---- Sticky block header + sticky table head ---------------------- */
.accordion-header {
position: sticky;
top: 0;
z-index: 3;
background-color: #f8f9fa;
}
.report-table thead th {
position: sticky;
top: var(--acc-header-h, 56px);
z-index: 2;
background-color: #f8f9fa;
}
body.bg-dark .accordion-header,
body.bg-dark .report-table thead th { background-color: #212529; }
.block-count { font-size: 1.5rem;min-width: 3.5em; }
.accordion-button .block-title { font-size: 2rem; font-weight: 600; letter-spacing: .02em; }
</style>
</head>
<body class="bg-light text-dark">
<?php include $_SERVER['DOCUMENT_ROOT'] . "/include/all.php"; ?>
<?php
/* ============================================================================
Unified backup reporting : Veeam (target) + NetBackup (legacy)
During the migration a VM can still exist in both SQL tables.
It is displayed only on the side holding the most recent backup.
========================================================================== */
/* ---------------------------------------------------------------- helpers */
// Whole days between a date and today. Returns null when the date is unusable.
function days_since($dateStr) {
if (empty($dateStr)) return null;
$ts = strtotime($dateStr);
if ($ts === false || $ts < strtotime('1990-01-01')) return null;
return (int) floor((strtotime(date('Y-m-d')) - strtotime(date('Y-m-d', $ts))) / 86400);
}
// Backups do not run on Sunday night, so Sunday and Monday get a wider window.
function backup_tolerance_days() {
$dow = (int) date('w'); // 0 = Sunday, 1 = Monday
return ($dow === 0 || $dow === 1) ? 4 : 3;
}
function fmt_datetime($value) {
$ts = strtotime((string) $value);
return ($ts && $ts > strtotime('1990-01-01')) ? date('Y-m-d H:i', $ts) : '-';
}
function fmt_lastgood($dateStr) {
$days = days_since($dateStr);
if ($days === null || $days > 365) return 'NEVER';
return date('Y-m-d H:i', strtotime($dateStr)) . " ({$days}J)";
}
/* ------------------------------------------------------- active inventory */
$activeRows = Invoke_Infra("SELECT name FROM cmdb_vms
WHERE CAST(lastinventory AS DATETIME) > DATEADD(DAY, -2, GETDATE())");
$active = [];
foreach ($activeRows as $r) { $active[strtoupper(trim($r['name']))] = true; }
/* -------------------------------------------------------------- 1. Veeam */
$veeam = [];
$rows = Invoke_Infra("SELECT * FROM Veeam_Reporting");
foreach ($rows as $row) {
$key = strtoupper(trim($row['ClientName']));
if (!isset($active[$key])) continue;
$ageGood = days_since($row['LastSuccess']);
$status = trim((string) $row['LastStatus']);
if ($status === 'Failed' || $status === 'Unknown' || $status === '') {
$cat = 'error'; $cls = 'table-danger';
} elseif ($status === 'Warning') {
$cat = 'warning'; $cls = 'table-warning';
} elseif ($ageGood === null || $ageGood >= backup_tolerance_days()) {
$cat = 'outdated'; $cls = 'table-info';
} else {
$cat = 'ok'; $cls = 'table-success';
}
$veeam[$key] = [
'cat' => $cat,
'row_class' => $cls,
'ts' => strtotime((string) $row['LastRun']) ?: 0,
'vm' => $row['ClientName'],
'last_run' => fmt_datetime($row['LastRun']),
'status' => $status,
'last_good' => fmt_lastgood($row['LastSuccess']),
'size' => $row['SizeGB'],
'extra' => $row['Duration'],
'policy' => $row['PolicyName'],
'message' => $row['ErrorMessage'],
];
}
/* ---------------------------------------------------------- 2. NetBackup */
$netbackup = [];
$rows = Invoke_Infra("SELECT * FROM VMs_Backup
WHERE (Owner LIKE 'DUN-VMH%' OR Owner LIKE 'MDK-VMH%')
AND name NOT LIKE 'WS%'
AND owner NOT LIKE '%WKG%'
AND owner NOT LIKE '%VMH-WM%'
ORDER BY name");
foreach ($rows as $row) {
$key = strtoupper(trim($row['Name']));
$excluded = trim((string) $row['Exclusion']) !== '';
// Excluded VMs are listed even when no longer inventoried, like on the old page.
if (!$excluded && !isset($active[$key])) continue;
$result = trim((string) $row['LastResult']);
$ageGood = days_since($row['LastKnownGood']);
if ($excluded) {
$cat = 'excluded'; $cls = 'table-secondary';
} elseif ($result !== 'OK') {
// A failed job with a still fresh good copy is only a warning.
if ($ageGood === null || $ageGood >= backup_tolerance_days()) {
$cat = 'error'; $cls = 'table-danger';
} else {
$cat = 'warning'; $cls = 'table-warning';
}
} elseif ($ageGood === null || $ageGood >= backup_tolerance_days()) {
$cat = 'outdated'; $cls = 'table-info';
} else {
$cat = 'ok'; $cls = 'table-success';
}
$netbackup[$key] = [
'cat' => $cat,
'row_class' => $cls,
'excluded' => $excluded,
'ts' => strtotime(trim($row['LastBackup'] . ' ' . $row['TimeStamp'])) ?: 0,
'vm' => $row['Name'],
'last_run' => $excluded ? 'Tag NoBackup' : fmt_datetime($row['LastBackup'] . ' ' . $row['TimeStamp']),
'status' => $excluded ? 'Tag NoBackup' : ($result !== '' ? $result : 'Unknown'),
'last_good' => $excluded ? 'Tag NoBackup' : fmt_lastgood($row['LastKnownGood']),
'size' => $excluded ? '' : $row['LastSize'],
'extra' => $row['Owner'],
'policy' => $row['Policy'],
'message' => '',
];
}
/* ------------------------------------------- 3. Migration de-duplication */
// A VM present on both sides is kept where the most recent backup lives.
$migrated = 0;
foreach (array_keys($netbackup) as $key) {
if (!isset($veeam[$key])) continue;
if ($netbackup[$key]['excluded'] || $veeam[$key]['ts'] >= $netbackup[$key]['ts']) {
unset($netbackup[$key]); // Veeam has taken over
$migrated++;
} else {
unset($veeam[$key]); // NetBackup is still the live copy
}
}
/* -------------------------------------------------------- 4. Bucketing */
$buckets = [
'veeam' => ['error' => [], 'warning' => [], 'outdated' => [], 'ok' => []],
'nb' => ['error' => [], 'warning' => [], 'outdated' => [], 'ok' => [], 'excluded' => []],
];
foreach ($veeam as $e) { $buckets['veeam'][$e['cat']][] = $e; }
foreach ($netbackup as $e) { $buckets['nb'][$e['cat']][] = $e; }
foreach ($buckets as $src => $cats) {
foreach ($cats as $cat => $list) {
usort($list, fn($a, $b) => strcasecmp($a['vm'], $b['vm']));
$buckets[$src][$cat] = $list;
}
}
$veeamTotal = count($veeam);
$nbTotal = count($netbackup);
/* -------------------------------------------------------- 5. Rendering */
$blockMeta = [
'error' => ['label' => 'Error', 'badge' => 'bg-danger', 'icon' => 'bi-x-octagon'],
'warning' => ['label' => 'Warning', 'badge' => 'bg-warning text-dark', 'icon' => 'bi-exclamation-triangle'],
'outdated' => ['label' => 'Outdated', 'badge' => 'bg-info text-dark', 'icon' => 'bi-clock-history'],
'ok' => ['label' => 'OK', 'badge' => 'bg-success', 'icon' => 'bi-check-circle'],
'excluded' => ['label' => 'Excluded', 'badge' => 'bg-secondary', 'icon' => 'bi-slash-circle'],
];
/**
* Render one accordion item holding a bootstrap-table.
* $extraLabel is "Duration" for Veeam and "Host" for NetBackup.
*/
function render_block($blockId, $meta, $rows, $extraLabel, $expanded = false) {
$count = count($rows);
$collapsed = $expanded ? '' : 'collapsed';
$show = $expanded ? 'show' : '';
$aria = $expanded ? 'true' : 'false';
?>
<div class="accordion-item">
<h1 class="accordion-header">
<button class="accordion-button <?php echo $collapsed; ?>" type="button"
data-bs-toggle="collapse" data-bs-target="#c_<?php echo $blockId; ?>"
aria-expanded="<?php echo $aria; ?>" <?php echo $count ? '' : 'disabled'; ?>>
<i class="bi <?php echo $meta['icon']; ?> me-2"></i>
<span class="block-title"><?php echo $meta['label']; ?></span>
<span class="badge <?php echo $meta['badge']; ?> ms-3 block-count"><?php echo $count; ?></span>
</button>
</h1>
<div id="c_<?php echo $blockId; ?>" class="accordion-collapse collapse <?php echo $show; ?>">
<div class="accordion-body p-2">
<?php if ($count === 0) : ?>
<p class="text-muted mb-0">Aucune VM dans cette catégorie.</p>
<?php else : ?>
<table class="table table-bordered table-hover table-sm report-table"
id="t_<?php echo $blockId; ?>" data-sortable="true">
<thead>
<tr>
<th data-field="vm" data-sortable="true"><?php echo $GLOBALS['w_VMs']; ?></th>
<th data-field="lastrun" data-sortable="true"><?php echo $GLOBALS['w_backuplu']; ?></th>
<th data-field="status" data-sortable="true"><?php echo $GLOBALS['w_lastResult']; ?></th>
<th data-field="lastgood" data-sortable="true"><?php echo $GLOBALS['w_lastGoodBackup']; ?></th>
<th data-field="size" data-sortable="true"><?php echo $GLOBALS['w_size']; ?> (GB)</th>
<th data-field="extra" data-sortable="true"><?php echo $extraLabel; ?></th>
<th data-field="policy" data-sortable="true"><?php echo $GLOBALS['w_policy']; ?></th>
<th data-field="message" data-sortable="true">Message</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r) : ?>
<tr class="<?php echo $r['row_class']; ?>">
<td><b><?php echo htmlspecialchars($r['vm']); ?></b></td>
<td><?php echo htmlspecialchars($r['last_run']); ?></td>
<td><?php echo htmlspecialchars($r['status']); ?></td>
<td><?php echo htmlspecialchars($r['last_good']); ?></td>
<td><?php echo htmlspecialchars((string) $r['size']); ?></td>
<td><?php echo htmlspecialchars((string) $r['extra']); ?></td>
<td><?php echo htmlspecialchars((string) $r['policy']); ?></td>
<td style="font-size:.8em;"><i><?php echo htmlspecialchars((string) $r['message']); ?></i></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
<?php
}
?>
<div class="container-fluid" id="content">
<div class="row flex-nowrap">
<div class="col-auto col-md-2 col-xl-2 px-sm-2 px-0 bg-dark vh-100 position-sticky top-0" style="-ms-flex: 0 0 230px;flex: 0 0 230px;">
<?php include $_SERVER['DOCUMENT_ROOT'] . "/navbar.html"; ?>
</div>
<div class="col py-3">
<h2>
<span class="badge text-bg-secondary w-100" id="ERR">
VM Backup —
<span class="text-light"><?php echo $veeamTotal; ?> Veeam</span> /
<span class="text-light"><?php echo $nbTotal; ?> NetBackup</span> —
<span class="text-danger"><?php echo count($buckets['veeam']['error']) + count($buckets['nb']['error']); ?> Error</span> -
<span class="text-warning"><?php echo count($buckets['veeam']['warning']) + count($buckets['nb']['warning']); ?> Warning</span> -
<span class="text-info"><?php echo count($buckets['veeam']['outdated']) + count($buckets['nb']['outdated']); ?> Outdated</span>
</span>
</h2>
<div class="container-fluid">
<!-- ============================ VEEAM ============================ -->
<h4 class="mt-3 mb-2">
<i class="bi bi-shield-check me-1"></i> VEEAM
<small class="text-muted"><?php echo $veeamTotal; ?> VMs</small>
</h4>
<div class="accordion" id="acc_veeam">
<?php
foreach (['error', 'warning', 'outdated', 'ok'] as $cat) {
render_block('veeam_' . $cat, $blockMeta[$cat], $buckets['veeam'][$cat], 'Duration', false);
}
?>
</div>
<!-- ========================== NETBACKUP ========================== -->
<h4 class="mt-4 mb-2">
<i class="bi bi-hdd-stack me-1"></i> NETBACKUP
<small class="text-muted"><?php echo $nbTotal; ?> VMs</small>
</h4>
<div class="accordion mb-4" id="acc_nb">
<?php
foreach (['error', 'warning', 'outdated', 'ok', 'excluded'] as $cat) {
render_block('nb_' . $cat, $blockMeta[$cat], $buckets['nb'][$cat], $GLOBALS['w_host'], false);
}
?>
</div>
</div>
</div>
</div>
</div>
<script src="/js/switch.js"></script>
<script>
$(function () {
// Tables are built lazily, after switch.js has already themed the page,
// so they have to pick up the current theme themselves.
function isDark() {
return document.body.classList.contains('bg-dark');
}
function syncTheme($table) {
$table.toggleClass('table-dark', isDark());
}
// Pin the table head right below its own (sticky) accordion header.
function updateStickyOffset($item) {
var h = $item.find('.accordion-header').outerHeight() || 56;
$item.css('--acc-header-h', h + 'px');
}
// bootstrap-table cannot measure a hidden container, so tables are
// initialised the first time their accordion block is opened.
function initTable($table) {
if ($table.length === 0) return;
if (!$table.data('bt-initialized')) {
$table.bootstrapTable();
$table.data('bt-initialized', true);
} else {
$table.bootstrapTable('resetView');
}
syncTheme($table);
updateStickyOffset($table.closest('.accordion-item'));
}
$('.accordion-collapse').on('shown.bs.collapse', function () {
initTable($(this).find('table.report-table'));
});
// Keep tables in sync when the light switch is toggled after load.
$('#lightSwitch').on('change', function () {
setTimeout(function () {
$('table.report-table').each(function () { syncTheme($(this)); });
}, 0);
});
$(window).on('resize', function () {
$('.accordion-item').each(function () { updateStickyOffset($(this)); });
});
});
</script>
</body>
</html>

View 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 &mdash; 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>&Eacute;cart UBR (m&ecirc;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">&Eacute;cart</th>
<th data-field="noeuds" data-sortable="true">N&oelig;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&egrave;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&eacute;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&eacute;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>&Eacute;cart UBR &mdash; m&ecirc;me build, KB &agrave; rattraper</h4>
<p class="text-muted small mb-2">
Ces clusters sont conformes au niveau cluster (build align&eacute;), mais un ou plusieurs n&oelig;uds
sont en retard de correctif. Tri&eacute;s par &eacute;cart d&eacute;croissant &mdash; les plus gros &agrave; corriger en priorit&eacute;.
</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&oelig;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&egrave;le</th>
<th data-field="bios" data-sortable="false">BIOS</th>
<th data-field="detail" data-sortable="false">D&eacute;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&eacute;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&oelig;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&egrave;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">&mdash; <?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>

View File

@@ -0,0 +1,221 @@
<!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>
code.sig { font-size: .85em; }
.table td { vertical-align: middle; }
.pivot th, .pivot td { text-align: center; }
.pivot td.comp { text-align: left; font-weight: 500; }
</style>
</head>
<body>
<?php include $_SERVER['DOCUMENT_ROOT']."/include/all.php" ; ?>
<div class="container-fluid">
<div class="row flex-nowrap">
<div class="col-auto col-md-2 col-xl-2 px-sm-2 px-0 bg-dark" style="-ms-flex: 0 0 230px;flex: 0 0 230px;">
<?php include $_SERVER['DOCUMENT_ROOT']."/navbar.html" ; ?>
</div>
<div class="col py-3">
<?php
// -----------------------------------------------------------
// Helpers
// -----------------------------------------------------------
function majority($arr) {
if (count($arr) == 0) { return null; }
$c = array_count_values($arr);
arsort($c);
return key($c);
}
$cluster = isset($_GET['cluster']) ? $_GET['cluster'] : '';
$run = isset($_GET['run']) ? $_GET['run'] : '';
$conn = DB_INFRA();
$nodes = array();
$delta = array();
$runDate = '-';
if ($cluster !== '' && $run !== '') {
// --- Node facts (parameterized to avoid injection) -------
$sqlNodes = "SELECT NodeName, OsName, MsPatchLevel, LastHotfix,
CONVERT(VARCHAR(10), LastHotfixOn, 120) AS LastHotfixOn,
SppSignature, Reachable, MsDrift, SppDrift, ErrorMessage,
CONVERT(VARCHAR(16), RunDate, 120) AS RunDate
FROM dbo.ClusterComplianceResults
WHERE ClusterName = ? AND RunId = ?
ORDER BY NodeName";
$stmt = odbc_prepare($conn, $sqlNodes);
if ($stmt && odbc_execute($stmt, array($cluster, $run))) {
while ($r = odbc_fetch_array($stmt)) {
if ($runDate === '-' && !empty($r['RunDate'])) { $runDate = $r['RunDate']; }
$nodes[] = $r;
}
}
// --- SPP component delta ---------------------------------
$sqlDelta = "SELECT ComponentName, NodeName, ComponentVersion
FROM dbo.ClusterComplianceSppDelta
WHERE ClusterName = ? AND RunId = ?
ORDER BY ComponentName, NodeName";
$stmt2 = odbc_prepare($conn, $sqlDelta);
if ($stmt2 && odbc_execute($stmt2, array($cluster, $run))) {
while ($r = odbc_fetch_array($stmt2)) { $delta[] = $r; }
}
}
// Build the delta pivot : component x node
$deltaNodes = array();
$comps = array();
$matrix = array();
foreach ($delta as $d) {
$cp = $d['ComponentName'];
$nd = $d['NodeName'];
if (!in_array($nd, $deltaNodes)) { $deltaNodes[] = $nd; }
$comps[$cp] = true;
$matrix[$cp][$nd] = $d['ComponentVersion'];
}
sort($deltaNodes);
$comps = array_keys($comps);
sort($comps);
// MS majority for highlighting
$msVals = array();
foreach ($nodes as $n) {
if ($n['Reachable'] == 1 && $n['MsPatchLevel'] !== '' && $n['MsPatchLevel'] !== null) {
$msVals[] = $n['MsPatchLevel'];
}
}
$msMajority = majority($msVals);
?>
<h1>
<span class="badge text-bg-secondary" style="width:100%;">
D&eacute;tail cluster : <?php echo htmlspecialchars($cluster); ?>
<small><small>(<?php echo htmlspecialchars($runDate); ?>)</small></small>
</span>
</h1>
<a href="cluster_compliance.php" class="btn btn-sm btn-outline-secondary mb-3">
<i class="bi bi-arrow-left"></i> Retour au tableau de bord
</a>
<?php if (count($nodes) == 0): ?>
<div class="alert alert-warning">Aucune donn&eacute;e pour ce cluster / ce run.</div>
<?php else: ?>
<!-- ============ Node facts ============ -->
<h4 class="mt-3">&Eacute;tat des n&oelig;uds</h4>
<table class="table table-bordered table-sm">
<thead class="table-light">
<tr>
<th>N&oelig;ud</th>
<th>OS</th>
<th>Niveau MS</th>
<th>Signature SPP</th>
<th>&Eacute;tat</th>
<th>Erreur</th>
</tr>
</thead>
<tbody>
<?php foreach ($nodes as $n): ?>
<?php
$msCls = '';
if ($n['Reachable'] == 1 && $msMajority !== null && $n['MsPatchLevel'] !== $msMajority) {
$msCls = 'table-warning';
}
?>
<tr>
<td><?php echo htmlspecialchars($n['NodeName']); ?></td>
<td><?php echo htmlspecialchars($n['OsName']); ?></td>
<td class="<?php echo $msCls; ?>"><?php echo htmlspecialchars($n['MsPatchLevel']); ?></td>
<td><code class="sig"><?php echo htmlspecialchars($n['SppSignature']); ?></code></td>
<td>
<?php if ($n['Reachable'] == 1): ?>
<span class="badge text-bg-success">Joignable</span>
<?php else: ?>
<span class="badge text-bg-danger">Injoignable</span>
<?php endif; ?>
</td>
<td><small class="text-muted"><?php echo htmlspecialchars($n['ErrorMessage']); ?></small></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- ============ SPP delta pivot ============ -->
<h4 class="mt-4">Delta SPP HP (composants divergents)</h4>
<?php if (count($comps) == 0): ?>
<div class="alert alert-info mb-0">
Aucun &eacute;cart de composant HP relev&eacute; (drift Microsoft uniquement, ou signatures identiques).
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-bordered table-sm pivot">
<thead class="table-light">
<tr>
<th class="comp">Composant</th>
<?php foreach ($deltaNodes as $nd): ?>
<th><?php echo htmlspecialchars($nd); ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($comps as $cp): ?>
<?php
$rowVals = array();
foreach ($deltaNodes as $nd) {
$rowVals[] = isset($matrix[$cp][$nd]) ? $matrix[$cp][$nd] : '<absent>';
}
$maj = majority($rowVals);
?>
<tr>
<td class="comp"><?php echo htmlspecialchars($cp); ?></td>
<?php foreach ($deltaNodes as $nd): ?>
<?php
$v = isset($matrix[$cp][$nd]) ? $matrix[$cp][$nd] : '<absent>';
$cls = '';
if ($v === '<absent>') { $cls = 'table-danger'; }
elseif ($v !== $maj) { $cls = 'table-warning'; }
?>
<td class="<?php echo $cls; ?>"><?php echo htmlspecialchars($v); ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="small text-muted mt-1">
<span class="badge text-bg-warning text-dark">jaune</span> version diff&eacute;rente de la majorit&eacute; &nbsp;
<span class="badge text-bg-danger">rouge</span> composant absent du n&oelig;ud
</div>
<?php endif; ?>
<?php endif; /* nodes exist */ ?>
</div>
</div>
</div>
<script src="/js/switch.js"></script>
</body>
</html>

941
reports/Netbackup_Daily.php Normal file
View File

@@ -0,0 +1,941 @@
<?php ob_start();?>
<!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 IT - NetBackup Daily</title>
<link rel="shortcut icon" type="image/png" href="/include/favicon-32x32.png">
<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">
<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>
.accordion,
.accordion-item,
.accordion-button,
.accordion-button:not(.collapsed),
.accordion-body {
background-color: transparent;
color: inherit;
}
.accordion-item {
border-color: rgba(128, 128, 128, .35);
overflow: visible;
}
.accordion-button:not(.collapsed) {
box-shadow: none;
background-color: rgba(128, 128, 128, .12);
}
.accordion-button:focus {
box-shadow: none;
border-color: rgba(128, 128, 128, .35);
}
.accordion-button::after {
filter: grayscale(1) opacity(.75);
}
body.bg-dark .accordion-button::after {
filter: invert(1) grayscale(1) opacity(.85);
}
.accordion-body,
.accordion-body .fixed-table-container,
.accordion-body .fixed-table-body,
.accordion-body .fixed-table-toolbar {
overflow: visible !important;
}
.accordion-body .fixed-table-container {
border: 0;
}
.accordion-header {
position: sticky;
top: 0;
z-index: 3;
background-color: #f8f9fa;
}
.report-table thead th {
position: sticky;
top: var(--acc-header-h, 56px);
z-index: 2;
background-color: #f8f9fa;
}
body.bg-dark .accordion-header,
body.bg-dark .report-table thead th {
background-color: #212529;
}
.block-count {
font-size: 1.25rem;
min-width: 3.2em;
}
.accordion-button .block-title {
font-size: 1.45rem;
font-weight: 600;
letter-spacing: .01em;
}
.master-card {
border-left-width: 6px !important;
border-left-style: solid !important;
}
.master-ok { border-left-color: #198754 !important; }
.master-warning { border-left-color: #ffc107 !important; }
.master-critical { border-left-color: #dc3545 !important; }
.metric {
font-size: 1.05rem;
font-weight: 600;
}
.metric-label {
font-size: .72rem;
text-transform: uppercase;
letter-spacing: .04em;
opacity: .75;
}
.summary-card {
min-height: 86px;
}
.warning-size {
color: #b7791f;
font-weight: 700;
}
body.bg-dark .warning-size {
color: #ffc107;
}
body.bg-dark .card {
--bs-card-bg: #2b3035;
--bs-card-color: #f8f9fa;
--bs-card-border-color: #495057;
background-color: #2b3035;
color: #f8f9fa;
border-color: #495057;
}
body.bg-dark .master-card,
body.bg-dark .summary-card {
background-color: #2b3035 !important;
color: #f8f9fa !important;
}
body.bg-dark .card .text-muted,
body.bg-dark .master-card .text-muted,
body.bg-dark .summary-card .text-muted {
color: #adb5bd !important;
}
body.bg-dark .metric-label {
color: #adb5bd;
opacity: 1;
}
/* More readable OK green, especially on the grey title badge. */
.title-ok {
color: #00d084 !important;
}
body.bg-dark .title-ok {
color: #5ee6a8 !important;
}
@media (max-width: 767.98px) {
.accordion-button .block-title {
font-size: 1.1rem;
}
.block-count {
font-size: 1rem;
min-width: 2.8em;
}
.summary-card {
min-height: auto;
}
.report-table {
font-size: .82rem;
}
}
</style>
</head>
<body class="bg-light text-dark">
<?php include $_SERVER['DOCUMENT_ROOT'] . "/include/all.php"; ?>
<?php
/* ============================================================================
Helpers
========================================================================== */
function h($value) {
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function db_int($row, $key) {
return isset($row[$key]) && $row[$key] !== null ? (int)$row[$key] : 0;
}
function fmt_dt($value) {
if (empty($value)) return '-';
$ts = strtotime((string)$value);
return $ts ? date('d/m/Y H:i', $ts) : '-';
}
function fmt_gb_from_kb($kb) {
if ($kb === null || $kb === '') return '-';
return number_format(((float)$kb) / 1048576, 1, ',', ' ') . ' GB';
}
// PURGE Random datas //
Invoke_Infra("delete from netbackupjobs where master not like '%-%'");
/* ============================================================================
SQL commun : point de départ par master
Dernier jour (dans les 3 jours precedents) ayant un Infra/full,
puis premier JobID Infra/full de ce jour.
========================================================================== */
$startPointCte = "
WITH Masters AS
(
SELECT DISTINCT master
FROM dbo.NetBackupJobs
),
LatestFullDay AS
(
SELECT
master,
MAX(CAST(start_date AS date)) AS full_day
FROM dbo.NetBackupJobs
WHERE
start_date >= CAST(DATEADD(DAY, -3, GETDATE()) AS date)
AND start_date < CAST(GETDATE() AS date)
AND policy LIKE '%Infra%'
AND schedule_type = 'full'
GROUP BY master
),
StartPoints AS
(
SELECT
j.master,
MIN(j.job) AS start_job
FROM dbo.NetBackupJobs j
INNER JOIN LatestFullDay d
ON d.master = j.master
AND CAST(j.start_date AS date) = d.full_day
WHERE
j.policy LIKE '%Infra%'
AND j.schedule_type = 'full'
GROUP BY j.master
)
";
/* ============================================================================
1. Résumé par master
========================================================================== */
$summarySql = $startPointCte . ",
Jobs AS
(
SELECT j.*
FROM dbo.NetBackupJobs j
INNER JOIN StartPoints s
ON s.master = j.master
AND j.job >= s.start_job
),
LargeIncrementals AS
(
SELECT
j.master,
COUNT(DISTINCT j.client) AS large_incremental_clients
FROM dbo.NetBackupJobs j
INNER JOIN StartPoints s
ON s.master = j.master
AND j.job >= s.start_job
WHERE
j.schedule_type = 'incremental'
AND j.status = 0
AND j.policy NOT LIKE '%Hyper%'
AND j.policy NOT LIKE '%standard%'
AND j.policy NOT LIKE '%WE%'
AND j.size_kb > 73400320
GROUP BY j.master
)
SELECT
m.master,
s.start_job,
COUNT(j.job) AS total_jobs,
COALESCE(SUM(CASE WHEN j.status <= 1 THEN 1 ELSE 0 END), 0) AS jobs_ok,
COALESCE(SUM(CASE WHEN j.status > 1 THEN 1 ELSE 0 END), 0) AS jobs_error,
COALESCE(SUM(
CASE WHEN j.schedule_type IN ('full','incremental') THEN 1 ELSE 0 END
), 0) AS scheduled_jobs,
COALESCE(SUM(
CASE WHEN j.schedule_type IN ('full','incremental')
AND j.status > 1 THEN 1 ELSE 0 END
), 0) AS scheduled_errors,
COALESCE(SUM(
CASE WHEN j.schedule_type = 'user backup' THEN 1 ELSE 0 END
), 0) AS user_jobs,
COALESCE(SUM(
CASE WHEN j.schedule_type = 'user backup'
AND j.status > 1 THEN 1 ELSE 0 END
), 0) AS user_errors,
COALESCE(li.large_incremental_clients, 0) AS large_incremental_clients
FROM Masters m
LEFT JOIN StartPoints s
ON s.master = m.master
LEFT JOIN Jobs j
ON j.master = m.master
LEFT JOIN LargeIncrementals li
ON li.master = m.master
GROUP BY
m.master,
s.start_job,
li.large_incremental_clients
ORDER BY m.master
";
$summary = Invoke_Infra($summarySql);
if (!is_array($summary)) $summary = [];
/* ============================================================================
2. Jobs en erreur + description NBRC
========================================================================== */
$errorSql = "
WITH LatestFullDay AS
(
SELECT
master,
MAX(CAST(start_date AS date)) AS full_day
FROM dbo.NetBackupJobs
WHERE
start_date >= CAST(DATEADD(DAY, -3, GETDATE()) AS date)
AND start_date < CAST(GETDATE() AS date)
AND policy LIKE '%Infra%'
AND schedule_type = 'full'
GROUP BY master
),
StartPoints AS
(
SELECT
j.master,
MIN(j.job) AS start_job
FROM dbo.NetBackupJobs j
INNER JOIN LatestFullDay d
ON d.master = j.master
AND CAST(j.start_date AS date) = d.full_day
WHERE
j.policy LIKE '%Infra%'
AND j.schedule_type = 'full'
GROUP BY j.master
)
SELECT
j.master,
j.job,
j.client,
j.policy,
j.schedule_type,
j.status,
r.RC_DESC AS status_desc,
j.start_date,
j.end_date,
j.size_kb
FROM dbo.NetBackupJobs j
INNER JOIN StartPoints s
ON s.master = j.master
AND j.job >= s.start_job
LEFT JOIN dbo.NBRC r
ON r.RC = j.status
WHERE
j.status > 1
ORDER BY
j.master,
CASE
WHEN j.schedule_type IN ('full','incremental') THEN 0
WHEN j.schedule_type = 'user backup' THEN 1
ELSE 2
END,
j.client,
j.job
";
$errors = Invoke_Infra($errorSql);
if (!is_array($errors)) $errors = [];
/* ============================================================================
3. Warnings : incrémentales > 70 GB
Règle métier exacte :
- incremental
- status = 0
- policy NOT LIKE Hyper / standard / WE
- size > 70 GB
- à partir du point de départ du master
On garde une ligne par master/client, correspondant au plus gros job du client.
========================================================================== */
$largeSql = "
WITH LatestFullDay AS
(
SELECT
master,
MAX(CAST(start_date AS date)) AS full_day
FROM dbo.NetBackupJobs
WHERE
start_date >= CAST(DATEADD(DAY, -3, GETDATE()) AS date)
AND start_date < CAST(GETDATE() AS date)
AND policy LIKE '%Infra%'
AND schedule_type = 'full'
GROUP BY master
),
StartPoints AS
(
SELECT
j.master,
MIN(j.job) AS start_job
FROM dbo.NetBackupJobs j
INNER JOIN LatestFullDay d
ON d.master = j.master
AND CAST(j.start_date AS date) = d.full_day
WHERE
j.policy LIKE '%Infra%'
AND j.schedule_type = 'full'
GROUP BY j.master
),
Candidates AS
(
SELECT
j.*,
ROW_NUMBER() OVER
(
PARTITION BY j.master, j.client
ORDER BY j.size_kb DESC, j.job DESC
) AS rn
FROM dbo.NetBackupJobs j
INNER JOIN StartPoints s
ON s.master = j.master
AND j.job >= s.start_job
WHERE
j.schedule_type = 'incremental'
AND j.status = 0
AND j.policy NOT LIKE '%Hyper%'
AND j.policy NOT LIKE '%standard%'
AND j.policy NOT LIKE '%WE%'
AND j.size_kb > 73400320
)
SELECT
master,
job,
client,
policy,
schedule_type,
status,
start_date,
end_date,
size_kb
FROM Candidates
WHERE rn = 1
ORDER BY master, size_kb DESC, client
";
$largeIncrementals = Invoke_Infra($largeSql);
if (!is_array($largeIncrementals)) $largeIncrementals = [];
/* ============================================================================
Préparation des données
========================================================================== */
$scheduledErrors = [];
$userErrors = [];
foreach ($errors as $row) {
$type = strtolower(trim((string)($row['schedule_type'] ?? '')));
if ($type === 'full' || $type === 'incremental') {
$scheduledErrors[] = $row;
} elseif ($type === 'user backup') {
$userErrors[] = $row;
}
}
$mastersOk = 0;
$mastersWarning = 0;
$mastersCritical = 0;
$totalJobs = 0;
$totalErrors = 0;
foreach ($summary as &$row) {
$total = db_int($row, 'total_jobs');
$ok = db_int($row, 'jobs_ok');
$err = db_int($row, 'jobs_error');
$large = db_int($row, 'large_incremental_clients');
$noStart = !isset($row['start_job']) || $row['start_job'] === null || $row['start_job'] === '';
$totalJobs += $total;
$totalErrors += $err;
if ($noStart || $total === 0 || ($total > 0 && $ok === 0)) {
$row['_state'] = 'critical';
$mastersCritical++;
if ($noStart) {
$row['_state_text'] = 'Aucun point de départ Infra trouvé';
} elseif ($total === 0) {
$row['_state_text'] = 'Aucune sauvegarde détectée';
} else {
$row['_state_text'] = 'Toutes les sauvegardes sont en erreur';
}
} elseif ($err > 0 || $large > 0) {
$row['_state'] = 'warning';
$mastersWarning++;
$parts = [];
if ($err > 0) $parts[] = $err . ' erreur(s)';
if ($large > 0) $parts[] = $large . ' incr. > 70 GB';
$row['_state_text'] = implode(' / ', $parts);
} else {
$row['_state'] = 'ok';
$row['_state_text'] = 'OK';
$mastersOk++;
}
}
unset($row);
$masterCount = count($summary);
$totalLarge = count($largeIncrementals);
/* ============================================================================
Helpers de rendu
========================================================================== */
function render_master_card($m) {
$state = $m['_state'] ?? 'ok';
$borderClass = [
'ok' => 'master-ok',
'warning' => 'master-warning',
'critical' => 'master-critical',
][$state];
$badgeClass = [
'ok' => 'text-bg-success',
'warning' => 'bg-warning text-dark',
'critical' => 'text-bg-danger',
][$state];
$largeClass = db_int($m, 'large_incremental_clients') > 0 ? 'text-warning' : 'text-success';
?>
<div class="card shadow-sm mb-2 master-card <?php echo $borderClass; ?>">
<div class="card-body py-2 px-3">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
<div>
<div class="fw-bold fs-5"><?php echo h($m['master'] ?? ''); ?></div>
<span class="badge <?php echo $badgeClass; ?>">
<?php echo h($m['_state_text'] ?? ''); ?>
</span>
</div>
<div class="small text-muted">
Start job :
<strong><?php echo h($m['start_job'] ?? '-'); ?></strong>
</div>
</div>
<?php if (db_int($m, 'total_jobs') > 0) : ?>
<div class="row g-2 mt-2">
<div class="col-4 col-md-2">
<div class="metric"><?php echo db_int($m, 'total_jobs'); ?></div>
<div class="metric-label">Total</div>
</div>
<div class="col-4 col-md-2">
<div class="metric text-success"><?php echo db_int($m, 'jobs_ok'); ?></div>
<div class="metric-label">OK</div>
</div>
<div class="col-4 col-md-2">
<div class="metric text-danger"><?php echo db_int($m, 'jobs_error'); ?></div>
<div class="metric-label">Erreurs</div>
</div>
<div class="col-6 col-md-3">
<div class="metric">
<?php echo db_int($m, 'scheduled_errors'); ?> /
<?php echo db_int($m, 'scheduled_jobs'); ?>
</div>
<div class="metric-label">Full / Incr.</div>
</div>
<div class="col-6 col-md-3">
<div class="metric">
<?php echo db_int($m, 'user_errors'); ?> /
<?php echo db_int($m, 'user_jobs'); ?>
</div>
<div class="metric-label">User backup</div>
</div>
</div>
<div class="mt-2 small">
Incr. &gt; 70 GB :
<strong class="<?php echo $largeClass; ?>">
<?php echo db_int($m, 'large_incremental_clients'); ?>
</strong>
</div>
<?php endif; ?>
</div>
</div>
<?php
}
function render_error_table($id, $rows, $emptyText = 'Aucun job dans cette catégorie.') {
?>
<?php if (count($rows) === 0) : ?>
<p class="text-muted mb-0"><?php echo h($emptyText); ?></p>
<?php else : ?>
<table
class="table table-bordered table-hover table-sm report-table"
id="<?php echo h($id); ?>"
data-sortable="true"
data-search="true"
data-show-export="true"
data-export-types='["csv","excel"]'
>
<thead>
<tr>
<th data-field="master" data-sortable="true">Master</th>
<th data-field="client" data-sortable="true">Client</th>
<th data-field="policy" data-sortable="true">Policy</th>
<th data-field="schedule" data-sortable="true">Schedule</th>
<th data-field="status" data-sortable="true">Status</th>
<th data-field="description" data-sortable="true">Description</th>
<th data-field="start" data-sortable="true">Début</th>
<th data-field="end" data-sortable="true">Fin</th>
<th data-field="size" data-sortable="true">Taille</th>
<th data-field="job" data-sortable="true">Job</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r) : ?>
<tr class="table-danger">
<td><?php echo h($r['master'] ?? ''); ?></td>
<td><strong><?php echo h($r['client'] ?? ''); ?></strong></td>
<td><?php echo h($r['policy'] ?? ''); ?></td>
<td><?php echo h($r['schedule_type'] ?? ''); ?></td>
<td><strong><?php echo h($r['status'] ?? ''); ?></strong></td>
<td><?php echo h($r['status_desc'] ?? 'Description non trouvée'); ?></td>
<td><?php echo h(fmt_dt($r['start_date'] ?? null)); ?></td>
<td><?php echo h(fmt_dt($r['end_date'] ?? null)); ?></td>
<td><?php echo h(fmt_gb_from_kb($r['size_kb'] ?? null)); ?></td>
<td><?php echo h($r['job'] ?? ''); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif;
}
function render_warning_table($id, $rows) {
?>
<?php if (count($rows) === 0) : ?>
<p class="text-muted mb-0">Aucune incrémentale &gt; 70 GB.</p>
<?php else : ?>
<table
class="table table-bordered table-hover table-sm report-table"
id="<?php echo h($id); ?>"
data-sortable="true"
data-search="true"
data-show-export="true"
data-export-types='["csv","excel"]'
>
<thead>
<tr>
<th data-field="master" data-sortable="true">Master</th>
<th data-field="client" data-sortable="true">Client</th>
<th data-field="policy" data-sortable="true">Policy</th>
<th data-field="size" data-sortable="true">Taille</th>
<th data-field="status" data-sortable="true">Status</th>
<th data-field="start" data-sortable="true">Début</th>
<th data-field="end" data-sortable="true">Fin</th>
<th data-field="job" data-sortable="true">Job</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r) : ?>
<tr class="table-warning">
<td><?php echo h($r['master'] ?? ''); ?></td>
<td><strong><?php echo h($r['client'] ?? ''); ?></strong></td>
<td><?php echo h($r['policy'] ?? ''); ?></td>
<td class="warning-size"><?php echo h(fmt_gb_from_kb($r['size_kb'] ?? null)); ?></td>
<td><span class="badge text-bg-success"><?php echo h($r['status'] ?? '0'); ?> - OK</span></td>
<td><?php echo h(fmt_dt($r['start_date'] ?? null)); ?></td>
<td><?php echo h(fmt_dt($r['end_date'] ?? null)); ?></td>
<td><?php echo h($r['job'] ?? ''); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif;
}
function render_accordion_block($id, $title, $icon, $badgeClass, $count, $renderer, $expanded = false) {
$collapsed = $expanded ? '' : 'collapsed';
$show = $expanded ? 'show' : '';
$aria = $expanded ? 'true' : 'false';
?>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button <?php echo $collapsed; ?>"
type="button"
data-bs-toggle="collapse"
data-bs-target="#c_<?php echo h($id); ?>"
aria-expanded="<?php echo $aria; ?>">
<i class="bi <?php echo h($icon); ?> me-2"></i>
<span class="block-title"><?php echo h($title); ?></span>
<span class="badge <?php echo h($badgeClass); ?> ms-3 block-count"><?php echo (int)$count; ?></span>
</button>
</h2>
<div id="c_<?php echo h($id); ?>" class="accordion-collapse collapse <?php echo $show; ?>">
<div class="accordion-body p-2">
<?php $renderer(); ?>
</div>
</div>
</div>
<?php
}
?>
<div class="container-fluid" id="content">
<div class="row flex-nowrap">
<div class="col-auto col-md-2 col-xl-2 px-sm-2 px-0 bg-dark vh-100 position-sticky top-0"
style="-ms-flex:0 0 230px; flex:0 0 230px;">
<?php include $_SERVER['DOCUMENT_ROOT'] . "/navbar.html"; ?>
</div>
<div class="col py-3">
<!-- ================================================================
Titre global
================================================================ -->
<h2>
<span class="badge text-bg-secondary w-100 text-wrap" id="ERR">
NetBackup Daily —
<span class="text-light"><?php echo $masterCount; ?> masters</span> —
<span class="title-ok"><?php echo $mastersOk; ?> OK</span> -
<span class="text-warning"><?php echo $mastersWarning; ?> Warning</span> -
<span class="text-danger"><?php echo $mastersCritical; ?> Critical</span>
</span>
</h2>
<div class="container-fluid px-0">
<!-- ================================================================
Compteurs globaux
================================================================ -->
<div class="row g-2 mt-1 mb-3">
<div class="col-6 col-md-3">
<div class="card shadow-sm summary-card">
<div class="card-body text-center">
<div class="fs-3 fw-bold"><?php echo $totalJobs; ?></div>
<div class="text-muted small">Jobs analysés</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card shadow-sm summary-card">
<div class="card-body text-center">
<div class="fs-3 fw-bold text-danger"><?php echo $totalErrors; ?></div>
<div class="text-muted small">Jobs en erreur</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card shadow-sm summary-card">
<div class="card-body text-center">
<div class="fs-3 fw-bold text-warning"><?php echo $totalLarge; ?></div>
<div class="text-muted small">Incr. &gt; 70 GB</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card shadow-sm summary-card">
<div class="card-body text-center">
<div class="fs-3 fw-bold text-danger"><?php echo $mastersCritical; ?></div>
<div class="text-muted small">Masters critiques</div>
</div>
</div>
</div>
</div>
<!-- ================================================================
Résumé par master
================================================================ -->
<h4 class="mt-3 mb-2">
<i class="bi bi-hdd-stack me-1"></i>
Résumé par master
</h4>
<div class="row">
<?php foreach ($summary as $m) : ?>
<div class="col-12 col-xl-6">
<?php render_master_card($m); ?>
</div>
<?php endforeach; ?>
</div>
<!-- ================================================================
Détails
================================================================ -->
<h4 class="mt-4 mb-2">
<i class="bi bi-list-check me-1"></i>
Détails
</h4>
<div class="accordion mb-4" id="acc_netbackup_daily">
<?php
render_accordion_block(
'scheduled_errors',
'Full / Incremental en erreur',
'bi-x-octagon',
'bg-danger',
count($scheduledErrors),
function () use ($scheduledErrors) {
render_error_table('t_scheduled_errors', $scheduledErrors);
},
count($scheduledErrors) > 0
);
render_accordion_block(
'user_errors',
'User Backup en erreur',
'bi-person-exclamation',
'bg-danger',
count($userErrors),
function () use ($userErrors) {
render_error_table('t_user_errors', $userErrors);
},
false
);
render_accordion_block(
'large_incrementals',
'Incrémentales > 70 GB',
'bi-exclamation-triangle',
'bg-warning text-dark',
count($largeIncrementals),
function () use ($largeIncrementals) {
render_warning_table('t_large_incrementals', $largeIncrementals);
},
count($largeIncrementals) > 0
);
?>
</div>
</div>
</div>
</div>
</div>
<script src="/js/switch.js"></script>
<script>
$(function () {
function isDark() {
return document.body.classList.contains('bg-dark');
}
function syncTheme($table) {
$table.toggleClass('table-dark', isDark());
}
function updateStickyOffset($item) {
var h = $item.find('.accordion-header').outerHeight() || 56;
$item.css('--acc-header-h', h + 'px');
}
function initTable($table) {
if ($table.length === 0) return;
if (!$table.data('bt-initialized')) {
$table.bootstrapTable();
$table.data('bt-initialized', true);
} else {
$table.bootstrapTable('resetView');
}
syncTheme($table);
updateStickyOffset($table.closest('.accordion-item'));
}
$('.accordion-collapse').on('shown.bs.collapse', function () {
initTable($(this).find('table.report-table'));
});
// Initialise aussi les accordéons ouverts par défaut au chargement.
$('.accordion-collapse.show').each(function () {
initTable($(this).find('table.report-table'));
});
$('#lightSwitch').on('change', function () {
setTimeout(function () {
$('table.report-table').each(function () {
syncTheme($(this));
});
}, 0);
});
$(window).on('resize', function () {
$('.accordion-item').each(function () {
updateStickyOffset($(this));
});
});
});
</script>
</body>
</html>

View File

@@ -6,170 +6,391 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Page Title -->
<title>Infra Reports IT</title>
<link rel="shortcut icon" type="image/png" href="/include/favicon-32x32.png">
<!-- JQuery -->
<script src="/js/jquery-3.6.1.min.js"></script>
<!-- Bootstrap -->
<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>
<!-- Bootstrap-tables -->
<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>
<script src="/js/bootstrap.bundle.min.js" defer></script>
<script src="/js/jquery-3.6.1.min.js" defer></script>
</head>
<body class="bg-light text-dark">
<?php include $_SERVER['DOCUMENT_ROOT'] . "/include/all.php"; ?>
<?php // DATA
$windows = Invoke_Infra("select * from cmdb_srvall where DECOM is null and hostname not in (select server from maintenance_status where scom = 'Y' or zabbix = 'Y') and hostname <> '' order by hostname asc");
$linux = Invoke_aixcmdb("select h.hostname, h.hbtime, h.prevtime from heartbeat h left join srvall s on s.hostname = h.hostname where h.mainttime is null and h.decomtime is null and s.os_type = 'LINUX' order by h.hostname");
$aix = Invoke_aixcmdb("select h.hostname, h.hbtime, h.prevtime from heartbeat h left join srvall s on s.hostname = h.hostname where h.mainttime is null and h.decomtime is null and s.os_type = 'AIX' order by h.hostname");
$other = Invoke_aixcmdb("select h.hostname, h.hbtime, h.prevtime from heartbeat h left join srvall s on s.hostname = h.hostname where h.mainttime is null and h.decomtime is null and s.os_type is null order by h.hostname");
$lok = "<div class='row'>";$lko = "<div class='row'>";$lcount=0;
foreach($linux as $s){
if($s['HBTIME'] < date('Y-m-d H:i:s', strtotime(' -11 minutes '))){
$lko .= "<div class='col-6'><span class='badge bg-danger'>".strtoupper($s['HOSTNAME'])." since ".explode(".",$s['PREVTIME'])[0]."</span></div>";
$lcount++;
}else{
$lok .= "<div class='col-2'><span class='badge bg-success'>".strtoupper($s['HOSTNAME']." ")."</span></div>";
}
}
if($lcount){$color = "DarkOrange";$msg = $lcount." issues";}else{$color = "green";$msg = "OK";}
$aok = "<div class='row'>";$ako = "<div class='row'>";$acount=0;
foreach($aix as $s){
if($s['HBTIME'] < date('Y-m-d H:i:s', strtotime(' -11 minutes '))){
$ako .= "<div class='col-6'><span class='badge bg-danger'>".strtoupper($s['HOSTNAME'])." since".explode(".",$s['PREVTIME'])[0]."</span></div>";
$acount++;
}else{
$aok .= "<div class='col-2'><span class='badge bg-success'>".strtoupper($s['HOSTNAME']." ")."</span></div>";
}
}
$ook = "<div class='row'>";$oko = "<div class='row'>";$ocount=0;
foreach($other as $s){
if($s['HBTIME'] < date('Y-m-d H:i:s', strtotime(' -11 minutes '))){
$oko .= "<div class='col-6'><span class='badge bg-danger'>".strtoupper($s['HOSTNAME'])." since".explode(".",$s['PREVTIME'])[0]."</span></div>";
$ocount++;
}else{
$ook .= "<div class='col-2'><span class='badge bg-success'>".strtoupper($s['HOSTNAME']." ")."</span></div>";
}
}
$wok = "<div class='row'>";$wko = "<div class='row'>";$wcount=0;
foreach($windows as $s){
if($s['heartbeat'] < date('Y-m-d H:i:s', strtotime(' -16 minutes '))){
$wko .= "<div class='col-6'><span class='badge bg-danger'>".strtoupper($s['hostname'])." since ".explode(".",$s['heartbeat'])[0]."</span></div>";
$wcount++;
}else{
$wok .= "<div class='col-2'><span class='badge bg-success'>".strtoupper($s['hostname']." ")."</span></div>";
<?php
include $_SERVER['DOCUMENT_ROOT'] . "/include/all.php";
/**
* Récupère une valeur dans un tableau sans dépendre de la casse
* utilisée par le pilote ODBC.
*/
function getRowValue(array $row, string $key, $default = null)
{
if (array_key_exists($key, $row)) {
return $row[$key];
}
$upperKey = strtoupper($key);
if (array_key_exists($upperKey, $row)) {
return $row[$upperKey];
}
$lowerKey = strtolower($key);
if (array_key_exists($lowerKey, $row)) {
return $row[$lowerKey];
}
return $default;
}
/**
* Génère les listes de serveurs OK et en erreur.
*/
function buildHeartbeatStatus(
array $servers,
string $hostnameKey,
string $heartbeatKey,
string $previousHeartbeatKey,
string $limit
): array {
$okItems = [];
$koItems = [];
$issueCount = 0;
foreach ($servers as $server) {
$hostname = trim((string)getRowValue($server, $hostnameKey, ''));
if ($hostname === '') {
continue;
}
$heartbeat = getRowValue($server, $heartbeatKey);
$previousHeartbeat = getRowValue(
$server,
$previousHeartbeatKey,
$heartbeat
);
$hostnameDisplay = htmlspecialchars(
strtoupper($hostname),
ENT_QUOTES,
'UTF-8'
);
/*
* Une date absente est considérée comme une erreur.
* Les dates au format Y-m-d H:i:s peuvent être comparées
* directement sous forme de chaînes.
*/
$isKo = empty($heartbeat) || $heartbeat < $limit;
if ($isKo) {
$previousDisplay = 'unknown';
if (!empty($previousHeartbeat)) {
$previousDisplay = explode('.', (string)$previousHeartbeat)[0];
$previousDisplay = htmlspecialchars(
$previousDisplay,
ENT_QUOTES,
'UTF-8'
);
}
$koItems[] = "
<div class=\"col-12 col-md-6 mb-1\">
<span class=\"badge bg-danger text-wrap\">
{$hostnameDisplay} since {$previousDisplay}
</span>
</div>
";
$issueCount++;
} else {
$okItems[] = "
<div class=\"col-6 col-md-4 col-xl-2 mb-1\">
<span class=\"badge bg-success text-wrap\">
{$hostnameDisplay}
</span>
</div>
";
}
}
return [
'ok' => implode('', $okItems),
'ko' => implode('', $koItems),
'issues' => $issueCount
];
}
/**
* Affiche une section de l'accordéon.
*/
function displayHeartbeatSection(
string $id,
string $title,
array $servers,
array $status,
string $deviceLabel
): void {
$count = count($servers);
$issues = $status['issues'];
$backgroundColor = $issues > 0 ? 'DarkOrange' : 'green';
$message = $issues > 0
? "<strong> → {$issues} issue(s)</strong>"
: 'OK';
?>
<div class="card mb-3" style="background-color: <?= $backgroundColor ?>;">
<h4 class="card-header">
<button
class="btn text-white fs-3 text-start w-100"
type="button"
data-bs-toggle="collapse"
data-bs-target="#<?= htmlspecialchars($id, ENT_QUOTES, 'UTF-8') ?>"
aria-expanded="false"
aria-controls="<?= htmlspecialchars($id, ENT_QUOTES, 'UTF-8') ?>"
>
<strong>
<?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?> :
<?= $count ?>
<?= htmlspecialchars($deviceLabel, ENT_QUOTES, 'UTF-8') ?>
<?= $message ?>
</strong>
</button>
</h4>
</div>
<div
id="<?= htmlspecialchars($id, ENT_QUOTES, 'UTF-8') ?>"
class="accordion-collapse collapse"
data-bs-parent="#accordion"
>
<div class="card card-body mb-3 bg-light">
<?php if ($issues > 0): ?>
<h5 class="mb-3">Issues</h5>
<div class="row">
<?= $status['ko'] ?>
</div>
<?php endif; ?>
<?php if ($status['ok'] !== ''): ?>
<?php if ($issues > 0): ?>
<hr>
<?php endif; ?>
<h5 class="mb-3">OK</h5>
<div class="row">
<?= $status['ok'] ?>
</div>
<?php endif; ?>
</div>
</div>
<?php
}
/*
* Seuils de heartbeat.
*/
$unixHeartbeatLimit = date(
'Y-m-d H:i:s',
time() - (11 * 60)
);
$windowsHeartbeatLimit = date(
'Y-m-d H:i:s',
time() - (16 * 60)
);
/*
* Serveurs Windows.
*/
$windows = Invoke_Infra("
SELECT
c.hostname,
c.heartbeat
FROM cmdb_srvall c
WHERE c.decom IS NULL
AND c.hostname <> ''
AND NOT EXISTS (
SELECT 1
FROM maintenance_status m
WHERE m.server = c.hostname
AND (
m.scom = 'Y'
OR m.zabbix = 'Y'
)
)
ORDER BY c.hostname ASC
");
/*
* Linux, AIX et autres :
* une seule requête au lieu de trois.
*/
$unixServers = Invoke_aixcmdb("
SELECT
h.hostname,
h.hbtime,
h.prevtime,
s.os_type
FROM heartbeat h
LEFT JOIN srvall s
ON s.hostname = h.hostname
WHERE h.mainttime IS NULL
AND h.decomtime IS NULL
AND (
s.os_type IN ('LINUX', 'AIX')
OR s.os_type IS NULL
)
ORDER BY h.hostname ASC
");
/*
* Répartition des résultats Unix.
*/
$linux = [];
$aix = [];
$other = [];
foreach ($unixServers as $server) {
$osType = strtoupper(
trim((string)getRowValue($server, 'os_type', ''))
);
switch ($osType) {
case 'LINUX':
$linux[] = $server;
break;
case 'AIX':
$aix[] = $server;
break;
default:
$other[] = $server;
break;
}
}
/*
* Génération des états.
*/
$windowsStatus = buildHeartbeatStatus(
$windows,
'hostname',
'heartbeat',
'heartbeat',
$windowsHeartbeatLimit
);
$linuxStatus = buildHeartbeatStatus(
$linux,
'hostname',
'hbtime',
'prevtime',
$unixHeartbeatLimit
);
$aixStatus = buildHeartbeatStatus(
$aix,
'hostname',
'hbtime',
'prevtime',
$unixHeartbeatLimit
);
$otherStatus = buildHeartbeatStatus(
$other,
'hostname',
'hbtime',
'prevtime',
$unixHeartbeatLimit
);
?>
<!-- HTML -->
<div class="container-fluid" id="content">
<div class="row flex-nowrap">
<!-- Left NAVBAR -->
<div class="col-auto col-md-2 col-xl-2 px-sm-2 px-0 bg-dark vh-100 position-sticky top-0" style="-ms-flex: 0 0 230px;flex: 0 0 230px;">
<!-- Left navbar -->
<div
class="col-auto col-md-2 col-xl-2 px-sm-2 px-0 bg-dark vh-100 position-sticky top-0"
style="-ms-flex: 0 0 230px; flex: 0 0 230px;"
>
<?php include $_SERVER['DOCUMENT_ROOT'] . "/navbar.html"; ?>
</div>
<!-- Display -->
<div class="col py-3">
<!-- Page Title -->
<h1><span class="badge text-bg-secondary font-weight-bold" style="width:100%;"><?php echo $ti_17;?></span></h1>
<main class="col py-3">
<!-- Page title -->
<h1>
<span
class="badge text-bg-secondary fw-bold"
style="width: 100%;"
>
<?= htmlspecialchars((string)$ti_17, ENT_QUOTES, 'UTF-8') ?>
</span>
</h1>
<!-- Main content -->
<div class="container">
<!-- TABLE -->
<div>
<div id="accordion">
<?php if($wcount){$color = "DarkOrange";$msg = "<b> --> ".$wcount." issue(s)</b>";}else{$color = "green";$msg = "OK";} ?>
<div class="card" style='background-color:<?php echo $color ?>;'>
<h4 class="card-header">
<a class="btn text-white fs-3" data-bs-toggle="collapse" href="#windows"><b>Windows : <?php echo count($windows)." $w_devices ". $msg; ?></b></a>
</h4>
</div>
<div id="windows" class="collapse" data-bs-parent="#accordion">
<?php echo "<br><h5>".$wko.'</div><br><br>'.$wok."</div></h5>"; ?>
</div>
<br>
<div class="accordion" id="accordion">
<?php if($lcount){$color = "DarkOrange";$msg = "<b> --> ".$lcount." issue(s)</b>";}else{$color = "green";$msg = "OK";} ?>
<div class="card" style='background-color:<?php echo $color ?>;'>
<h4 class="card-header">
<a class="btn text-white fs-3" data-bs-toggle="collapse" href="#linux"><b>Linux : <?php echo count($linux)." $w_devices ". $msg; ?></b></a>
</h4>
</div>
<div id="linux" class="collapse" data-bs-parent="#accordion">
<?php echo "<br><h5>".$lko.'</div><br><br>'.$lok."</div></h5>"; ?>
</div>
<br>
<?php
displayHeartbeatSection(
'windows',
'Windows',
$windows,
$windowsStatus,
$w_devices
);
<?php if($acount){$color = "DarkOrange";$msg = "<b> --> ".$acount." issue(s)</b>";}else{$color = "green";$msg = "OK";} ?>
<div class="card" style='background-color:<?php echo $color ?>;'>
<h4 class="card-header text-white">
<a class="btn text-white fs-3" data-bs-toggle="collapse" href="#aix"><b>AIX : <?php echo count($aix)." $w_devices ". $msg; ?></b></a>
</h4>
</div>
<div id="aix" class="collapse" data-bs-parent="#accordion">
<?php echo "<br><h5>".$ako.'</div><br><br>'.$aok."</div></h5>"; ?>
</div>
<br>
displayHeartbeatSection(
'linux',
'Linux',
$linux,
$linuxStatus,
$w_devices
);
<?php if($ocount){$color = "DarkOrange";$msg = "<b> --> ".$ocount." issue(s)</b>";}else{$color = "green";$msg = "OK";} ?>
<div class="card" style='background-color:<?php echo $color ?>;'>
<h4 class="card-header text-white">
<a class="btn text-white fs-3" data-bs-toggle="collapse" href="#other"><b>Other : <?php echo count($other)." $w_devices ". $msg; ?></b></a>
</h4>
</div>
<div id="other" class="collapse" data-bs-parent="#accordion">
<?php echo "<br><h5>".$oko.'</div><br><br>'.$ook."</div></h5>"; ?>
</div>
<br>
displayHeartbeatSection(
'aix',
'AIX',
$aix,
$aixStatus,
$w_devices
);
displayHeartbeatSection(
'other',
'Other',
$other,
$otherStatus,
$w_devices
);
?>
</div>
</div>
<!-- End of main content -->
</div>
</div>
</main>
</div>
</body>
</div>
<script src="/js/switch.js"></script>
</HTML>
<SCRIPT>
let table = $('#t1');
$(document).ready(function() {
table.DataTable({
scrollY: '50vh',
scrollCollapse: true,
paging: false,
});
});
$(function () {
let options = table.bootstrapTable('getOptions');
options.height= document.getElementById('content').clientHeight-170;
table.bootstrapTable('refreshOptions',options);
});
function tableresize() {
let options = table.bootstrapTable('getOptions');
options.height= document.getElementById('content').clientHeight-170;
table.bootstrapTable('refreshOptions',options);
}
window.addEventListener("resize", tableresize);
</SCRIPT>
</body>
</html>