Files
Web-Infra-Reports-IT/AD/LocalAdmins.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

1582 lines
32 KiB
PHP

<?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 - Local Administrators</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>
html,
body {height: 100%;overflow: hidden;}
#content {
height: 100vh;
overflow: hidden;
}
#content > .row {
height: 100%;
}
#content .col.py-3 {
height: 100vh;
overflow: hidden;
}
/* ============================================================
Cards
============================================================ */
.summary-card {
min-height: 92px;
}
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 .card .text-muted {
color: #adb5bd !important;
}
/* ============================================================
Navigation tabs
============================================================ */
.nav-tabs .nav-link {
font-weight: 600;
}
body.bg-dark .nav-tabs {
border-bottom-color: #495057;
}
body.bg-dark .nav-tabs .nav-link {
color: #adb5bd;
}
body.bg-dark .nav-tabs .nav-link:hover {
border-color: #495057 #495057 transparent;
color: #fff;
}
body.bg-dark .nav-tabs .nav-link.active {
background-color: #2b3035;
color: #fff;
border-color: #495057 #495057 #2b3035;
}
/* ============================================================
Tables
============================================================ */
.report-table {
font-size: .90rem;
}
.report-table td,
.report-table th {
vertical-align: middle;
}
.server-name {
font-weight: 600;
white-space: nowrap;
}
.admin-name {
font-family: Consolas, "Courier New", monospace;
font-size: .88rem;
}
/*
* Bootstrap Table applique la hauteur sur son container.
* Le scroll vertical se fera dans cette zone.
*/
.fixed-table-body {
overflow-y: auto;
}
/* ============================================================
Badges / boutons administrateurs
============================================================ */
.admin-badge {
display: inline-block;
margin: 2px 3px 2px 0;
padding: .30rem .50rem;
border: 0;
border-radius: .35rem;
background-color: #e9ecef;
color: #212529;
font-family: Consolas, "Courier New", monospace;
font-size: .78rem;
cursor: pointer;
}
.admin-badge:hover {
background-color: #ced4da;
}
body.bg-dark .admin-badge {
background-color: #495057;
color: #f8f9fa;
}
body.bg-dark .admin-badge:hover {
background-color: #6c757d;
}
/* ============================================================
Modal
============================================================ */
body.bg-dark .modal-content {
background-color: #2b3035;
color: #f8f9fa;
border-color: #495057;
}
body.bg-dark .modal-header,
body.bg-dark .modal-footer {
border-color: #495057;
}
body.bg-dark .btn-close {
filter: invert(1);
}
.modal-admin-name {
font-family: Consolas, "Courier New", monospace;
}
.server-modal-item {
font-family: Consolas, "Courier New", monospace;
font-size: .92rem;
}
/* ============================================================
Bootstrap Table / dark mode
============================================================ */
body.bg-dark .fixed-table-toolbar .search input {
background-color: #212529;
color: #f8f9fa;
border-color: #495057;
}
body.bg-dark .fixed-table-toolbar .search input::placeholder {
color: #adb5bd;
}
body.bg-dark .pagination .page-link {
background-color: #212529;
border-color: #495057;
color: #f8f9fa;
}
body.bg-dark .pagination .active .page-link {
background-color: #0d6efd;
border-color: #0d6efd;
}
body.bg-dark .dropdown-menu {
background-color: #2b3035;
border-color: #495057;
}
body.bg-dark .dropdown-item {
color: #f8f9fa;
}
body.bg-dark .dropdown-item:hover {
background-color: #495057;
}
/* ============================================================
Responsive
============================================================ */
@media (max-width: 767.98px) {
.summary-card {
min-height: auto;
}
.report-table {
font-size: .80rem;
}
.admin-badge {
font-size: .72rem;
}
}
</style>
</head>
<body class="bg-light text-dark">
<?php include $_SERVER['DOCUMENT_ROOT'] . "/include/all.php"; ?>
<?php
/* ============================================================================
Helpers
========================================================================== */
function local_admin_h($value)
{
return htmlspecialchars(
(string)$value,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
}
/* ============================================================================
Lecture MySQL
========================================================================== */
$sql = "
SELECT
`Server`,
`Admins`
FROM infra.localadmins
ORDER BY `Server`
";
$rows = Invoke_Entry01($sql);
if (!is_array($rows)) {
$rows = [];
}
/* ============================================================================
Préparation des données
========================================================================== */
$servers = [];
$admins = [];
$serversWithoutAdmins = 0;
foreach ($rows as $row) {
$server = trim((string)($row['Server'] ?? ''));
$rawAdmins = trim((string)($row['Admins'] ?? ''));
if ($server === '') {
continue;
}
/* ------------------------------------------------------------------------
Découpage des admins sur |
------------------------------------------------------------------------ */
$serverAdmins = [];
if ($rawAdmins !== '') {
foreach (explode('|', $rawAdmins) as $admin) {
$admin = trim($admin);
if ($admin === '') {
continue;
}
/*
* Évite les doublons dans une même ligne serveur.
*/
$key = strtolower($admin);
$serverAdmins[$key] = $admin;
}
}
/* ------------------------------------------------------------------------
Tri des admins du serveur
------------------------------------------------------------------------ */
natcasesort($serverAdmins);
if (count($serverAdmins) === 0) {
$serversWithoutAdmins++;
}
$servers[] = [
'server' => $server,
'admins' => array_values($serverAdmins)
];
/* ------------------------------------------------------------------------
Construction de la vue inverse :
admin -> serveurs
------------------------------------------------------------------------ */
foreach ($serverAdmins as $admin) {
$key = strtolower($admin);
if (!isset($admins[$key])) {
$admins[$key] = [
'name' => $admin,
'servers' => []
];
}
/*
* Tableau associatif pour éviter les doublons de serveurs.
*/
$admins[$key]['servers'][strtolower($server)] = $server;
}
}
/* ============================================================================
Tri des serveurs
========================================================================== */
usort($servers, function ($a, $b) {
return strnatcasecmp(
$a['server'],
$b['server']
);
});
/* ============================================================================
Tri des admins et de leurs serveurs
========================================================================== */
foreach ($admins as &$admin) {
natcasesort($admin['servers']);
$admin['servers'] = array_values($admin['servers']);
}
unset($admin);
uasort($admins, function ($a, $b) {
return strnatcasecmp(
$a['name'],
$b['name']
);
});
/* ============================================================================
Compteurs
========================================================================== */
$totalServers = count($servers);
$totalAdmins = count($admins);
/*
* Nombre total d'affectations :
*
* 1 admin sur 10 serveurs = 10 affectations.
*/
$totalAssignments = 0;
foreach ($admins as $admin) {
$totalAssignments += count($admin['servers']);
}
/* ============================================================================
JSON utilisé par la modal
========================================================================== */
$adminsForJavascript = [];
foreach ($admins as $key => $admin) {
$adminsForJavascript[$key] = [
'name' => $admin['name'],
'servers' => $admin['servers']
];
}
?>
<div class="container-fluid" id="content">
<div class="row flex-nowrap">
<!-- =====================================================================
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>
<!-- =====================================================================
Contenu
===================================================================== -->
<div class="col py-3">
<!-- ===================================================================
Titre
=================================================================== -->
<h2>
<span class="badge text-bg-secondary w-100 text-wrap">
<i class="bi bi-shield-lock me-1"></i>
Local Administrators —
<span class="text-light">
<?php echo $totalServers; ?> serveurs
</span>
—
<span class="text-info">
<?php echo $totalAdmins; ?> administrateurs
</span>
</span>
</h2>
<div class="container-fluid px-0">
<!-- =================================================================
Résumé
================================================================= -->
<div class="row g-2 mt-1 mb-4">
<!-- Serveurs -->
<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 $totalServers; ?>
</div>
<div class="text-muted small">
Serveurs
</div>
</div>
</div>
</div>
<!-- Administrateurs -->
<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-info">
<?php echo $totalAdmins; ?>
</div>
<div class="text-muted small">
Administrateurs distincts
</div>
</div>
</div>
</div>
<!-- Affectations -->
<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-primary">
<?php echo $totalAssignments; ?>
</div>
<div class="text-muted small">
Affectations admin / serveur
</div>
</div>
</div>
</div>
<!-- Sans admin -->
<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 $serversWithoutAdmins > 0 ? 'text-warning' : 'text-success'; ?>"
>
<?php echo $serversWithoutAdmins; ?>
</div>
<div class="text-muted small">
Serveurs sans admin renseigné
</div>
</div>
</div>
</div>
</div>
<!-- =================================================================
Onglets
================================================================= -->
<ul
class="nav nav-tabs"
id="localAdminsTabs"
role="tablist"
>
<!-- Par serveur -->
<li class="nav-item" role="presentation">
<button
class="nav-link active"
id="servers-tab"
data-bs-toggle="tab"
data-bs-target="#servers-panel"
type="button"
role="tab"
>
<i class="bi bi-hdd-rack me-1"></i>
Par serveur
<span class="badge text-bg-secondary ms-1">
<?php echo $totalServers; ?>
</span>
</button>
</li>
<!-- Par administrateur -->
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="admins-tab"
data-bs-toggle="tab"
data-bs-target="#admins-panel"
type="button"
role="tab"
>
<i class="bi bi-people me-1"></i>
Par administrateur
<span class="badge text-bg-secondary ms-1">
<?php echo $totalAdmins; ?>
</span>
</button>
</li>
</ul>
<!-- =================================================================
Contenu onglets
================================================================= -->
<div class="tab-content pt-3">
<!-- ===============================================================
PAR SERVEUR
=============================================================== -->
<div
class="tab-pane fade show active"
id="servers-panel"
role="tabpanel"
>
<table
id="tableServers"
class="table table-bordered table-hover table-sm report-table"
data-search="true"
data-search-highlight="true"
data-sortable="true"
data-show-columns="true"
data-show-export="true"
data-export-types='["csv","excel"]'
>
<thead>
<tr>
<th
data-field="server"
data-sortable="true"
>
Serveur
</th>
<th
data-field="admins"
data-sortable="false"
>
Administrateurs locaux
</th>
<th
data-field="count"
data-sortable="true"
data-align="center"
>
Nombre
</th>
</tr>
</thead>
<tbody>
<?php foreach ($servers as $row) : ?>
<tr>
<!-- Serveur -->
<td class="server-name">
<?php echo local_admin_h($row['server']); ?>
</td>
<!-- Admins -->
<td>
<?php if (count($row['admins']) === 0) : ?>
<span class="badge bg-warning text-dark">
Aucun administrateur renseigné
</span>
<?php else : ?>
<?php foreach ($row['admins'] as $admin) : ?>
<button
type="button"
class="admin-badge js-show-admin"
data-admin-key="<?php echo local_admin_h(strtolower($admin)); ?>"
title="Afficher les serveurs où cet administrateur est présent"
>
<?php echo local_admin_h($admin); ?>
</button>
<?php endforeach; ?>
<?php endif; ?>
</td>
<!-- Nombre -->
<td class="text-center">
<?php echo count($row['admins']); ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- ===============================================================
PAR ADMINISTRATEUR
=============================================================== -->
<div
class="tab-pane fade"
id="admins-panel"
role="tabpanel"
>
<table
id="tableAdmins"
class="table table-bordered table-hover table-sm report-table"
data-search="true"
data-search-highlight="true"
data-sortable="true"
data-show-columns="true"
data-show-export="true"
data-export-types='["csv","excel"]'
>
<thead>
<tr>
<th
data-field="admin"
data-sortable="true"
>
Administrateur
</th>
<th
data-field="count"
data-sortable="true"
data-align="center"
>
Serveurs
</th>
<th
data-field="action"
data-sortable="false"
data-align="center"
data-switchable="false"
>
Détail
</th>
</tr>
</thead>
<tbody>
<?php foreach ($admins as $key => $admin) : ?>
<tr>
<!-- Admin -->
<td class="admin-name">
<?php echo local_admin_h($admin['name']); ?>
</td>
<!-- Nombre de serveurs -->
<td class="text-center">
<?php echo count($admin['servers']); ?>
</td>
<!-- Bouton détail -->
<td class="text-center">
<button
type="button"
class="btn btn-sm btn-outline-primary js-show-admin"
data-admin-key="<?php echo local_admin_h($key); ?>"
>
<i class="bi bi-list-ul me-1"></i>
Voir les serveurs
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- =========================================================================
Modal détail administrateur
========================================================================= -->
<div
class="modal fade"
id="adminServersModal"
tabindex="-1"
aria-labelledby="adminServersModalLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<!-- Header -->
<div class="modal-header">
<h5
class="modal-title"
id="adminServersModalLabel"
>
<i class="bi bi-person-lock me-1"></i>
<span id="modalAdminName" class="modal-admin-name"></span>
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Fermer"
></button>
</div>
<!-- Body -->
<div class="modal-body">
<div class="mb-3">
<span class="badge text-bg-primary">
<span id="modalServerCount">0</span>
serveur(s)
</span>
</div>
<div
class="list-group"
id="modalServerList"
>
</div>
</div>
<!-- Footer -->
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Fermer
</button>
</div>
</div>
</div>
</div>
<script src="/js/switch.js"></script>
<script>
/* ============================================================================
Données PHP -> Javascript
========================================================================== */
const localAdmins = <?php
echo json_encode(
$adminsForJavascript,
JSON_UNESCAPED_UNICODE |
JSON_UNESCAPED_SLASHES |
JSON_HEX_TAG |
JSON_HEX_APOS |
JSON_HEX_AMP |
JSON_HEX_QUOT
);
?>;
/* ============================================================================
Initialisation
========================================================================== */
$(function () {
/* ------------------------------------------------------------------------
Dark mode
------------------------------------------------------------------------ */
function isDark() {
return document.body.classList.contains('bg-dark');
}
function syncTheme($table) {
$table.toggleClass(
'table-dark',
isDark()
);
}
/* ------------------------------------------------------------------------
Debounce
------------------------------------------------------------------------ */
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(
function () {
func.apply(
context,
args
);
},
wait
);
};
}
/* ------------------------------------------------------------------------
Calcul dynamique de la hauteur du tableau
------------------------------------------------------------------------ */
function updateTableHeight($table) {
if (!$table || $table.length === 0) {
return;
}
/*
* Impossible de calculer correctement la géométrie
* d'un tableau placé dans un onglet masqué.
*/
if (!$table.is(':visible')) {
return;
}
const $bootstrapTable =
$table.closest('.bootstrap-table');
if ($bootstrapTable.length === 0) {
return;
}
/*
* Le fixed-table-container commence juste sous la toolbar
* Bootstrap Table : recherche, export, choix colonnes...
*
* C'est donc le meilleur point de départ pour calculer
* l'espace restant jusqu'en bas de l'écran.
*/
const $container =
$bootstrapTable.find('.fixed-table-container');
if ($container.length === 0) {
return;
}
const rect =
$container[0].getBoundingClientRect();
/*
* Petite marge en bas.
*/
const bottomMargin = 15;
let availableHeight =
window.innerHeight
- rect.top
- bottomMargin;
/*
* Evite une hauteur négative ou minuscule.
*/
availableHeight = Math.max(
availableHeight,
250
);
$table.bootstrapTable(
'resetView',
{
height: availableHeight
}
);
}
/* ------------------------------------------------------------------------
Bootstrap Table
------------------------------------------------------------------------ */
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);
/*
* Laisse Bootstrap Table construire son DOM
* avant de mesurer la hauteur disponible.
*/
setTimeout(
function () {
updateTableHeight($table);
},
50
);
}
/* ------------------------------------------------------------------------
Première table visible au chargement
------------------------------------------------------------------------ */
initTable(
$('#tableServers')
);
/* ------------------------------------------------------------------------
Lorsqu'on change d'onglet
------------------------------------------------------------------------ */
$('button[data-bs-toggle="tab"]').on(
'shown.bs.tab',
function (event) {
/*
* Lecture directe de data-bs-target.
* Plus fiable que jQuery .data() avec les attributs Bootstrap 5.
*/
const target =
event.target.getAttribute('data-bs-target');
if (!target) {
return;
}
const $table =
$(target).find('table.report-table');
/*
* On laisse Bootstrap terminer complètement
* l'affichage de l'onglet avant de toucher à Bootstrap Table.
*/
setTimeout(
function () {
function initTable($table) {
if ($table.length === 0) {
return;
}
/*
* Important :
* ne jamais initialiser Bootstrap Table
* pendant que la table est cachée.
*/
if (!$table.is(':visible')) {
return;
}
if (!$table.data('bt-initialized')) {
/*
* Première ouverture de l'onglet :
* Bootstrap Table construit son DOM.
*/
$table.bootstrapTable();
$table.data(
'bt-initialized',
true
);
} else {
/*
* Table déjà initialisée :
* on force Bootstrap Table à recalculer son affichage.
*/
$table.bootstrapTable(
'resetView'
);
}
syncTheme($table);
/*
* Une fois le DOM Bootstrap Table construit,
* calcul de la hauteur disponible.
*/
setTimeout(
function () {
$table.bootstrapTable(
'resetView'
);
updateTableHeight(
$table
);
},
50
);
}
},
10
);
}
);
/* ------------------------------------------------------------------------
Changement clair / sombre
------------------------------------------------------------------------ */
$('#lightSwitch').on(
'change',
function () {
setTimeout(
function () {
$('table.report-table').each(
function () {
syncTheme(
$(this)
);
}
);
},
0
);
}
);
/* ------------------------------------------------------------------------
Modal administrateur
------------------------------------------------------------------------ */
const adminModal = new bootstrap.Modal(
document.getElementById('adminServersModal')
);
$(document).on(
'click',
'.js-show-admin',
function () {
const key = String(
$(this).data('admin-key') || ''
).toLowerCase();
if (!localAdmins[key]) {
return;
}
const admin =
localAdmins[key];
/* Nom */
$('#modalAdminName').text(
admin.name
);
/* Compteur */
$('#modalServerCount').text(
admin.servers.length
);
/* Liste */
const $list =
$('#modalServerList');
$list.empty();
admin.servers.forEach(
function (server) {
$('<div>')
.addClass(
'list-group-item server-modal-item'
)
.text(server)
.appendTo($list);
}
);
/*
* Thème sombre des éléments créés dynamiquement.
*/
if (isDark()) {
$list
.find('.list-group-item')
.addClass(
'bg-dark text-light border-secondary'
);
}
adminModal.show();
}
);
/* ------------------------------------------------------------------------
Le modal peut être ouvert puis le thème changé
------------------------------------------------------------------------ */
$('#lightSwitch').on(
'change',
function () {
setTimeout(
function () {
const dark =
isDark();
$('#modalServerList .list-group-item')
.toggleClass(
'bg-dark text-light border-secondary',
dark
);
},
0
);
}
);
/* ------------------------------------------------------------------------
Resize navigateur
------------------------------------------------------------------------ */
$(window).on(
'resize',
debounce(
function () {
$('table.report-table:visible').each(
function () {
const $table =
$(this);
if ($table.data('bt-initialized')) {
updateTableHeight(
$table
);
}
}
);
},
150
)
);
/* ------------------------------------------------------------------------
Sécurité :
deuxième calcul après rendu complet de la page
------------------------------------------------------------------------ */
setTimeout(
function () {
$('table.report-table:visible').each(
function () {
const $table =
$(this);
if ($table.data('bt-initialized')) {
updateTableHeight(
$table
);
}
}
);
},
250
);
});
</script>
</body>
</html>