index.php
<?php
declare(strict_types=1);
/*
|--------------------------------------------------------------------------
| Dashboard page
|--------------------------------------------------------------------------
|
| The single entry point for the admin dashboard. Most of the actual work
| happens in the required files below before any HTML is output:
| - startup.php: install/auth/maintenance gate (can exit early).
| - dashboard/_init.php: parses the period/site from the query string into
| $view, $day/$week/$month/$year, $rangeSql/$rangeParams, $currentSite...
| - dashboard/counters.php, global.php, privacy.php, referrers.php,
| top_pages.php: each computes the data for one dashboard section.
|
| The HTML below just renders what those files prepared — by the time we
| reach the <!doctype html> tag, every $variable used in the page (view,
| counters, topPages, referrers, etc.) already exists.
|
*/
defined('START_TIME') || define('START_TIME', microtime(true));
require_once __DIR__ . '/includes/core.php';
brivaciaSecurityHeaders();
loadTranslations();
require_once __DIR__ . '/includes/startup.php';
require_once __DIR__ . '/includes/dashboard/_init.php';
require_once __DIR__ . '/includes/dashboard/counters.php';
require_once __DIR__ . '/includes/dashboard/global.php';
require_once __DIR__ . '/includes/dashboard/privacy.php';
require_once __DIR__ . '/includes/dashboard/referrers.php';
require_once __DIR__ . '/includes/dashboard/top_pages.php';
require_once __DIR__ . '/includes/modules/update.php';
require_once __DIR__ . '/includes/modules/version.php';
/*
|--------------------------------------------------------------------------
| Graph buttons visibility
|--------------------------------------------------------------------------
|
| Keep these rules here in index.php so the dashboard stays easy to read:
| the class is added directly on each button instead of being hidden in a
| helper function.
|
*/
$hideLineGraphs = $view === 'today';
$hideCountriesGraphs = count($countries) < 2;
$hideGlobalMapGraph = !$showCountries;
$hideReferrersGraphs = count($referrers) < 2;
$hideSearchEnginesGraphs = count($searchEngines) < 2;
$hideTopPagesGraph = $topTotal < 2;
$restoreAlert = brivaciaRestoreAlert();
?>
<!doctype html>
<html data-theme="<?= h($theme) ?>" dir="<?= h(currentDir()) ?>" lang="<?= h(currentLang()) ?>">
<head>
<title><?= h(t('title.statistics')) ?> • <?= brivacia_setting('dashboard.instance_name', 'Brivacia') ?></title>
<meta charset="utf-8">
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="<?= h(assetOrPlaceholder('favicon', brivaciaFaviconPlaceholder(), $currentSite ?? '')) ?>">
<link rel="stylesheet" type="text/css" href="/assets/css/style.min.css">
</head>
<body>
<header class="toolbar">
<h1 data-tooltip="<?= h(t('ui.back.home')) ?>"><a href="/"><img src="<?= assetOrPlaceholder('logo', brivaciaLogoPlaceholder(), $currentSite) ?>" alt="<?= h($siteTitle) ?>"><?= h($siteTitle) ?> <?= h(t('title.statistics')) ?></a></h1>
<div class="toolbar-filters">
<form class="date-form" method="get">
<button aria-label="<?= h(t('ui.show.calendar')) ?>" data-period-type="<?= h($view) ?>" data-period-value="<?= h($periodInputValue) ?>" data-tooltip="<?= h(t('ui.show.calendar')) ?>" data-tooltip-placement="left" data-week-label="<?= h(t('form.week')) ?>" tabindex="0" type="button"><span data-live-date><?= h($periodLabel) ?></span><small data-current-time></small></button>
<input name="view" type="hidden" value="<?= h($view) ?>">
<?php if ($view !== 'all'): ?><input data-ios-type="<?= h($periodIosInputType ?? $periodInputType) ?>" data-ios-value="<?= h($periodIosInputValue ?? $periodInputValue) ?>" id="period" name="<?= h($periodInputName) ?>" type="<?= h($periodInputType) ?>" value="<?= h($periodInputValue) ?>" data-auto-submit><?php endif; ?>
<small class="ios-time" data-current-time></small>
</form>
<nav class="view-tabs">
<?php $viewLabels = ['today' => t('form.today'), 'week' => t('form.week'), 'month' => t('form.month'), 'year' => t('form.year'), 'all' => t('form.all'),]; ?>
<?php foreach ($viewLabels as $key => $label): ?><a class="<?= $view === $key ? 'active' : '' ?>" href="<?= h(dashboardViewUrl($key, $day, $week, $month, $year, ['site' => $currentSite])) ?>"><?= h($label) ?></a><?php endforeach; ?>
</nav>
<form method="get" class="view-select">
<select name="view" data-auto-submit>
<?php foreach ($viewLabels as $key => $label): ?>
<option value="<?= h($key) ?>" <?= $view === $key ? 'selected' : '' ?>>
<?= h($label) ?>
</option>
<?php endforeach; ?>
</select>
<?php if ($view === 'today'): ?>
<input type="hidden" name="day" value="<?= h($day) ?>">
<?php elseif ($view === 'week'): ?>
<input type="hidden" name="week" value="<?= h($week) ?>">
<?php elseif ($view === 'month'): ?>
<input type="hidden" name="month" value="<?= h($month) ?>">
<?php elseif ($view === 'year'): ?>
<input type="hidden" name="year" value="<?= h($year) ?>">
<?php endif; ?>
<?php if ($currentSite !== ''): ?>
<input type="hidden" name="site" value="<?= h($currentSite) ?>">
<?php endif; ?>
</form>
</div>
<div class="toolbar-actions">
<button data-privacy-open data-tooltip="<?= h(t('privacy.title', ['instance' => brivacia_setting('dashboard.instance_name', 'Brivacia')])) ?>" type="button"><?= icon('privacy') ?> <?= h(t('privacy.button')) ?></button>
<?php if (count(brivacia_sites()) > 1): ?>
<div class="sites-menu" data-dropdown>
<button data-dropdown-toggle data-tooltip="<?= h(t('ui.sites')) ?>" data-tooltip-placement="left" type="button"><?= icon('activity') ?> <?= h($currentSite === '' ? t('ui.sites.all') : $currentSite) ?> </button>
<div class="site-dropdown" data-dropdown-menu hidden>
<?php if ($currentSite !== ''): ?>
<a href="<?= h(dashboardViewUrl($view, $day, $week, $month, $year, ['site' => ''])) ?>"><?= h(t('ui.sites.all')) ?></a>
<?php endif; ?>
<?php foreach (array_keys(brivacia_sites()) as $siteCode): ?>
<?php if ($siteCode === $currentSite) continue; ?>
<a href="<?= h(dashboardViewUrl($view, $day, $week, $month, $year, ['site' => $siteCode])) ?>"><?= h($siteCode) ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if (brivaciaUpdateAvailable()): ?>
<div class="notification-menu" data-dropdown>
<button aria-label="<?= h(t('update.available')) ?>" data-dropdown-toggle id="updates" title="<?= h(t('update.available')) ?>" type="button">
<?= icon('bell') ?>
<span></span>
</button>
<div class="notification-dropdown" data-dropdown-submenu hidden>
<div id="update-available">
<h3><?= h(t('update.available')) ?></h3>
<p><strong>Brivacia v<?= h(brivaciaLatestVersion()) ?></strong></p>
<p><?= h(t('update.description')) ?></p>
<button data-update-cleanup="<?= h(t('update.cleanup')) ?>" data-update-download="<?= h(t('update.download')) ?>" data-update-extract="<?= h(t('update.extract')) ?>" data-update-failed="<?= h(t('update.failed')) ?>" data-update-installing="<?= h(t('update.installing')) ?>" data-update-reload="<?= h(t('update.reload')) ?>" data-update-starting="<?= h(t('update.starting')) ?>" data-update-verify="<?= h(t('update.verify')) ?>" id="update-install" type="button">
<?= icon('export') ?>
<?= h(t('update.install')) ?>
</button>
</div>
<div id="update-progress" hidden>
<h3><?= h(t('update.running')) ?></h3>
<p id="update-progress-text"><?= h(t('update.starting')) ?></p>
<small><?= h(t('update.wait')) ?></small>
</div>
</div>
</div>
<?php endif; ?>
<div class="menu" data-dropdown>
<button data-dropdown-toggle data-tooltip="<?= h(t('ui.menu')) ?>" data-tooltip-placement="left" type="button"><?= icon('menu') ?></button>
<div class="menu-dropdown" data-dropdown-menu hidden>
<button data-modal-open="pixel-modal" data-tooltip="<?= h(t('ui.pixel.open', ['instance' => brivacia_setting('dashboard.instance_name', 'Brivacia')])) ?>" data-tooltip-placement="left" type="button"><?= icon('code') ?> <?= h(t('ui.pixel')) ?></button>
<div class="export-menu" data-dropdown>
<button data-dropdown-toggle type="button"><?= icon('export') ?> <?= h(t('ui.export')) ?></button>
<?php $exportParams = ltrim(dashboardViewUrl($view, $day, $week, $month, $year, ['site' => $currentSite]), '?'); ?>
<div class="export-dropdown" data-dropdown-submenu hidden>
<a href="/api/export.php?format=csv&<?= h($exportParams) ?>"><?= icon('csv') ?> CSV</a>
<a href="/api/export.php?format=json&<?= h($exportParams) ?>"><?= icon('json') ?> JSON</a>
<a href="/api/export.php?format=txt&<?= h($exportParams) ?>"><?= icon('text') ?> TXT</a>
<a href="/api/export.php?format=legacy_detailed" data-tooltip="<?= h(t('ui.export.legacy.detailed.help')) ?>" data-tooltip-placement="left"><?= icon('json') ?> <?= h(t('ui.export.legacy.detailed')) ?></a>
<a href="/api/export.php?format=legacy_totals" data-tooltip="<?= h(t('ui.export.legacy.totals.help')) ?>" data-tooltip-placement="left"><?= icon('json') ?> <?= h(t('ui.export.legacy.totals')) ?></a>
</div>
</div>
<div class="import-menu" data-dropdown>
<button data-dropdown-toggle type="button"><?= icon('import') ?> <?= h(t('ui.import')) ?></button>
<div class="import-dropdown" data-dropdown-submenu hidden>
<button data-modal-open="import-modal" data-import-provider="adobe_analytics_api" data-tooltip="<?= h(t('import.provider.unavailable')) ?>" data-tooltip-placement="left" disabled type="button"><?= icon('adobe_analytics') ?> Adobe Analytics</button>
<button data-modal-open="import-modal" data-import-provider="google_analytics_api" data-tooltip="<?= h(t('import.provider.unavailable')) ?>" data-tooltip-placement="left" disabled type="button"><?= icon('google_analytics') ?> Google Analytics</button>
<button data-modal-open="import-modal" data-import-provider="matomo_db" type="button"><?= icon('matomo') ?> matomo</button>
<button data-modal-open="import-modal" data-import-provider="plausible_db" data-tooltip="<?= h(t('import.provider.unavailable')) ?>" data-tooltip-placement="left" disabled type="button"><?= icon('plausible') ?> Plausible</button>
<button data-modal-open="import-modal" data-import-provider="umami_db" data-tooltip="<?= h(t('import.provider.unavailable')) ?>" data-tooltip-placement="left" disabled type="button"><?= icon('umami') ?> Umami</button>
</div>
</div>
<button data-modal-open="settings-modal" type="button"><?= icon('settings') ?> <?= h(t('ui.settings')) ?></button>
<hr>
<a href="https://breat.fr/contact" target="_blank" rel="noopener"><?= icon('feedback') ?> <?= t('ui.feedback') ?></a>
<hr>
<form id="logout-form" method="post">
<input name="auth_action" type="hidden" value="logout">
</form>
<button form="logout-form" type="submit"><?= icon('log-out') ?> <?= h(t('auth.logout.button')) ?></button>
</div>
</div>
</div>
</header>
<?php if ($restoreAlert): ?>
<div class="restore-alert" data-restore-alert>
<div>
<strong><?= h(t('ui.restore.detected')) ?></strong>
<?php $time = DateTime::createFromFormat('Y-m-d H:i:s', $restoreAlert['time']); ?>
<p class="restore-alert-time"><?= h($time?->format('d/m/Y H:i') ?? '') ?></p>
<p><?= h(t('ui.restore.description')) ?></p>
<small><?= h(t('ui.restore.backup')) ?> <?= h((string)($restoreAlert['backup'] ?? '')) ?></small>
</div>
<button aria-label="<?= h(t('ui.close')) ?>" data-restore-alert-close type="button">
<?= icon('close') ?>
</button>
</div>
<?php endif; ?>
<div class="cards">
<div class="card">
<span><?= h(t('metric.unique.visitors')) ?><button aria-label="<?= h(t('ui.show.graph.line')) ?>" <?= $hideLineGraphs ? 'class="hidden"' : '' ?> data-graph="unique_visitors" data-tooltip="<?= h(t('ui.show.graph.line')) ?>" tabindex="0" type="button"><?= icon('chart-line') ?></button></span>
<strong><?= h($today['unique_visitors'] ?? 0) ?></strong>
</div>
<div class="card">
<span><?= h(t('metric.visits')) ?><button aria-label="<?= h(t('ui.show.graph.line')) ?>" <?= $hideLineGraphs ? 'class="hidden"' : '' ?> data-graph="visits" data-tooltip="<?= h(t('ui.show.graph.line')) ?>" tabindex="0" type="button"><?= icon('chart-line') ?></button></span>
<strong><?= h($today['visits'] ?? 0) ?></strong>
</div>
<div class="card">
<span><?= h(t('metric.page.views')) ?><button aria-label="<?= h(t('ui.show.graph.line')) ?>" <?= $hideLineGraphs ? 'class="hidden"' : '' ?> data-graph="page_views" data-tooltip="<?= h(t('ui.show.graph.line')) ?>" tabindex="0" type="button"><?= icon('chart-line') ?></button></span>
<strong><?= h($today['pageviews'] ?? 0) ?></strong>
</div>
<div class="card">
<span><?= h(t('metric.bots')) ?><button aria-label="<?= h(t('ui.bots.info')) ?>" data-tooltip="<?= h(t('ui.bots.info')) ?>" tabindex="0" type="button"><?= icon('info') ?></button></span>
<strong><?= h($today['bots'] ?? 0) ?></strong>
</div>
<div class="card"><span><?= h(t('metric.average.visitors.per.day')) ?></span><strong><?= h($avgDay['avg_unique'] ?? 0) ?></strong></div>
<div class="card"><span><?= h(t('metric.average.visits.per.day')) ?></span><strong><?= h($avgDay['avg_visits'] ?? 0) ?></strong></div>
<div class="card"><span><?= h(t('metric.average.pageviews.per.day')) ?></span><strong><?= h($avgDay['avg_views'] ?? 0) ?></strong></div>
</div>
<div class="trends">
<?php if (brivacia_setting('trends.visitors', true) && $view !== 'all'): ?>
<div class="card" data-tooltip="<?= h(t('metric.change.visitors.tooltip')) ?>">
<span><?= h(t('metric.change.visitors')) ?></span>
<?php if ($visitorsChange !== null): ?>
<strong class="<?= $visitorsChange >= 0 ? 'success' : 'danger' ?>">
<?= $visitorsChange > 0 ? '+' : '' ?><?= h($visitorsChange) ?>%
</strong>
<small><?= h($visitsChangeLabel) ?></small>
<?php else: ?>
<strong class="trend-new-day"><?= icon('moon') ?><?= icon('sun') ?></strong>
<small><?= h($trendPlaceholderText) ?></small>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (brivacia_setting('trends.visits', true) && $view !== 'all'): ?>
<div class="card" data-tooltip="<?= h(t('metric.change.visits.tooltip')) ?>">
<span><?= h(t('metric.change.visits')) ?></span>
<?php if ($visitsChange !== null): ?>
<strong class="<?= $visitsChange >= 0 ? 'success' : 'danger' ?>">
<?= $visitsChange > 0 ? '+' : '' ?><?= h($visitsChange) ?>%
</strong>
<small><?= h($visitsChangeLabel) ?></small>
<?php else: ?>
<strong class="trend-new-day"><?= icon('moon') ?><?= icon('sun') ?></strong>
<small><?= h($trendPlaceholderText) ?></small>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (brivacia_setting('trends.pageviews', true) && $view !== 'all'): ?>
<div class="card" data-tooltip="<?= h(t('metric.change.pageviews.tooltip')) ?>">
<span><?= h(t('metric.change.pageviews')) ?></span>
<?php if ($pageviewsChange !== null): ?>
<strong class="<?= $pageviewsChange >= 0 ? 'success' : 'danger' ?>">
<?= $pageviewsChange > 0 ? '+' : '' ?><?= h($pageviewsChange) ?>%
</strong>
<small><?= h($visitsChangeLabel) ?></small>
<?php else: ?>
<strong class="trend-new-day"><?= icon('moon') ?><?= icon('sun') ?></strong>
<small><?= h($trendPlaceholderText) ?></small>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="summary-row <?= !$showCountries ? 'summary-row--no-countries' : '' ?>">
<section>
<h2>
<?= h(t('section.global.total')) ?>
<button aria-label="<?= h(t('ui.show.graph.map')) ?>" <?= $hideGlobalMapGraph ? 'class="hidden"' : '' ?> data-graph="global_map" data-tooltip="<?= h(t('ui.show.graph.map')) ?>" tabindex="0" type="button"><?= icon('chart-map') ?></button>
<button aria-label="<?= h(t('ui.expand')) ?>" data-modal-open="global-modal" data-tooltip="<?= h(t('ui.expand')) ?>" tabindex="0" type="button"><?= icon('expand') ?></button>
</h2>
<table>
<tbody>
<tr>
<td><?= icon('visitor') ?> <?= h(t('metric.unique.visitors')) ?></td>
<td><?= h($globalTotal['unique_visitors']) ?></td>
</tr>
<tr>
<td><?= icon('visits') ?> <?= h(t('metric.visits')) ?></td>
<td><?= h($globalTotal['visits']) ?></td>
</tr>
<tr>
<td><?= icon('page') ?> <?= h(t('metric.page.views')) ?></td>
<td><?= h($globalTotal['pageviews']) ?></td>
</tr>
<tr>
<td><?= icon('bot') ?> <?= h(t('metric.bots')) ?></td>
<td><?= h($globalTotal['bots'] ?? 0) ?></td>
</tr>
<?php if ($showCountries): ?>
<tr>
<td><?= icon('countries') ?> <?= h(t('section.countries')) ?></td>
<td><?= h($globalTotal['countries']) ?></td>
</tr>
<?php endif; ?>
<tr>
<td><?= icon('search') ?> <?= h(t('section.search.engines')) ?></td>
<td><?= h($globalTotal['search_engines'] ?? 0) ?></td>
</tr>
<tr>
<td><?= icon('referrals') ?> <?= h(t('section.referrers')) ?></td>
<td><?= h($globalTotal['referrers']) ?></td>
</tr>
</tbody>
</table>
</section>
<?php if ($showCountries): ?>
<section class="countries">
<h2>
<?= h(t('section.countries')) ?>
<button aria-label="<?= h(t('ui.show.graph.bar')) ?>" <?= $hideCountriesGraphs ? 'class="hidden"' : '' ?> data-graph="countries_bar" data-tooltip="<?= h(t('ui.show.graph.bar')) ?>" tabindex="0" type="button"><?= icon('chart-bar') ?></button>
<button aria-label="<?= h(t('ui.show.graph.pie')) ?>" <?= $hideCountriesGraphs ? 'class="hidden"' : '' ?> data-graph="countries_pie" data-tooltip="<?= h(t('ui.show.graph.pie')) ?>" tabindex="0" type="button"><?= icon('chart-pie') ?></button>
<button aria-label="<?= h(t('ui.expand')) ?>" data-modal-open="countries-modal" data-tooltip="<?= h(t('ui.expand')) ?>" tabindex="0" type="button"><?= icon('expand') ?></button>
</h2>
<table>
<tbody>
<?php foreach ($countries as $row): ?>
<?php $cc = strtolower((string)$row['country']); ?>
<tr>
<td>
<?= countryFlag($cc) ?> <span data-country="<?= h(strtoupper($cc)) ?>"><?= h(countryName($cc)) ?></span>
</td>
<td><?= h($row['views']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
<?php endif; ?>
<section>
<h2>
<?= h(t('section.search.engines')) ?>
<button aria-label="<?= h(t('ui.show.graph.bar')) ?>" <?= $hideSearchEnginesGraphs ? 'class="hidden"' : '' ?> data-graph="search_engines_bar" data-tooltip="<?= h(t('ui.show.graph.bar')) ?>" tabindex="0" type="button"><?= icon('chart-bar') ?></button>
<button aria-label="<?= h(t('ui.show.graph.pie')) ?>" <?= $hideSearchEnginesGraphs ? 'class="hidden"' : '' ?> data-graph="search_engines_pie" data-tooltip="<?= h(t('ui.show.graph.pie')) ?>" tabindex="0" type="button"><?= icon('chart-pie') ?></button>
<button aria-label="<?= h(t('ui.expand')) ?>" data-modal-open="search-engines-modal" data-tooltip="<?= h(t('ui.expand')) ?>" tabindex="0" type="button"><?= icon('expand') ?></button>
</h2>
<table>
<tbody>
<?php foreach ($searchEngines as $row): ?>
<tr>
<td>
<?php if (($row['referrer'] ?? '') === BRIVACIA_BLOCKED): ?>
<?= icon('blocked') ?>
<?php else: ?>
<?= referrerIconHtml((string)$row['referrer']) ?>
<?php endif; ?>
<?= h((string)$row['label']) ?>
</td>
<td><?= h($row['views']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
<section>
<h2>
<?= h(t('section.referrers')) ?>
<button aria-label="<?= h(t('ui.show.graph.bar')) ?>" <?= $hideReferrersGraphs ? 'class="hidden"' : '' ?> data-graph="referrers_bar" data-tooltip="<?= h(t('ui.show.graph.bar')) ?>" tabindex="0" type="button"><?= icon('chart-bar') ?></button>
<button aria-label="<?= h(t('ui.show.graph.pie')) ?>" <?= $hideReferrersGraphs ? 'class="hidden"' : '' ?> data-graph="referrers_pie" data-tooltip="<?= h(t('ui.show.graph.pie')) ?>" tabindex="0" type="button"><?= icon('chart-pie') ?></button>
<button aria-label="<?= h(t('ui.expand')) ?>" data-modal-open="referrers-modal" data-tooltip="<?= h(t('ui.expand')) ?>" tabindex="0" type="button"><?= icon('expand') ?></button>
</h2>
<table>
<tbody>
<?php foreach ($referrers as $row): ?>
<tr>
<td>
<?php if (($row['referrer'] ?? '') === BRIVACIA_BLOCKED): ?>
<?= icon('blocked') ?>
<?php else: ?>
<?= referrerIconHtml((string)$row['referrer']) ?>
<?php endif; ?>
<?= h((string)$row['label']) ?>
</td>
<td><?= h($row['views']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
</div>
<section class="top-pages">
<section>
<div class="section-header">
<h2><?= h(t('section.top.pages')) ?> <button aria-label="<?= h(t('ui.show.graph.bar')) ?>" <?= $hideTopPagesGraph ? 'class="hidden"' : '' ?> data-graph="top_pages" data-tooltip="<?= h(t('ui.show.graph.bar')) ?>" tabindex="0" type="button"><?= icon('chart-bar') ?></button></h2>
<form method="get" class="per-page">
<?php if ($view === 'today'): ?>
<input name="day" type="hidden" value="<?= h($day) ?>">
<?php elseif ($view === 'week'): ?>
<input name="week" type="hidden" value="<?= h($week) ?>">
<?php elseif ($view === 'month'): ?>
<input name="month" type="hidden" value="<?= h($month) ?>">
<?php elseif ($view === 'year'): ?>
<input name="year" type="hidden" value="<?= h($year) ?>">
<?php endif; ?>
<input name="tp" type="hidden"value="1">
<input name="view" type="hidden" value="<?= h($view) ?>">
<label>
<?= h(t('form.per.page')) ?>
<select name="per" data-auto-submit>
<?php foreach ([20, 50, 100, 500, 1000] as $n): ?>
<option value="<?= $n ?>" <?= $topPerPage === $n ? 'selected' : '' ?>>
<?= $n ?>
</option>
<?php endforeach; ?>
</select>
</label>
</form>
</div>
<?php $showPageLanguage = brivacia_setting('dashboard.show_page_language_in_top_pages', false); ?>
<table>
<thead>
<tr>
<th><?= h(t('table.site')) ?></th>
<?php if ($showPageLanguage): ?><th data-mobile-label="<?= h(t('table.language.short')) ?>"><?= h(t('table.language')) ?></th><?php endif; ?>
<th><?= h(t('table.page')) ?></th>
<th><?= h(t('table.views')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($topPages as $row): ?>
<tr>
<td class="site"><?= h((string)$row['site']) ?></td>
<?php if ($showPageLanguage): ?>
<?php
$pageLanguageCode = pageLanguageCode((string)$row['page_key']);
$pageLanguageCountry = pageLanguageCountry((string)$row['page_key']);
?>
<td class="page-language">
<?php if ($pageLanguageCountry !== ''): ?>
<span data-language="<?= h($pageLanguageCode) ?>" tabindex="0">
<?= countryFlag($pageLanguageCountry) ?>
</span>
<?php endif; ?>
</td>
<?php endif; ?>
<td>
<?= externalLink(
pageUrl(
(string)$row['site'],
(string)$row['page_key'],
(string)($row['url'] ?? '')
),
cleanPageTitle(
(string)($row['title'] ?: $row['page_key']),
(string)$row['site'],
(string)$row['page_key'],
(string)($row['url'] ?? '')
),
brivacia_setting('dashboard.show_external_icon_in_top_pages', true)
) ?>
</td>
<td><?= h($row['views']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php include __DIR__ . '/includes/modules/pagination.php'; ?>
</section>
</section>
<?php require __DIR__ . '/includes/modules/modals.php'; ?>
<footer>
<span><?= h(t('footer.powered')) ?><img src="<?= brivaciaLogoPlaceholder() ?>" alt="" aria-hidden="true"><?= externalLink('https://code.breat.fr/b/brivacia/docs/changelog', 'Brivacia ' . brivacia_version(), true, t('ui.show.changelog'), 'top') ?><span aria-hidden="true" class="desktop-only"> • </span><br class="mobile-only"> <?= h(t('footer.created')) ?><img src="<?= brivaciaBreatfrLogoIcon() ?>" alt="" aria-hidden="true"><?= externalLink('https://breat.fr','breat.fr') ?></span>
<span><button data-modal-open="about-modal" type="button"><?= h(t('footer.about')) ?></button> • <?= externalLink('https://code.breat.fr/b/brivacia/docs/faq', h(t('wizard.help.link'))) ?><?= currentLang() !== 'en' ? ' ' . h(t('ui.en')) : '' ?></span>
<span><?= h(t('footer.support')) ?><br class="mobile-only"> <?= h(t('footer.via')) ?> <?= icon('ko-fi') ?> <?= externalLink('https://ko-fi.com/breatfr','Ko-fi') ?> <?= h(t('footer.or')) ?> <?= icon('paypal') ?> <?= externalLink('https://paypal.me/breat','PayPal') ?></span>
</footer>
<!-- Dashboard auto-refresh --><script nonce="<?= h(brivaciaCspNonce()) ?>">window.BRIVACIA_AUTO_REFRESH_SECONDS = <?= max(0, (int) brivacia_setting('dashboard.auto_refresh', 1) * 60) ?>;</script><!-- Dashboard auto-refresh -->
<!-- CSRF protection + auto-submit fields: attaches the token to every
non-GET fetch() call made from this page, and wires up the plain
<select>/<input> fields that used to auto-submit their form via an
inline onchange="" attribute (removed so script-src can drop
'unsafe-inline'). Kept in one nonce'd block instead of touching
main.min.js. --><script nonce="<?= h(brivaciaCspNonce()) ?>">
window.BRIVACIA_CSRF_TOKEN = <?= json_encode(brivaciaCsrfToken(), JSON_UNESCAPED_SLASHES) ?>;
(() => {
const token = window.BRIVACIA_CSRF_TOKEN;
if (!token) return;
const originalFetch = window.fetch.bind(window);
window.fetch = (input, init) => {
init = init || {};
const method = (init.method || 'GET').toUpperCase();
if (method !== 'GET' && method !== 'HEAD') {
const headers = new Headers(init.headers || {});
headers.set('X-Brivacia-CSRF', token);
init = { ...init, headers };
}
return originalFetch(input, init);
};
})();
document.querySelectorAll('[data-auto-submit]').forEach((field) => {
field.addEventListener('change', () => field.form?.submit());
});
</script><!-- /CSRF protection -->
<!-- Main Script --><script defer src="/assets/js/main.min.js"></script><!-- Main Script -->
<!-- Restore alert --><script nonce="<?= h(brivaciaCspNonce()) ?>">
document.querySelector('[data-restore-alert-close]')?.addEventListener('click', async () => {
await fetch('/api/restore_alert.php', { method: 'POST' });
document.querySelector('[data-restore-alert]')?.remove();
});
</script><!-- Restore alert -->
<?php
// Rendering time + request id, output as an HTML comment (never shown to
// the visitor, harmless to leave in) — handy when reporting a slow page
// load without needing server-side profiling tools.
if (defined('START_TIME')) {
echo "\n<!--\n";
echo "=== BRIVACIA DEBUG ===\n\n";
echo "PHP generated in " . round((microtime(true) - START_TIME) * 1000, 3) . " ms\n";
echo "Request ID : " . ($_SERVER['HTTP_X_REQUEST_ID'] ?? 'none') . "\n";
echo "Timestamp : " . date('Y-m-d H:i:s') . "\n\n";
echo "===================\n";
echo "-->\n";
} ?>
</body>
</html>