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