Files
Web-Infra-Reports-IT/index.php
sva-e025532 c185d099d6 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.
2026-09-14 10:32:15 +02:00

1382 lines
34 KiB
PHP

<?php
include $_SERVER['DOCUMENT_ROOT']."/include/all.php";
/* ============================================================
Helpers
============================================================ */
function mysqlScalar($conn, $query, $field) {
$result = $conn->query($query);
if (!$result) {
return 0;
}
$row = mysqli_fetch_assoc($result);
return (int)($row[$field] ?? 0);
}
function fetchInfraRows($conn, $query) {
$rows = [];
if ($conn instanceof PDO) {
$stmt = $conn->query($query);
return $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
}
if (function_exists('sqlsrv_query')) {
$stmt = @sqlsrv_query($conn, $query);
if ($stmt !== false) {
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$rows[] = $row;
}
return $rows;
}
}
if (function_exists('odbc_exec')) {
$stmt = @odbc_exec($conn, $query);
if ($stmt !== false) {
while ($row = odbc_fetch_array($stmt)) {
$rows[] = $row;
}
return $rows;
}
}
return $rows;
}
function normalizeDateValue($value) {
if ($value instanceof DateTimeInterface) {
return $value->format('Y-m-d');
}
if ($value === null || trim((string)$value) === '') {
return null;
}
return substr((string)$value, 0, 10);
}
function normalizeOsKey($os) {
$os = trim((string)$os);
if ($os === '') {
return 'Unknown';
}
// Windows Server / Client
if (preg_match('/Windows/i', $os)) {
// Server versions
if (preg_match('/2003/', $os)) return 'Windows 2003';
if (preg_match('/2008/', $os)) return 'Windows 2008';
if (preg_match('/2012/', $os)) return 'Windows 2012';
if (preg_match('/2016/', $os)) return 'Windows 2016';
if (preg_match('/2019/', $os)) return 'Windows 2019';
if (preg_match('/2022/', $os)) return 'Windows 2022';
if (preg_match('/2025/', $os)) return 'Windows 2025';
// Clients
if (preg_match('/Windows\s*10/i', $os)) return 'Windows 10';
if (preg_match('/Windows\s*11/i', $os)) return 'Windows 11';
return 'Windows';
}
if (preg_match('/Windows\s+(10|11)/i', $os, $match)) {
return 'Windows ' . $match[1];
}
// RedHat / RHEL
// Examples handled:
// Linux / RedHat 8.8-0.8 -> RedHat 8.8
// Linux / RedHat 8-8.0-8 -> RedHat 8.8
// Red Hat Enterprise Linux 8.10 -> RedHat 8.10
if (preg_match('/Red\s*Hat|RedHat|RHEL/i', $os)) {
if (preg_match('/Red\s*Hat\s+(\d+)[\.\-](\d+)/i', $os, $match)) {
return 'RedHat ' . $match[1] . '.' . $match[2];
}
if (preg_match('/RedHat\s+(\d+)[\.\-](\d+)/i', $os, $match)) {
return 'RedHat ' . $match[1] . '.' . $match[2];
}
if (preg_match('/RHEL\s+(\d+)[\.\-](\d+)/i', $os, $match)) {
return 'RedHat ' . $match[1] . '.' . $match[2];
}
if (preg_match('/release\s+(\d+)[\.\-](\d+)/i', $os, $match)) {
return 'RedHat ' . $match[1] . '.' . $match[2];
}
if (preg_match('/(\d+)[\.\-](\d+)/', $os, $match)) {
return 'RedHat ' . $match[1] . '.' . $match[2];
}
if (preg_match('/(\d+)/', $os, $match)) {
return 'RedHat ' . $match[1];
}
return 'RedHat';
}
// AlmaLinux
if (preg_match('/AlmaLinux/i', $os)) {
if (preg_match('/AlmaLinux\s+(\d+)[\.\-](\d+)/i', $os, $match)) {
return 'AlmaLinux ' . $match[1] . '.' . $match[2];
}
if (preg_match('/release\s+(\d+)[\.\-](\d+)/i', $os, $match)) {
return 'AlmaLinux ' . $match[1] . '.' . $match[2];
}
if (preg_match('/(\d+)[\.\-](\d+)/', $os, $match)) {
return 'AlmaLinux ' . $match[1] . '.' . $match[2];
}
if (preg_match('/(\d+)/', $os, $match)) {
return 'AlmaLinux ' . $match[1];
}
return 'AlmaLinux';
}
// Rocky Linux
if (preg_match('/Rocky/i', $os)) {
if (preg_match('/(\d+)[\.\-](\d+)/', $os, $match)) {
return 'Rocky Linux ' . $match[1] . '.' . $match[2];
}
if (preg_match('/(\d+)/', $os, $match)) {
return 'Rocky Linux ' . $match[1];
}
return 'Rocky Linux';
}
// CentOS
if (preg_match('/CentOS/i', $os)) {
if (preg_match('/(\d+)[\.\-](\d+)/', $os, $match)) {
return 'CentOS ' . $match[1] . '.' . $match[2];
}
if (preg_match('/(\d+)/', $os, $match)) {
return 'CentOS ' . $match[1];
}
return 'CentOS';
}
// Ubuntu
if (preg_match('/Ubuntu/i', $os)) {
if (preg_match('/(\d+)\.(\d+)/', $os, $match)) {
return 'Ubuntu ' . $match[1] . '.' . $match[2];
}
if (preg_match('/(\d+)/', $os, $match)) {
return 'Ubuntu ' . $match[1];
}
return 'Ubuntu';
}
// Debian
if (preg_match('/Debian/i', $os)) {
if (preg_match('/(\d+)\.(\d+)/', $os, $match)) {
return 'Debian ' . $match[1] . '.' . $match[2];
}
if (preg_match('/(\d+)/', $os, $match)) {
return 'Debian ' . $match[1];
}
return 'Debian';
}
// SUSE / SLES
if (preg_match('/SUSE|SLES/i', $os)) {
if (preg_match('/(\d+).*?(?:SP\s*|\.)(\d+)/i', $os, $match)) {
return 'SLES ' . $match[1] . '.' . $match[2];
}
if (preg_match('/(\d+)/', $os, $match)) {
return 'SLES ' . $match[1];
}
return 'SLES';
}
// Oracle Linux
if (preg_match('/Oracle Linux/i', $os)) {
if (preg_match('/(\d+)\.(\d+)/', $os, $match)) {
return 'Oracle Linux ' . $match[1] . '.' . $match[2];
}
if (preg_match('/(\d+)/', $os, $match)) {
return 'Oracle Linux ' . $match[1];
}
return 'Oracle Linux';
}
// AIX
if (preg_match('/AIX\s+6100/i', $os)) {
return 'AIX <7.2';
}
if (preg_match('/AIX\s+7100/i', $os)) {
return 'AIX <7.2';
}
if (preg_match('/AIX\s+7200/i', $os)) {
return 'AIX 7.2';
}
if (preg_match('/AIX\s+7300/i', $os)) {
return 'AIX 7.3';
}
if (preg_match('/AIX/i', $os)) {
return 'AIX <7.2';
}
if (preg_match('/Unix/i', $os)) {
return 'Unix';
}
return $os;
}
function isWindowsOs($os) {
return preg_match('/Windows/i', (string)$os) === 1;
}
function isAixOs($os) {
return preg_match('/AIX/i', (string)$os) === 1;
}
function isLinuxOs($os) {
$os = trim((string)$os);
if ($os === '') {
return false;
}
if (isWindowsOs($os) || isAixOs($os)) {
return false;
}
if (preg_match('/Unix/i', $os)) {
return false;
}
return preg_match('/Linux|Red Hat|RedHat|RHEL|AlmaLinux|Rocky|CentOS|Ubuntu|Debian|SUSE|SLES|Oracle|Data Domain OS|/i', $os) === 1;
}
function hasS1Agent($value) {
$value = trim((string)$value);
if ($value === '') {
return false;
}
if (in_array($value, ['N', 'Non supported OS'], true)) {
return false;
}
return true;
}
function incrementCounter(&$array, $key) {
if (!isset($array[$key])) {
$array[$key] = 0;
}
$array[$key]++;
}
function getLifecycleStatus($osKey, $lifecycleMap) {
$match = null;
$bestLength = 0;
if (isset($lifecycleMap[$osKey])) {
$match = $lifecycleMap[$osKey];
} else {
foreach ($lifecycleMap as $key => $data) {
if (stripos($osKey, $key) === 0 && strlen($key) > $bestLength) {
$match = $data;
$bestLength = strlen($key);
}
}
}
if (!$match || empty($match['SupportEndDate'])) {
return [
'label' => 'Unknown',
'color' => '#6c757d',
'end' => null
];
}
$today = new DateTimeImmutable('today');
$warningLimit = $today->modify('+1 year');
$supportEndDate = new DateTimeImmutable($match['SupportEndDate']);
if ($supportEndDate < $today) {
return [
'label' => 'Expired',
'color' => '#dc3545',
'end' => $supportEndDate->format('Y-m-d')
];
}
if ($supportEndDate < $warningLimit) {
return [
'label' => 'Warning',
'color' => '#fd7e14',
'end' => $supportEndDate->format('Y-m-d')
];
}
return [
'label' => 'Supported',
'color' => '#198754',
'end' => $supportEndDate->format('Y-m-d')
];
}
function sortOsCounter(&$counter) {
uksort($counter, function($a, $b) {
return strnatcasecmp($a, $b);
});
}
function chartFromCounter($counter, $lifecycleMap = null, $limit = null, $groupOthers = false) {
uksort($counter, function($a, $b) use ($lifecycleMap) {
$statusA = $lifecycleMap ? getLifecycleStatus($a, $lifecycleMap) : null;
$statusB = $lifecycleMap ? getLifecycleStatus($b, $lifecycleMap) : null;
$dateA = ($statusA && !empty($statusA['end'])) ? $statusA['end'] : '0000-00-00';
$dateB = ($statusB && !empty($statusB['end'])) ? $statusB['end'] : '0000-00-00';
return strcmp($dateB, $dateA); // décroissant
});
if ($limit !== null && count($counter) > $limit) {
$limited = array_slice($counter, 0, $limit, true);
$remaining = array_slice($counter, $limit, null, true);
if ($groupOthers && count($remaining) > 0) {
$limited['Others'] = array_sum($remaining);
}
$counter = $limited;
}
$labels = [];
$data = [];
$colors = [];
$palette = [
'#198754',
'#fd7e14',
'#0d6efd',
'#6f42c1',
'#20c997',
'#dc3545',
'#6c757d',
'#0dcaf0',
'#6610f2',
'#d63384',
'#2f855a',
'#495057'
];
$i = 0;
foreach ($counter as $key => $value) {
$labels[] = $key;
$data[] = $value;
if ($lifecycleMap !== null && $key !== 'Others') {
$colors[] = getLifecycleStatus($key, $lifecycleMap)['color'];
} else {
$colors[] = $palette[$i % count($palette)];
}
$i++;
}
return [
'labels' => $labels,
'data' => $data,
'colors' => $colors
];
}
/* ============================================================
Data loading
============================================================ */
$connEntry = DB_ENTRY02();
$globalRows = [];
$result = $connEntry->query("
SELECT
Server,
AD,
GLPI,
SCCM,
EPO,
NBU,
OS,
crit,
dpt,
virtual,
S1
FROM GlobalCrossover
WHERE OS IS NOT NULL
AND LTRIM(RTRIM(OS)) <> ''
");
if ($result) {
while ($row = mysqli_fetch_assoc($result)) {
$globalRows[] = $row;
}
}
$connInfra = DB_INFRA();
$lifecycleMap = [];
$lifecycleRows = fetchInfraRows($connInfra, "
SELECT
OSKey,
OSFamily,
SupportEndDate,
Comment
FROM dbo.OSLifeCycle
");
foreach ($lifecycleRows as $row) {
$key = trim((string)($row['OSKey'] ?? ''));
if ($key === '') {
continue;
}
$lifecycleMap[$key] = [
'OSFamily' => $row['OSFamily'] ?? '',
'SupportEndDate' => normalizeDateValue($row['SupportEndDate'] ?? null),
'Comment' => $row['Comment'] ?? ''
];
}
/* ============================================================
KPI + chart data
============================================================ */
$totalWindows = 0;
$sccmOk = 0;
$s1Eligible = 0;
$s1Ok = 0;
$windowsAll = [];
$linuxAix = [];
$typeCounter = [
'Physical' => 0,
'Virtual' => 0
];
$statusCounter = [
'Supported' => 0,
'Warning' => 0,
'Expired' => 0,
'Unknown' => 0
];
$osRowsForTable = [];
foreach ($globalRows as $row) {
$os = $row['OS'] ?? '';
$osKey = normalizeOsKey($os);
$department = strtoupper(trim((string)($row['dpt'] ?? '')));
if (isWindowsOs($os)) {
$totalWindows++;
if (trim((string)($row['SCCM'] ?? '')) === 'Y') {
$sccmOk++;
}
incrementCounter($windowsAll, $osKey);
}
// SentinelOne KPI = Windows + Linux only. AIX excluded.
if (isWindowsOs($os) || isLinuxOs($os)) {
$s1Eligible++;
if (hasS1Agent($row['S1'] ?? '')) {
$s1Ok++;
}
}
if (isLinuxOs($os) || isAixOs($os)) {
incrementCounter($linuxAix, $osKey);
}
if (($row['virtual'] ?? '') === 'Y') {
incrementCounter($typeCounter, 'Virtual');
}
if (($row['virtual'] ?? '') === 'N') {
incrementCounter($typeCounter, 'Physical');
}
$status = getLifecycleStatus($osKey, $lifecycleMap);
incrementCounter($statusCounter, $status['label']);
if (!isset($osRowsForTable[$osKey])) {
$osRowsForTable[$osKey] = [
'OSKey' => $osKey,
'Count' => 0,
'Status' => $status['label'],
'EndDate' => $status['end'] ?? '-'
];
}
$osRowsForTable[$osKey]['Count']++;
}
$sccmPercent = $totalWindows > 0 ? round($sccmOk * 100 / $totalWindows, 2) : 0;
$s1Percent = $s1Eligible > 0 ? round($s1Ok * 100 / $s1Eligible, 2) : 0;
$totalType = array_sum($typeCounter);
$virtualPercent = $totalType > 0 ? round($typeCounter['Virtual'] * 100 / $totalType, 1) : 0;
$physicalPercent = $totalType > 0 ? round($typeCounter['Physical'] * 100 / $totalType, 1) : 0;
$chartWindowsAll = chartFromCounter($windowsAll, $lifecycleMap);
$chartLinuxAix = chartFromCounter($linuxAix, $lifecycleMap, 14, true);
$chartSupport = [
'labels' => array_keys($statusCounter),
'data' => array_values($statusCounter),
'colors' => ['#198754', '#fd7e14', '#dc3545', '#6c757d']
];
/* ============================================================
Existing cards data
============================================================ */
$nbADactive = mysqlScalar($connEntry, "SELECT COUNT(*) AS total FROM adcomputers", "total");
$nbADinactive = mysqlScalar($connEntry, "SELECT COUNT(*) AS total FROM adcomputers WHERE enabled = 'False'", "total");
$nbnessus = mysqlScalar($connEntry, "
SELECT COUNT(*) AS total
FROM GlobalCrossover
WHERE EPO IS NOT NULL
AND LTRIM(RTRIM(EPO)) <> ''
AND EPO NOT IN ('Non supported OS', 'N')
", "total");
$connGlpi = DB_GLPI();
$nbglpi = mysqlScalar($connGlpi, "
SELECT COUNT(name) AS total
FROM glpi_computers
WHERE entities_id = 6
AND is_deleted = 0
AND states_ID = 2
AND computertypes_id IN (7,19)
AND name <> ''
", "total");
mysqli_close($connGlpi);
$connNbu = DB_ENTRY01();
$nbnbu = mysqlScalar($connNbu, "SELECT COUNT(DISTINCT server) AS total FROM nb_jobs_full", "total");
mysqli_close($connNbu);
uasort($osRowsForTable, function($a, $b) {
$dateA = ($a['EndDate'] === '-' || empty($a['EndDate'])) ? '9999-12-31' : $a['EndDate'];
$dateB = ($b['EndDate'] === '-' || empty($b['EndDate'])) ? '9999-12-31' : $b['EndDate'];
$dateCompare = strcmp($dateA, $dateB);
if ($dateCompare !== 0) {
return $dateCompare;
}
return $b['Count'] <=> $a['Count'];
});
?>
<!DOCTYPE html>
<html lang="en">
<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-1.13.1/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/chart.min.js"></script>
<style>
:root {
--page-bg: #f4f7fb;
--card-bg: #ffffff;
--border: #e6eaf0;
--text-main: #0f172a;
--text-muted: #64748b;
--shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
--shadow-soft: 0 4px 14px rgba(15, 23, 42, 0.06);
}
body {
background: var(--page-bg);
}
.main-content {
padding: 22px 26px;
overflow-x: hidden;
background: var(--page-bg);
color: var(--text-main);
min-height: 100vh;
}
body.text-light .main-content {
background: #1f2429;
color: #f8fafc;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 22px;
}
.page-title {
font-size: 1.65rem;
font-weight: 800;
display: flex;
align-items: center;
gap: 12px;
color: var(--text-main);
}
body.text-light .page-title {
color: #f8fafc;
}
.last-update {
color: var(--text-muted);
font-size: .85rem;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(7, minmax(150px, 1fr));
gap: 16px;
margin-bottom: 22px;
}
.kpi-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 14px;
box-shadow: var(--shadow);
padding: 20px 18px;
min-height: 150px;
display: flex;
align-items: center;
gap: 16px;
color: var(--text-main);
}
.kpi-icon {
width: 52px;
height: 52px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.55rem;
flex-shrink: 0;
}
.kpi-title {
font-size: .92rem;
font-weight: 800;
margin-bottom: 8px;
color: var(--text-main);
}
.kpi-value {
font-size: 1.75rem;
font-weight: 900;
line-height: 1.1;
color: var(--text-main);
}
.kpi-sub {
font-size: .82rem;
color: var(--text-muted);
margin-top: 6px;
}
.kpi-progress {
height: 5px;
background: #eef2f7;
border-radius: 999px;
overflow: hidden;
margin-top: 12px;
}
.kpi-progress > div {
height: 100%;
border-radius: 999px;
}
.env-line {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 10px;
align-items: center;
font-size: .88rem;
margin-top: 10px;
color: var(--text-main);
}
.env-line small {
color: var(--text-muted);
}
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr 2.35fr;
gap: 18px;
margin-bottom: 22px;
}
.panel {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 14px;
box-shadow: var(--shadow);
padding: 18px;
color: var(--text-main);
}
.panel-title {
font-size: 1.15rem;
font-weight: 850;
display: flex;
align-items: center;
gap: 10px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
margin-bottom: 12px;
color: var(--text-main);
}
.chart-wrap {
height: 260px;
position: relative;
}
.linux-chart-wrap {
height: 315px;
}
.support-panel {
padding-bottom: 20px;
}
.support-grid {
display: grid;
grid-template-columns: 285px 1fr 1.55fr;
gap: 20px;
align-items: stretch;
}
.support-chart {
height: 295px;
position: relative;
}
.status-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
}
.status-card {
border-radius: 14px;
padding: 18px;
border: 1px solid var(--border);
background: #fff;
box-shadow: var(--shadow-soft);
min-height: 142px;
}
.status-card .label {
font-weight: 850;
margin-bottom: 12px;
}
.status-card .value {
font-size: 1.6rem;
font-weight: 900;
}
.status-card .sub {
color: var(--text-muted);
font-size: .82rem;
}
.status-supported {
background: #ecfdf5;
border-color: #bbf7d0;
color: #15803d;
}
.status-warning {
background: #fff7ed;
border-color: #fed7aa;
color: #ea580c;
}
.status-expired {
background: #fef2f2;
border-color: #fecaca;
color: #dc2626;
}
.status-unknown {
background: #f8fafc;
border-color: #e2e8f0;
color: #64748b;
}
.os-table {
width: 100%;
font-size: .84rem;
color: var(--text-main);
}
.os-table th {
color: var(--text-muted);
font-weight: 800;
border-bottom: 1px solid var(--border);
padding: 8px;
white-space: nowrap;
}
.os-table td {
padding: 8px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
color: var(--text-main);
}
.badge-status {
border-radius: 999px;
padding: 4px 10px;
font-size: .74rem;
font-weight: 800;
display: inline-block;
}
.badge-Supported {
background: #dcfce7;
color: #15803d;
}
.badge-Warning {
background: #ffedd5;
color: #ea580c;
}
.badge-Expired {
background: #fee2e2;
color: #dc2626;
}
.badge-Unknown {
background: #e2e8f0;
color: #475569;
}
.footer-note {
color: var(--text-muted);
font-size: .82rem;
}
body.text-light .kpi-card,
body.text-light .panel {
background: #ffffff;
color: #0f172a;
}
body.text-light .kpi-card *,
body.text-light .panel *,
body.text-light .os-table,
body.text-light .os-table th,
body.text-light .os-table td {
color: inherit;
}
body.text-light .kpi-title,
body.text-light .kpi-value,
body.text-light .panel-title,
body.text-light .os-table td {
color: #0f172a;
}
body.text-light .kpi-sub,
body.text-light .last-update,
body.text-light .os-table th,
body.text-light .footer-note {
color: #64748b;
}
body.text-light .badge-Supported {
color: #15803d;
}
body.text-light .badge-Warning {
color: #ea580c;
}
body.text-light .badge-Expired {
color: #dc2626;
}
body.text-light .badge-Unknown {
color: #475569;
}
body.text-light .status-supported {
color: #15803d;
}
body.text-light .status-warning {
color: #ea580c;
}
body.text-light .status-expired {
color: #dc2626;
}
body.text-light .status-unknown {
color: #64748b;
}
@media (max-width: 1900px) {
.kpi-grid {
grid-template-columns: repeat(4, 1fr);
}
.dashboard-grid {
grid-template-columns: 1fr 1fr;
}
.dashboard-grid .panel:nth-child(3) {
grid-column: span 2;
}
.support-grid {
grid-template-columns: 1fr;
}
.status-cards {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 1200px) {
.kpi-grid,
.dashboard-grid {
grid-template-columns: 1fr;
}
.dashboard-grid .panel:nth-child(3) {
grid-column: span 1;
}
.status-cards {
grid-template-columns: repeat(2, 1fr);
}
}
.os-table-scroll {
max-height: 355px; /* environ 10 lignes */
overflow-y: auto;
padding-right: 6px;
}
.os-table-scroll thead th {
position: sticky;
top: 0;
background: #ffffff;
z-index: 2;
}
body.text-light .os-table-scroll thead th {
background: #ffffff;
}
.dashboard-grid-two {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin-bottom: 22px;
}
.large-chart-wrap {
height: 360px;
}
@media (max-width: 1400px) {
.dashboard-grid-two {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<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 vh-100 position-sticky top-0" style="-ms-flex:0 0 230px;flex:0 0 230px;">
<?php include $_SERVER['DOCUMENT_ROOT']."/navbar.html"; ?>
</div>
<main class="col main-content" style="zoom:80%">
<section class="kpi-grid">
<div class="kpi-card">
<div class="kpi-icon" style="background:#0d6efd;">
<i class="bi bi-server"></i>
</div>
<div>
<div class="kpi-title">Active Directory</div>
<div class="kpi-value"><?php echo $nbADactive; ?></div>
<div class="kpi-sub">
<?php echo $w_devices; ?> · <?php echo $nbADinactive; ?> inactive(s)
</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-icon" style="background:#6f42c1;">
<i class="bi bi-display"></i>
</div>
<div>
<div class="kpi-title">GLPI</div>
<div class="kpi-value"><?php echo $nbglpi; ?></div>
<div class="kpi-sub">Devices</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-icon" style="background:#0d6efd;">
<i class="bi bi-windows"></i>
</div>
<div style="width:100%;">
<div class="kpi-title">SCCM <span style="color:#64748b;">(Windows)</span></div>
<div class="kpi-value"><?php echo $sccmPercent; ?>%</div>
<div class="kpi-sub"><?php echo $sccmOk; ?> / <?php echo $totalWindows; ?> Windows</div>
<div class="kpi-progress">
<div style="width:<?php echo $sccmPercent; ?>%;background:#0d6efd;"></div>
</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-icon" style="background:#198754;">
<i class="bi bi-shield-check"></i>
</div>
<div>
<div class="kpi-title">NESSUS</div>
<div class="kpi-value"><?php echo $nbnessus; ?></div>
<div class="kpi-sub">Agents</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-icon" style="background:#6f42c1;">
<i class="bi bi-shield-lock"></i>
</div>
<div style="width:100%;">
<div class="kpi-title">SentinelOne</div>
<div class="kpi-value"><?php echo $s1Percent; ?>%</div>
<div class="kpi-sub"><?php echo $s1Ok; ?> / <?php echo $s1Eligible; ?> Windows + Linux</div>
<div class="kpi-progress">
<div style="width:<?php echo $s1Percent; ?>%;background:#6f42c1;"></div>
</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-icon" style="background:#fd7e14;">
<i class="bi bi-database-fill-lock"></i>
</div>
<div>
<div class="kpi-title">NetBackup</div>
<div class="kpi-value"><?php echo $nbnbu; ?></div>
<div class="kpi-sub">Clients</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-icon" style="background:#0d6efd;">
<i class="bi bi-pc-display"></i>
</div>
<div style="width:100%;">
<div class="kpi-title">Environment</div>
<div class="env-line">
<span><i class="bi bi-circle-fill" style="color:#198754;"></i> Virtual</span>
<b><?php echo $typeCounter['Virtual']; ?></b>
<small><?php echo $virtualPercent; ?>%</small>
</div>
<div class="env-line">
<span><i class="bi bi-circle-fill" style="color:#fd7e14;"></i> Physical</span>
<b><?php echo $typeCounter['Physical']; ?></b>
<small><?php echo $physicalPercent; ?>%</small>
</div>
</div>
</div>
</section>
<section class="dashboard-grid dashboard-grid-two">
<div class="panel">
<div class="panel-title">
<i class="bi bi-windows text-primary"></i>
Windows
</div>
<div class="chart-wrap large-chart-wrap">
<canvas id="ChartWindows"></canvas>
</div>
</div>
<div class="panel">
<div class="panel-title">
<i class="bi bi-ubuntu text-warning"></i>
Linux / AIX
</div>
<div class="chart-wrap large-chart-wrap">
<canvas id="ChartLinux"></canvas>
</div>
</div>
</section>
<section class="panel support-panel">
<div class="panel-title">
<i class="bi bi-shield-check text-primary"></i>
OS Support Status
</div>
<div class="support-grid">
<div class="support-chart">
<canvas id="ChartSupport"></canvas>
</div>
<div class="status-cards">
<?php foreach ($statusCounter as $label => $count): ?>
<?php
$class = 'status-' . strtolower($label);
$percent = count($globalRows) > 0 ? round($count * 100 / count($globalRows), 1) : 0;
?>
<div class="status-card <?php echo $class; ?>">
<div class="label"><?php echo $label; ?></div>
<div class="value"><?php echo $count; ?></div>
<div class="sub"><?php echo $percent; ?>%</div>
</div>
<?php endforeach; ?>
</div>
<div>
<div class="os-table-scroll">
<table class="os-table">
<thead>
<tr>
<th>OS</th>
<th>End of Support</th>
<th>Status</th>
<th class="text-end">Devices</th>
</tr>
</thead>
<tbody>
<?php foreach ($osRowsForTable as $row): ?>
<tr>
<td><?php echo htmlspecialchars($row['OSKey']); ?></td>
<td><?php echo htmlspecialchars($row['EndDate']); ?></td>
<td>
<span class="badge-status badge-<?php echo htmlspecialchars($row['Status']); ?>">
<?php echo htmlspecialchars($row['Status']); ?>
</span>
</td>
<td class="text-end"><?php echo (int)$row['Count']; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</section>
</main>
</div>
</div>
<script src="/js/switch.js"></script>
<script>
const chartWindowsAll = <?php echo json_encode($chartWindowsAll); ?>;
const chartLinuxAix = <?php echo json_encode($chartLinuxAix); ?>;
const chartSupport = <?php echo json_encode($chartSupport); ?>;
if (window.Chart && Chart.defaults) {
if (Chart.defaults.font) {
Chart.defaults.font.family = "'Segoe UI', Arial, sans-serif";
Chart.defaults.color = '#475569';
} else if (Chart.defaults.global) {
Chart.defaults.global.defaultFontFamily = "'Segoe UI', Arial, sans-serif";
Chart.defaults.global.defaultFontColor = '#475569';
}
}
function createDoughnut(canvasId, chartData, cutout = '62%', legendPosition = 'right') {
const canvas = document.getElementById(canvasId);
if (!canvas || !window.Chart) {
console.error('Chart.js not loaded or canvas missing:', canvasId);
return;
}
new Chart(canvas, {
type: 'doughnut',
data: {
labels: chartData.labels,
datasets: [{
data: chartData.data,
backgroundColor: chartData.colors,
borderColor: '#ffffff',
borderWidth: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
// Chart.js v3/v4
cutout: cutout,
// Chart.js v2
cutoutPercentage: parseInt(cutout, 10),
legend: {
display: true,
position: legendPosition,
labels: {
boxWidth: 12,
fontColor: '#334155',
padding: 14
}
},
plugins: {
legend: {
display: true,
position: legendPosition,
labels: {
boxWidth: 12,
boxHeight: 12,
color: '#334155',
padding: 14
}
},
tooltip: {
callbacks: {
label: function(context) {
const dataset = context.dataset || context.chart.data.datasets[0];
const value = Number(context.raw ?? dataset.data[context.dataIndex]);
const total = dataset.data.reduce((a, b) => Number(a) + Number(b), 0);
const percent = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
const label = context.label ?? context.chart.data.labels[context.dataIndex];
return `${label}: ${value} (${percent}%)`;
}
}
}
},
tooltips: {
callbacks: {
label: function(tooltipItem, data) {
const dataset = data.datasets[tooltipItem.datasetIndex];
const value = Number(dataset.data[tooltipItem.index]);
const total = dataset.data.reduce((a, b) => Number(a) + Number(b), 0);
const percent = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
const label = data.labels[tooltipItem.index];
return `${label}: ${value} (${percent}%)`;
}
}
}
}
});
}
document.addEventListener('DOMContentLoaded', function () {
createDoughnut('ChartWindows', chartWindowsAll, '58%', 'right');
createDoughnut('ChartLinux', chartLinuxAix, '58%', 'right');
createDoughnut('ChartSupport', chartSupport, '58%', 'right');
});
</script>
</body>
</html>