Add VIO FC Status monitoring page and enhance server inventory filtering

- Introduced `Storage/VIO.php` for monitoring VIO Fibre Channel status with client balancing checks and table sorting.
- Updated `/include/en.php` and `/include/fr.php` to include new translations for ServiceNow and VIO-related navigation elements.
- Improved SQL query and filtering logic in `/X/Inventory.php` to exclude VIO entries and add XenSam version data.
- Enhanced `/reports/heartbeat.php` with a new mechanism for handling server heartbeat status, allowing better granularity and readability.
This commit is contained in:
2026-09-14 10:32:15 +02:00
parent 1f1b7dcd22
commit c185d099d6
25 changed files with 6646 additions and 437 deletions

941
reports/Netbackup_Daily.php Normal file
View File

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