Add Veeam and NetBackup support to inventory and reporting

- Introduced Veeam and NetBackup version tracking in `Inventory.php` for backup validation.
- Enhanced backup report in `Backups.php` with improved SQL queries and support for short hostname normalization.
- Updated `navbar.html` and related navigation components for better usability and accessibility.
- Refactored ActiveDirectory scripts for better modularity and event handling.
- Added `AGENTS.md` and `modele.php` as templates for documentation and new page creation.
This commit is contained in:
e025532
2026-09-24 15:36:09 +02:00
parent c185d099d6
commit db223b805c
7 changed files with 847 additions and 127 deletions

View File

@@ -1223,12 +1223,6 @@
true true
); );
} else {
$table.bootstrapTable(
'resetView'
);
} }
@@ -1236,8 +1230,9 @@
/* /*
* Laisse Bootstrap Table construire son DOM * Laisse Bootstrap Table construire (ou reconstruire)
* avant de mesurer la hauteur disponible. * son DOM avant de mesurer la hauteur disponible.
* updateTableHeight fait lui-même le resetView avec la hauteur.
*/ */
setTimeout( setTimeout(
function () { function () {
@@ -1280,87 +1275,54 @@
} }
const $table =
$(target).find('table.report-table');
/* /*
* On laisse Bootstrap terminer complètement * IMPORTANT : on cible la table par son id.
* l'affichage de l'onglet avant de toucher à Bootstrap Table. * En mode hauteur fixe, Bootstrap Table clone le thead dans une
* seconde <table> à laquelle il recopie la classe d'origine
* (report-table). Sélectionner par classe renverrait donc AUSSI ce
* clone (placé avant l'originale dans le DOM), et .data('bt-initialized')
* lirait le clone -> fausse "première init" -> double init -> tableau vidé.
* Le clone ne reçoit jamais d'id : [id] ne matche que la vraie table.
*/ */
setTimeout( const $table =
function () { $(target).find('table.report-table[id]');
function initTable($table) {
if ($table.length === 0) { if ($table.length === 0) {
return; return;
} }
/*
* Important :
* ne jamais initialiser Bootstrap Table
* pendant que la table est cachée.
*/
if (!$table.is(':visible')) {
return;
}
if (!$table.data('bt-initialized')) { if (!$table.data('bt-initialized')) {
/* /*
* Première ouverture de l'onglet : * Première ouverture de l'onglet :
* Bootstrap Table construit son DOM. * on initialise et on dimensionne pendant que la table est visible.
*/ */
$table.bootstrapTable(); initTable($table);
$table.data(
'bt-initialized',
true
);
} else { } else {
/* /*
* Table déjà initialisée : * Onglet déjà construit : display:none / show conserve tout le rendu
* on force Bootstrap Table à recalculer son affichage. * de Bootstrap Table. On ne refait PAS de resetView, qui viderait
* le corps du tableau.
*/ */
$table.bootstrapTable(
'resetView'
);
}
syncTheme($table); syncTheme($table);
/* /*
* Une fois le DOM Bootstrap Table construit, * Sauf si un resize a eu lieu pendant que l'onglet était masqué :
* calcul de la hauteur disponible. * dans ce cas seulement, on recalcule la hauteur maintenant.
*/ */
setTimeout( if ($table.data('needs-resize')) {
function () {
$table.bootstrapTable( $table.data('needs-resize', false);
'resetView'
);
updateTableHeight( updateTableHeight($table);
$table
);
},
50
);
} }
}, }
10
);
} }
); );
@@ -1377,7 +1339,7 @@
setTimeout( setTimeout(
function () { function () {
$('table.report-table').each( $('table.report-table[id]').each(
function () { function () {
syncTheme( syncTheme(
@@ -1518,19 +1480,32 @@
debounce( debounce(
function () { function () {
$('table.report-table:visible').each( $('table.report-table[id]').each(
function () { function () {
const $table = const $table =
$(this); $(this);
if ($table.data('bt-initialized')) { if (!$table.data('bt-initialized')) {
return;
}
if ($table.is(':visible')) {
updateTableHeight( updateTableHeight(
$table $table
); );
} else {
/*
* On ne peut pas mesurer un onglet masqué :
* on marque la table pour recalcul à sa prochaine ouverture.
*/
$table.data('needs-resize', true);
} }
} }
@@ -1550,7 +1525,7 @@
setTimeout( setTimeout(
function () { function () {
$('table.report-table:visible').each( $('table.report-table[id]:visible').each(
function () { function () {
const $table = const $table =

222
AGENTS.md Normal file
View File

@@ -0,0 +1,222 @@
# AGENTS.md — Instructions Codex
## Contexte
Ce projet est un environnement de développement hébergé sur une infrastructure
professionnelle.
Le code présent dans ce workspace peut être modifié par Codex.
IMPORTANT :
Les bases de données auxquelles l'application peut accéder contiennent des
DONNÉES DE PRODUCTION.
La protection et l'intégrité de ces données sont prioritaires.
---
## 1. Périmètre autorisé
Codex peut :
- Lire tous les fichiers du workspace courant.
- Rechercher librement dans les fichiers du workspace.
- Créer, modifier, renommer ou supprimer des fichiers dans le workspace
lorsque cela est nécessaire à la tâche demandée.
- Exécuter des commandes locales nécessaires à l'analyse du projet.
- Utiliser Git pour examiner :
- status
- diff
- log
- historique
- Analyser et modifier du code PHP, JavaScript, HTML, CSS, SQL et PowerShell
présent dans le workspace.
Codex doit rester dans le workspace courant :
E:\Sites\web-infra-reports
Ne jamais modifier un fichier situé en dehors de ce workspace sans demander
explicitement l'autorisation.
---
## 2. Bases de données — RÈGLE CRITIQUE
Les bases de données accessibles depuis cette application doivent être
considérées comme des bases de PRODUCTION.
### Opérations autorisées
Codex peut effectuer des opérations SQL strictement en lecture seule :
SELECT
Il peut également :
- analyser les requêtes SQL présentes dans le code ;
- écrire ou modifier du SQL dans les fichiers source ;
- proposer des requêtes SQL ;
- examiner la structure des données lorsqu'elle est accessible en lecture.
### Opérations interdites
Codex ne doit JAMAIS exécuter sur une base de données une commande susceptible
de modifier les données, la structure ou la configuration.
Cela comprend notamment :
INSERT
UPDATE
DELETE
MERGE
TRUNCATE
DROP
ALTER
CREATE
GRANT
REVOKE
Ne jamais exécuter une procédure stockée ou une fonction SQL susceptible
d'avoir des effets de bord.
En particulier :
EXEC
EXECUTE
ne doivent être utilisés que s'il est certain que l'opération est strictement
en lecture seule.
En cas de doute :
NE PAS EXÉCUTER LA REQUÊTE.
Demander confirmation à l'utilisateur.
---
## 3. Credentials et secrets
Ne jamais :
- afficher inutilement un mot de passe ;
- recopier un mot de passe dans un nouveau fichier ;
- ajouter un secret dans Git ;
- modifier les credentials existants sans demande explicite ;
- transmettre un credential ou un secret vers Internet.
Si des credentials sont découverts dans le code, les considérer comme
confidentiels.
Ne pas les inclure dans les réponses sauf nécessité absolue.
---
## 4. Infrastructure externe
Ne jamais modifier directement :
- un autre site IIS ;
- une configuration IIS externe au workspace ;
- Active Directory ;
- DNS ;
- un serveur distant ;
- une machine virtuelle ;
- un partage réseau ;
- une tâche planifiée ;
- une configuration système Windows ;
- une base de données ;
- une ressource de production.
Codex peut analyser le code qui effectue ces opérations et le modifier dans
le workspace.
L'exécution réelle d'une opération d'administration doit être laissée à
l'utilisateur sauf demande explicite.
---
## 5. Commandes PowerShell et shell
Les commandes purement consultatives peuvent être exécutées librement.
Exemples :
rg
Get-Content
Select-String
Get-ChildItem
git status
git diff
git log
Avant d'exécuter une commande ayant potentiellement des effets de bord,
vérifier qu'elle ne peut pas modifier une ressource de production.
En cas de doute, demander confirmation.
---
## 6. Modifications du code
Avant une modification importante :
1. comprendre le code existant ;
2. rechercher les dépendances et les appels concernés ;
3. conserver le comportement existant qui n'est pas concerné par la demande ;
4. privilégier une modification minimale plutôt qu'une réécriture inutile.
Après modification :
1. vérifier les fichiers modifiés ;
2. rechercher les éventuelles références impactées ;
3. examiner le diff ;
4. signaler clairement les modifications réalisées ;
5. signaler les points qui n'ont pas pu être testés.
Ne jamais considérer qu'une modification est déployée en production.
---
## 7. Tests
Les tests locaux sans effet de bord sont autorisés.
Ne jamais utiliser une ressource de production pour tester une opération
d'écriture.
Si une fonctionnalité nécessite normalement une écriture en base de données,
modifier le code si demandé mais NE PAS effectuer l'écriture réelle.
Indiquer à l'utilisateur que cette partie doit être testée manuellement dans
un environnement approprié.
---
## 8. Accès Internet
Ne pas envoyer vers un service externe :
- code source propriétaire ;
- credentials ;
- noms d'utilisateurs ;
- données provenant des bases ;
- informations internes à l'entreprise.
Si une recherche Internet est nécessaire, demander l'autorisation lorsque
Codex le requiert et limiter la requête aux informations strictement
nécessaires.
---
## Principe général
Codex dispose d'une autonomie importante sur le CODE contenu dans ce
workspace.
Codex ne dispose d'aucune autonomie pour modifier les DONNÉES ou
l'INFRASTRUCTURE de production.
Si une action peut avoir un effet de bord sur une ressource extérieure au
workspace, demander confirmation avant de l'exécuter.

View File

@@ -68,10 +68,12 @@
$UTDBES = end($versions); $UTDBES = end($versions);
$UTDFI = (Invoke_Infra("select max(Ver) as utd from X_cmdb_Product where Name = 'fi'"))[0]['utd']; $UTDFI = (Invoke_Infra("select max(Ver) as utd from X_cmdb_Product where Name = 'fi'"))[0]['utd'];
$UTDxs = (Invoke_Infra("select max(Ver) as utd from X_cmdb_Product where Name = 'xs'"))[0]['utd']; $UTDxs = (Invoke_Infra("select max(Ver) as utd from X_cmdb_Product where Name = 'xs'"))[0]['utd'];
$UTDveeam = (Invoke_Infra("select max(Ver) as utd from X_cmdb_Product where Name = 'veeam'"))[0]['utd'];
$UTDnetbackup = (Invoke_Infra("select max(Ver) as utd from X_cmdb_Product where Name = 'netbackup'"))[0]['utd'];
?> ?>
<div class="container-fluid" id="content"> <div class="container-fluid" id="content" style="zoom:80%">
<div class="row flex-nowrap"> <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;"> <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"; ?> <?php include $_SERVER['DOCUMENT_ROOT'] . "/navbar.html"; ?>
@@ -111,6 +113,8 @@
<th data-field="BES" data-sortable="true" data-visible="false">BES</th> <th data-field="BES" data-sortable="true" data-visible="false">BES</th>
<th data-field="FI" data-sortable="true" data-visible="false">Fusion Inv.</th> <th data-field="FI" data-sortable="true" data-visible="false">Fusion Inv.</th>
<th data-field="xs" data-sortable="true" data-visible="false">XenSam</th> <th data-field="xs" data-sortable="true" data-visible="false">XenSam</th>
<th data-field="veeam" data-sortable="true" data-visible="true">Veeam</th>
<th data-field="netbackup" data-sortable="true" data-visible="true">Netbackup</th>
</tr> </tr>
</thead> </thead>
@@ -342,7 +346,46 @@
} else { } else {
echo "<td></td>"; echo "<td></td>";
} }
//Veeam
$s1s = $s['VeeamState'];
$s1v = $s['VeeamVersion'];
if ($s1s == "Y") {
if ($s1v == $UTDveeam) {
echo "<td class='table-success'>$s1v $s1s</td>";
} else {
echo "<td class='table-warning'><b>$s1v</b> $s1s</td>";
}
}elseif($s1s == "E"){
echo "<td class='table-secondary text-center'>Excluded</td>";
}elseif($s1s == "N"){
echo "<td class='table-primary text-center'>Nope :)</td>";
} else {
if ($s1v == "") {
echo "<td class='table-danger text-center'>MISSING</td>";
} else {
echo "<td class='table-danger'>$s1v <b>$s1s</b></td>";
}
}
//Netbackup
$s1s = $s['nbState'];
$s1v = $s['nbVersion'];
if ($s1s == "Y") {
if ($s1v == $UTDnetbackup) {
echo "<td class='table-success'>$s1v $s1s</td>";
} else {
echo "<td class='table-warning'><b>$s1v</b> $s1s</td>";
}
}elseif($s1s == "E"){
echo "<td class='table-secondary text-center'>Excluded</td>";
}elseif($s1s == "N"){
} else {
if ($s1v == "") {
echo "<td class='table-danger text-center'>MISSING</td>";
} else {
echo "<td class='table-danger'>$s1v <b>$s1s</b></td>";
}
}
echo "</tr>"; echo "</tr>";
} }

67
modele.php Normal file
View File

@@ -0,0 +1,67 @@
<?php
/**
* Squelette de page : dupliquer ce fichier et adapter le titre et le contenu.
* Les chemins depuis la racine restent valides dans les sous-dossiers du site.
*/
// Initialiser la session, la langue et les fonctions communes avant tout affichage.
// all.php produit aussi du HTML : le conserver pour l'insérer dans le body.
ob_start();
require $_SERVER['DOCUMENT_ROOT'] . '/include/all.php';
$commonMarkup = ob_get_clean();
// À personnaliser pour chaque nouvelle page.
$pageTitle = ($lang === 'fr') ? 'Titre de la page' : 'Page title';
$escapedPageTitle = htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8');
// Ajouter ici les traitements PHP propres à la page.
?>
<!DOCTYPE html>
<html lang="<?= htmlspecialchars($lang, ENT_QUOTES, 'UTF-8') ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= $escapedPageTitle ?> - Web Infra Reports IT</title>
<link rel="shortcut icon" type="image/png" href="/include/favicon-32x32.png">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/bootstrap-icons-1.13.1/bootstrap-icons.css">
<!-- jQuery est nécessaire à la recherche de serveurs du menu commun. -->
<script src="/js/jquery-3.6.1.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<!-- Ajouter ici les styles et dépendances propres à la page. -->
<style>
.page-sidebar {
flex: 0 0 230px;
overflow-y: auto;
}
.page-content {
min-width: 0;
}
</style>
</head>
<body class="bg-light text-dark">
<?= $commonMarkup ?>
<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 page-sidebar">
<?php require $_SERVER['DOCUMENT_ROOT'] . '/navbar.html'; ?>
</div>
<main class="col py-3 page-content" aria-labelledby="page-title">
<h1 id="page-title"><span class="badge text-bg-secondary w-100"><?= $escapedPageTitle ?></span></h1>
<div class="mt-4">
<!-- Ajouter ici le contenu de la page. -->
</div>
</main>
</div>
</div>
<!-- Charger après le menu pour initialiser le bouton clair/sombre. -->
<script src="/js/switch.js"></script>
<!-- Ajouter ici les scripts propres à la page. -->
</body>
</html>

278
modele_full.php Normal file
View File

@@ -0,0 +1,278 @@
<?php
/**
* Catalogue de composants à copier dans les prochaines pages.
* Sources : modele.php, index.php, AD/LocalAdmins.php,
* Network/dhcp_servers.php, Hyper-V/Clusters.php et reports/Backups.php.
* Toutes les données ci-dessous sont fictives. Aucun appel à une base.
*/
ob_start();
require $_SERVER['DOCUMENT_ROOT'] . '/include/all.php';
$commonMarkup = ob_get_clean();
$pageTitle = 'Modèles de composants';
$escape = static function ($value) {
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
};
// Remplacer ce jeu de démonstration par les données de votre future page.
$demoRows = [
['DEMO-SRV-01', 'Windows', 'Disponible', 'success', 24],
['DEMO-SRV-02', 'Linux', 'Disponible', 'success', 42],
['DEMO-SRV-03', 'Windows', 'À vérifier', 'warning', 76],
['DEMO-SRV-04', 'Linux', 'Indisponible', 'danger', 0],
['DEMO-SRV-05', 'Windows', 'Disponible', 'success', 31],
['DEMO-SRV-06', 'Linux', 'Disponible', 'success', 58],
];
?>
<!DOCTYPE html>
<html lang="<?= $escape($lang) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= $escape($pageTitle) ?> - Web Infra Reports IT</title>
<link rel="shortcut icon" type="image/png" href="/include/favicon-32x32.png">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/bootstrap-icons-1.13.1/bootstrap-icons.css">
<link rel="stylesheet" href="/css/bootstrap-table.min.css">
<script src="/js/jquery-3.6.1.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<!-- Dépendances du tableau avec recherche, tri, pagination et export. -->
<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/chart.min.js"></script>
<style>
.page-sidebar { flex: 0 0 230px; overflow-y: auto; }
.page-content { min-width: 0; }
.demo-section { scroll-margin-top: 1rem; }
.demo-kpi { border-radius: 14px; box-shadow: 0 4px 14px rgba(15, 23, 42, .06); }
.demo-icon { width: 48px; height: 48px; display: grid; place-items: center; border-radius: 14px; font-size: 1.5rem; }
.demo-chart { position: relative; height: 240px; }
.page-content .card, .demo-modal .modal-content { color: inherit; }
body.bg-dark .page-content .card,
body.bg-dark .demo-modal .modal-content,
body.bg-dark .page-content .list-group-item { background-color: #2b3035; color: #f8f9fa; border-color: #495057; }
body.bg-dark .page-content .text-muted { color: #adb5bd !important; }
body.bg-dark .page-content .form-control,
body.bg-dark .page-content .form-select,
body.bg-dark .page-content .page-link,
body.bg-dark .page-content .dropdown-menu { background-color: #212529; color: #f8f9fa; border-color: #495057; }
body.bg-dark .page-content .dropdown-item { color: #f8f9fa; }
body.bg-dark .page-content .dropdown-item:hover { background-color: #495057; }
body.bg-dark .page-content .nav-tabs .nav-link.active { background-color: #2b3035; color: #f8f9fa; border-color: #495057; }
.page-content .accordion-item, .page-content .accordion-button { background-color: transparent; color: inherit; }
.page-content .accordion-item { border-color: rgba(128, 128, 128, .35); }
.page-content .accordion-button:not(.collapsed) { background-color: rgba(128, 128, 128, .12); }
body.bg-dark .page-content .accordion-button::after,
body.bg-dark .demo-modal .btn-close { filter: invert(1) grayscale(1); }
</style>
</head>
<body class="bg-light text-dark">
<?= $commonMarkup ?>
<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 page-sidebar">
<?php require $_SERVER['DOCUMENT_ROOT'] . '/navbar.html'; ?>
</div>
<main class="col py-3 page-content" aria-labelledby="page-title">
<h1 id="page-title"><span class="badge text-bg-secondary w-100 text-wrap"><?= $escape($pageTitle) ?></span></h1>
<p class="text-muted">Exemples réutilisables — données fictives et actions de démonstration.</p>
<nav class="d-flex flex-wrap gap-2 mb-4" aria-label="Composants de la page">
<a class="btn btn-sm btn-outline-primary" href="#synthese">Synthèse</a>
<a class="btn btn-sm btn-outline-primary" href="#tableau">Tableau</a>
<a class="btn btn-sm btn-outline-primary" href="#boutons">Boutons</a>
<a class="btn btn-sm btn-outline-primary" href="#formulaire">Formulaire</a>
<a class="btn btn-sm btn-outline-primary" href="#indicateurs">Indicateurs</a>
<a class="btn btn-sm btn-outline-primary" href="#organisation">Onglets et accordéon</a>
</nav>
<!-- 1. CARTES DE SYNTHÈSE : reprendre une colonne pour chaque indicateur. -->
<section id="synthese" class="demo-section mb-4" aria-labelledby="synthese-title">
<h2 id="synthese-title" class="h4">Cartes de synthèse</h2>
<div class="row g-3">
<?php foreach ([['Total', 6, 'primary', 'hdd-rack'], ['Disponibles', 4, 'success', 'check-circle'], ['À vérifier', 1, 'warning', 'exclamation-triangle'], ['Indisponibles', 1, 'danger', 'x-circle']] as $kpi): ?>
<div class="col-sm-6 col-xl-3">
<div class="card demo-kpi h-100"><div class="card-body d-flex align-items-center gap-3">
<span class="demo-icon text-bg-<?= $escape($kpi[2]) ?>"><i class="bi bi-<?= $escape($kpi[3]) ?>" aria-hidden="true"></i></span>
<div><div class="text-muted"><?= $escape($kpi[0]) ?></div><div class="fs-2 fw-bold"><?= $escape($kpi[1]) ?></div></div>
</div></div>
</div>
<?php endforeach; ?>
</div>
</section>
<!-- 2. TABLEAU : identifiant unique requis pour chaque bootstrap-table. -->
<section id="tableau" class="card demo-section mb-4" aria-labelledby="tableau-title">
<div class="card-header"><h2 id="tableau-title" class="h5 mb-0"><i class="bi bi-table me-2" aria-hidden="true"></i>Tableau avec recherche, tri et export</h2></div>
<div class="card-body">
<table id="demo-table" class="table table-bordered table-hover table-sm"
data-toggle="table" data-locale="<?= $lang === 'fr' ? 'fr-FR' : 'en-US' ?>"
data-search="true" data-show-columns="true" data-pagination="true"
data-page-size="5" data-page-list="[5, 10, 25]" data-show-export="true"
data-export-types='["csv", "json"]' data-export-data-type="all">
<caption>Inventaire fictif : six serveurs de démonstration.</caption>
<thead><tr>
<th scope="col" data-field="server" data-sortable="true">Serveur</th>
<th scope="col" data-field="os" data-sortable="true">Système</th>
<th scope="col" data-field="status" data-sortable="true">État</th>
<th scope="col" data-field="usage" data-sortable="true">Utilisation (%)</th>
<th scope="col" data-field="actions" data-force-hide="true">Action</th>
</tr></thead>
<tbody>
<?php foreach ($demoRows as $row): ?>
<tr>
<td class="fw-bold"><?= $escape($row[0]) ?></td>
<td><?= $escape($row[1]) ?></td>
<td><span class="badge text-bg-<?= $escape($row[3]) ?>"><?= $escape($row[2]) ?></span></td>
<td><?= $escape($row[4]) ?></td>
<td><button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#demo-modal" data-demo-server="<?= $escape($row[0]) ?>"><i class="bi bi-eye me-1" aria-hidden="true"></i>Détails</button></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
<!-- 3. BOUTONS : les actions de cet exemple affichent seulement un message. -->
<section id="boutons" class="card demo-section mb-4" aria-labelledby="boutons-title">
<div class="card-header"><h2 id="boutons-title" class="h5 mb-0">Boutons et fenêtre modale</h2></div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2 mb-3">
<button type="button" class="btn btn-primary" data-demo-action="Action principale"><i class="bi bi-plus-circle me-1" aria-hidden="true"></i>Principal</button>
<button type="button" class="btn btn-secondary" data-demo-action="Action secondaire">Secondaire</button>
<button type="button" class="btn btn-success" data-demo-action="Validation"><i class="bi bi-check-lg me-1" aria-hidden="true"></i>Valider</button>
<button type="button" class="btn btn-warning" data-demo-action="Vérification">Vérifier</button>
<button type="button" class="btn btn-danger" data-demo-action="Action sensible simulée">Action sensible</button>
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#demo-modal">Ouvrir la modale</button>
</div>
<div class="d-flex flex-wrap align-items-center gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary" data-demo-action="Petit bouton">Petit bouton</button>
<button type="button" class="btn btn-primary" disabled>Désactivé</button>
<button type="button" class="btn btn-primary" disabled><span class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span>Chargement…</button>
<div class="dropdown">
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Autres actions</button>
<ul class="dropdown-menu">
<li><button type="button" class="dropdown-item" data-demo-action="Première option">Première option</button></li>
<li><button type="button" class="dropdown-item" data-demo-action="Seconde option">Seconde option</button></li>
</ul>
</div>
</div>
<p id="demo-action-result" class="mt-3 mb-0" role="status" aria-live="polite"></p>
</div>
</section>
<!-- 4. FORMULAIRE : validation navigateur et aperçu local, sans envoi. -->
<section id="formulaire" class="card demo-section mb-4" aria-labelledby="formulaire-title">
<div class="card-header"><h2 id="formulaire-title" class="h5 mb-0">Formulaire</h2></div>
<div class="card-body">
<form id="demo-form" onsubmit="return false;">
<div class="row g-3">
<div class="col-md-6"><label for="demo-name" class="form-label">Nom <span class="text-danger">*</span></label><input id="demo-name" class="form-control" required maxlength="80" placeholder="Exemple : DEMO-SRV-07"></div>
<div class="col-md-6"><label for="demo-system" class="form-label">Système</label><select id="demo-system" class="form-select"><option>Windows</option><option>Linux</option><option>Autre</option></select></div>
<div class="col-md-6"><label for="demo-date" class="form-label">Date</label><input type="date" id="demo-date" class="form-control"></div>
<div class="col-md-6"><label for="demo-capacity" class="form-label">Capacité</label><div class="input-group"><input type="number" id="demo-capacity" class="form-control" min="0" value="100"><span class="input-group-text">Go</span></div></div>
<div class="col-12"><label for="demo-comment" class="form-label">Commentaire</label><textarea id="demo-comment" class="form-control" rows="2" maxlength="500"></textarea></div>
<div class="col-md-6"><div class="form-check"><input type="checkbox" class="form-check-input" id="demo-notify"><label class="form-check-label" for="demo-notify">Inclure dans le rapport</label></div></div>
<div class="col-md-6"><div class="form-check form-switch"><input type="checkbox" class="form-check-input" id="demo-enabled" checked><label class="form-check-label" for="demo-enabled">Actif</label></div></div>
<div class="col-12 d-flex gap-2"><button type="submit" class="btn btn-primary">Prévisualiser</button><button type="reset" class="btn btn-outline-secondary">Réinitialiser</button></div>
</div>
<p id="demo-form-result" class="mt-3 mb-0" role="status" aria-live="polite"></p>
</form>
</div>
</section>
<!-- 5. ALERTES, BADGES, PROGRESSION ET GRAPHIQUE. -->
<section id="indicateurs" class="demo-section mb-4" aria-labelledby="indicateurs-title">
<h2 id="indicateurs-title" class="h4">Alertes et indicateurs</h2>
<div class="row g-3">
<div class="col-xl-7"><div class="card h-100"><div class="card-body">
<div class="alert alert-info"><i class="bi bi-info-circle me-2" aria-hidden="true"></i>Information : exemple de message contextuel.</div>
<div class="alert alert-success">Succès : vérification terminée.</div>
<div class="alert alert-warning">Attention : un élément nécessite une vérification.</div>
<div class="alert alert-danger">Erreur : exemple de message d’échec.</div>
<div class="d-flex flex-wrap gap-2 mb-3"><span class="badge text-bg-success">Conforme</span><span class="badge text-bg-warning">À vérifier</span><span class="badge text-bg-danger">Erreur</span><span class="badge text-bg-secondary">Inconnu</span></div>
<div id="demo-progress-label" class="mb-1">Capacité utilisée : 65 %</div>
<div class="progress"><div class="progress-bar progress-bar-striped" role="progressbar" style="width:65%" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100" aria-labelledby="demo-progress-label">65 %</div></div>
</div></div></div>
<div class="col-xl-5"><div class="card h-100"><div class="card-header"><h3 class="h5 mb-0">Répartition des serveurs</h3></div><div class="card-body">
<div class="demo-chart"><canvas id="demo-chart" role="img" aria-label="4 serveurs disponibles, 1 à vérifier et 1 indisponible">4 disponibles, 1 à vérifier, 1 indisponible.</canvas></div>
<ul class="list-unstyled mt-3 mb-0"><li><span class="badge text-bg-success me-2">4</span>Disponibles</li><li><span class="badge text-bg-warning me-2">1</span>À vérifier</li><li><span class="badge text-bg-danger me-2">1</span>Indisponibles</li></ul>
</div></div></div>
</div>
</section>
<!-- 6. ONGLETS ET ACCORDÉON : adapter ensemble les id et data-bs-target. -->
<section id="organisation" class="card demo-section mb-4" aria-labelledby="organisation-title">
<div class="card-header"><h2 id="organisation-title" class="h5 mb-0">Onglets et accordéon</h2></div>
<div class="card-body">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation"><button id="demo-summary-tab" class="nav-link active" type="button" role="tab" data-bs-toggle="tab" data-bs-target="#demo-summary-panel" aria-controls="demo-summary-panel" aria-selected="true">Résumé</button></li>
<li class="nav-item" role="presentation"><button id="demo-details-tab" class="nav-link" type="button" role="tab" data-bs-toggle="tab" data-bs-target="#demo-details-panel" aria-controls="demo-details-panel" aria-selected="false">Détails</button></li>
</ul>
<div class="tab-content pt-3">
<div id="demo-summary-panel" class="tab-pane fade show active" role="tabpanel" aria-labelledby="demo-summary-tab" tabindex="0"><p>Zone de synthèse : placer ici les informations principales.</p></div>
<div id="demo-details-panel" class="tab-pane fade" role="tabpanel" aria-labelledby="demo-details-tab" tabindex="0"><dl class="row"><dt class="col-sm-3">Environnement</dt><dd class="col-sm-9">Démonstration</dd><dt class="col-sm-3">Source</dt><dd class="col-sm-9">Données fictives intégrées à la page</dd></dl></div>
</div>
<div class="accordion" id="demo-accordion">
<div class="accordion-item">
<h3 class="accordion-header" id="demo-accordion-title"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#demo-accordion-panel" aria-expanded="false" aria-controls="demo-accordion-panel">Informations complémentaires</button></h3>
<div id="demo-accordion-panel" class="accordion-collapse collapse" aria-labelledby="demo-accordion-title" data-bs-parent="#demo-accordion"><div class="accordion-body">Ce bloc peut accueillir un tableau secondaire, une liste ou un détail technique.</div></div>
</div>
</div>
</div>
</section>
</main>
</div>
</div>
<!-- MODALE : placée hors de la grille pour éviter les contraintes de positionnement. -->
<div class="modal fade demo-modal" id="demo-modal" tabindex="-1" aria-labelledby="demo-modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable"><div class="modal-content">
<div class="modal-header"><h2 class="modal-title fs-5" id="demo-modal-title">Détail de démonstration</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button></div>
<div class="modal-body"><p id="demo-modal-description" class="mb-0"></p></div>
<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>
// Interactions de démonstration uniquement : aucun appel réseau ni enregistrement.
document.querySelectorAll('[data-demo-action]').forEach(function (button) {
button.addEventListener('click', function () {
document.getElementById('demo-action-result').textContent = button.dataset.demoAction + ' : démonstration exécutée.';
});
});
document.getElementById('demo-modal').addEventListener('show.bs.modal', function (event) {
var server = event.relatedTarget ? event.relatedTarget.getAttribute('data-demo-server') : null;
document.getElementById('demo-modal-title').textContent = server || 'Détail de démonstration';
document.getElementById('demo-modal-description').textContent = server
? 'Informations fictives pour ' + server + '. Ajouter ici le contenu détaillé.'
: 'Exemple de fenêtre modale avec un titre, un contenu et un bouton de fermeture.';
});
document.getElementById('demo-form').addEventListener('submit', function (event) {
event.preventDefault();
document.getElementById('demo-form-result').textContent = 'Aperçu : '
+ document.getElementById('demo-name').value + ' — '
+ document.getElementById('demo-system').value + ' — '
+ document.getElementById('demo-capacity').value + ' Go — '
+ (document.getElementById('demo-enabled').checked ? 'Actif' : 'Inactif')
+ '. Aucune donnée enregistrée.';
});
document.getElementById('demo-form').addEventListener('reset', function () {
document.getElementById('demo-form-result').textContent = '';
});
// API Chart.js 2.x, correspondant au fichier local js/chart.min.js.
new Chart(document.getElementById('demo-chart').getContext('2d'), {
type: 'doughnut',
data: {
labels: ['Disponibles', 'À vérifier', 'Indisponibles'],
datasets: [{ data: [4, 1, 1], backgroundColor: ['#198754', '#ffc107', '#dc3545'], borderWidth: 0 }]
},
options: { responsive: true, maintainAspectRatio: false, legend: { display: false } }
});
// Conserver le thème du tableau après une recherche, un tri ou une pagination.
$('#demo-table').on('post-body.bs.table', function () {
$(this).toggleClass('table-dark', document.body.classList.contains('bg-dark'));
});
</script>
</body>
</html>

View File

@@ -60,15 +60,6 @@
<a href="/crossover/GlobalCrossover.php" class="nav-link px-0 align-middle"> <a href="/crossover/GlobalCrossover.php" class="nav-link px-0 align-middle">
<i class="fs-6 bi-table text-white"></i> <span class="ms-1 d-none d-sm-inline text-white h7"><?php echo $nav_2; ?></span></a> <i class="fs-6 bi-table text-white"></i> <span class="ms-1 d-none d-sm-inline text-white h7"><?php echo $nav_2; ?></span></a>
</li> </li>
<li>
<a href="#AD" data-bs-toggle="collapse" class="nav-link px-0 align-middle">
<i class="fs-6 bi bi-shield-lock me-1 text-white"></i> <span class="ms-1 d-none d-sm-inline text-white h7">AD</span><i class="bi bi-caret-down"></i> </a>
<ul class="collapse nav flex-column ms-1" id="AD" data-bs-parent="#menu">
<li class="w-100">
<a href="/AD/LocalAdmins.php" class="nav-link px-0"> <span class="d-none d-sm-inline text-white h7">- Local Admins</span></a>
</li>
</ul>
</li>
<li> <li>
<a href="#SharedFolders" data-bs-toggle="collapse" class="nav-link px-0 align-middle"> <a href="#SharedFolders" data-bs-toggle="collapse" class="nav-link px-0 align-middle">
<i class="fs-6 bi bi-folder-symlink-fill text-white"></i> <span class="ms-1 d-none d-sm-inline text-white h7"><?php echo $nav_4; ?></span><i class="bi bi-caret-down"></i> </a> <i class="fs-6 bi bi-folder-symlink-fill text-white"></i> <span class="ms-1 d-none d-sm-inline text-white h7"><?php echo $nav_4; ?></span><i class="bi bi-caret-down"></i> </a>
@@ -157,6 +148,7 @@
<li class="w-100"> <li class="w-100">
<a href="/AD/GroupReview.php" class="nav-link px-0" > <span class="d-none d-sm-inline text-white h7">- <?php echo $nav_1_1; ?></span></a> <a href="/AD/GroupReview.php" class="nav-link px-0" > <span class="d-none d-sm-inline text-white h7">- <?php echo $nav_1_1; ?></span></a>
<a href="/AD/GroupSearch.php" class="nav-link px-0" > <span class="d-none d-sm-inline text-white h7">- <?php echo $nav_1_2; ?></span></a> <a href="/AD/GroupSearch.php" class="nav-link px-0" > <span class="d-none d-sm-inline text-white h7">- <?php echo $nav_1_2; ?></span></a>
<a href="/AD/LocalAdmins.php" class="nav-link px-0"> <span class="d-none d-sm-inline text-white h7">- Local Admins</span></a>
</li> </li>
</ul> </ul>
</li> </li>

View File

@@ -1,3 +1,4 @@
<?php ob_start(); ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -73,13 +74,22 @@
<?php <?php
/* ============================================================================ /* ============================================================================
Unified backup reporting : Veeam (target) + NetBackup (legacy) Unified backup reporting : Veeam + NetBackupJobs (dun-bkp-01)
During the migration a VM can still exist in both SQL tables. During the migration a VM can still exist in both backup sources.
It is displayed only on the side holding the most recent backup. It is displayed only on the side holding the most recent backup.
========================================================================== */ ========================================================================== */
/* ---------------------------------------------------------------- helpers */ /* ---------------------------------------------------------------- helpers */
// NetBackup can store a client as FQDN while CMDB/Veeam often use a short name.
// Use the short hostname as the common comparison key.
function normalize_host($name) {
$name = strtoupper(trim((string) $name));
if ($name === '') return '';
$parts = explode('.', $name, 2);
return $parts[0];
}
// Whole days between a date and today. Returns null when the date is unusable. // Whole days between a date and today. Returns null when the date is unusable.
function days_since($dateStr) { function days_since($dateStr) {
if (empty($dateStr)) return null; if (empty($dateStr)) return null;
@@ -110,14 +120,14 @@ function fmt_lastgood($dateStr) {
$activeRows = Invoke_Infra("SELECT name FROM cmdb_vms $activeRows = Invoke_Infra("SELECT name FROM cmdb_vms
WHERE CAST(lastinventory AS DATETIME) > DATEADD(DAY, -2, GETDATE())"); WHERE CAST(lastinventory AS DATETIME) > DATEADD(DAY, -2, GETDATE())");
$active = []; $active = [];
foreach ($activeRows as $r) { $active[strtoupper(trim($r['name']))] = true; } foreach ($activeRows as $r) { $active[normalize_host($r['name'])] = true; }
/* -------------------------------------------------------------- 1. Veeam */ /* -------------------------------------------------------------- 1. Veeam */
$veeam = []; $veeam = [];
$rows = Invoke_Infra("SELECT * FROM Veeam_Reporting"); $rows = Invoke_Infra("SELECT * FROM Veeam_Reporting");
foreach ($rows as $row) { foreach ($rows as $row) {
$key = strtoupper(trim($row['ClientName'])); $key = normalize_host($row['ClientName']);
if (!isset($active[$key])) continue; if (!isset($active[$key])) continue;
$ageGood = days_since($row['LastSuccess']); $ageGood = days_since($row['LastSuccess']);
@@ -149,55 +159,187 @@ foreach ($rows as $row) {
} }
/* ---------------------------------------------------------- 2. NetBackup */ /* ---------------------------------------------------------- 2. NetBackup */
/*
Source principale : dbo.NetBackupJobs
Master pris en compte : dun-bkp-01 uniquement.
Pour chaque client NetBackup :
- le dernier FULL/INCREMENTAL donne l'état courant ;
- le dernier FULL/INCREMENTAL avec status <= 1 donne "Last Good Backup" ;
- dbo.NBRC fournit la description du code retour.
La page reste une synthèse VM : seuls les clients présents dans cmdb_vms
avec un inventaire récent sont retenus.
*/
$netbackup = []; $netbackup = [];
$rows = Invoke_Infra("SELECT * FROM VMs_Backup
WHERE (Owner LIKE 'DUN-VMH%' OR Owner LIKE 'MDK-VMH%') $rows = Invoke_Infra("
AND name NOT LIKE 'WS%' WITH ScheduledJobs AS
AND owner NOT LIKE '%WKG%' (
AND owner NOT LIKE '%VMH-WM%' SELECT
ORDER BY name"); j.master,
j.job,
j.client,
j.policy,
j.schedule_type,
j.status,
j.start_date,
j.end_date,
j.size_kb,
COALESCE(j.end_date, j.start_date) AS run_date,
MAX(
CASE
WHEN j.status <= 1
THEN COALESCE(j.end_date, j.start_date)
ELSE NULL
END
) OVER (
PARTITION BY j.client
) AS last_success,
ROW_NUMBER() OVER
(
PARTITION BY j.client
ORDER BY
COALESCE(j.end_date, j.start_date) DESC,
j.job DESC
) AS rn
FROM dbo.NetBackupJobs j
WHERE
j.master like 'dun-bkp-01%'
AND j.schedule_type IN ('full', 'incremental')
),
Latest AS
(
SELECT *
FROM ScheduledJobs
WHERE rn = 1
)
SELECT
l.master,
l.job,
l.client,
l.policy,
l.schedule_type,
l.status,
l.start_date,
l.end_date,
l.run_date,
l.last_success,
l.size_kb,
r.RC_DESC AS status_desc
FROM Latest l
LEFT JOIN dbo.NBRC r
ON r.RC = l.status
ORDER BY l.client
");
if (!is_array($rows)) $rows = [];
foreach ($rows as $row) { 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. $key = normalize_host($row['client']);
if (!$excluded && !isset($active[$key])) continue;
$result = trim((string) $row['LastResult']); // This synthesis page is VM-oriented.
$ageGood = days_since($row['LastKnownGood']); if ($key === '' || !isset($active[$key])) continue;
if ($excluded) { $statusCode = (int) $row['status'];
$cat = 'excluded'; $cls = 'table-secondary'; $ageGood = days_since($row['last_success']);
} elseif ($result !== 'OK') {
// A failed job with a still fresh good copy is only a warning. if ($statusCode > 1) {
// Same policy as before:
// a failed latest run remains only a warning while a recent good copy exists.
if ($ageGood === null || $ageGood >= backup_tolerance_days()) { if ($ageGood === null || $ageGood >= backup_tolerance_days()) {
$cat = 'error'; $cls = 'table-danger'; $cat = 'error';
$cls = 'table-danger';
} else { } else {
$cat = 'warning'; $cls = 'table-warning'; $cat = 'warning';
$cls = 'table-warning';
} }
} elseif ($ageGood === null || $ageGood >= backup_tolerance_days()) { } elseif ($ageGood === null || $ageGood >= backup_tolerance_days()) {
$cat = 'outdated'; $cls = 'table-info'; $cat = 'outdated';
$cls = 'table-info';
} else { } else {
$cat = 'ok'; $cls = 'table-success'; $cat = 'ok';
$cls = 'table-success';
} }
$statusDesc = trim((string)($row['status_desc'] ?? ''));
if ($statusCode <= 1) {
$statusText = 'OK';
} else {
$statusText = 'Error ' . $statusCode;
if ($statusDesc !== '') {
$statusText .= ' - ' . $statusDesc;
}
}
$runDate = $row['run_date'] ?? $row['start_date'] ?? null;
$netbackup[$key] = [ $netbackup[$key] = [
'cat' => $cat, 'cat' => $cat,
'row_class' => $cls, 'row_class' => $cls,
'excluded' => $excluded, 'excluded' => false,
'ts' => strtotime(trim($row['LastBackup'] . ' ' . $row['TimeStamp'])) ?: 0, 'ts' => strtotime((string)$runDate) ?: 0,
'vm' => $row['Name'], 'vm' => $row['client'],
'last_run' => $excluded ? 'Tag NoBackup' : fmt_datetime($row['LastBackup'] . ' ' . $row['TimeStamp']), 'last_run' => fmt_datetime($runDate),
'status' => $excluded ? 'Tag NoBackup' : ($result !== '' ? $result : 'Unknown'), 'status' => $statusText,
'last_good' => $excluded ? 'Tag NoBackup' : fmt_lastgood($row['LastKnownGood']), 'last_good' => fmt_lastgood($row['last_success']),
'size' => $excluded ? '' : $row['LastSize'], 'size' => ($row['size_kb'] === null || $row['size_kb'] === '')
'extra' => $row['Owner'], ? ''
'policy' => $row['Policy'], : number_format(((float)$row['size_kb']) / 1048576, 1, '.', ''),
'message' => '', 'extra' => $row['schedule_type'],
'policy' => $row['policy'],
'message' => ($statusCode > 1) ? $statusDesc : '',
]; ];
} }
/*
Keep only the explicit NoBackup exclusions from the old VMs_Backup table.
NetBackupJobs cannot represent a deliberate exclusion because no backup job
exists for an intentionally excluded VM.
*/
$excludedRows = Invoke_Infra("
SELECT name, Exclusion, Owner, Policy
FROM VMs_Backup
WHERE Exclusion IS NOT NULL
AND LTRIM(RTRIM(Exclusion)) <> ''
");
if (is_array($excludedRows)) {
foreach ($excludedRows as $row) {
$key = normalize_host($row['name']);
// Do not overwrite a VM that has actual dun-bkp-01 job history.
if ($key === '' || isset($netbackup[$key])) continue;
$netbackup[$key] = [
'cat' => 'excluded',
'row_class' => 'table-secondary',
'excluded' => true,
'ts' => 0,
'vm' => $row['name'],
'last_run' => 'Tag NoBackup',
'status' => 'Tag NoBackup',
'last_good' => 'Tag NoBackup',
'size' => '',
'extra' => '',
'policy' => $row['Policy'],
'message' => trim((string)$row['Exclusion']),
];
}
}
/* ------------------------------------------- 3. Migration de-duplication */ /* ------------------------------------------- 3. Migration de-duplication */
// A VM present on both sides is kept where the most recent backup lives. // A VM present on both sides is kept where the most recent backup lives.
@@ -345,7 +487,7 @@ function render_block($blockId, $meta, $rows, $extraLabel, $expanded = false) {
<div class="accordion mb-4" id="acc_nb"> <div class="accordion mb-4" id="acc_nb">
<?php <?php
foreach (['error', 'warning', 'outdated', 'ok', 'excluded'] as $cat) { foreach (['error', 'warning', 'outdated', 'ok', 'excluded'] as $cat) {
render_block('nb_' . $cat, $blockMeta[$cat], $buckets['nb'][$cat], $GLOBALS['w_host'], false); render_block('nb_' . $cat, $blockMeta[$cat], $buckets['nb'][$cat], 'Schedule', false);
} }
?> ?>
</div> </div>
@@ -408,3 +550,4 @@ function render_block($blockId, $meta, $rows, $extraLabel, $expanded = false) {
</body> </body>
</html> </html>
<?php ob_end_flush(); ?>