v1.0.0
PriviMetrics 2
- <?php
-
- declare(strict_types=1);
- require_once __DIR__ . '/functions.php';
- require_once __DIR__ . '/font-options.php';
-
- if (!envIsInstalled()) {
- header('Location: install.php');
- exit;
- }
- require_once __DIR__ . '/updater-functions.php';
-
- requireLogin();
- pmSecurityHeaders();
-
- $csrfToken = generateCSRFToken();
- $flash = null;
- $updateInfo = null;
-
- if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (!verifyCSRFToken($_POST['csrf_token'] ?? null)) {
- $flash = ['type' => 'error', 'text' => 'Invalid security token.'];
- } else {
- $action = $_POST['action'] ?? '';
- try {
- if ($action === 'update_check') {
- $updateInfo = ['manifest' => pmUpdateManifest()];
- } elseif ($action === 'update_preview') {
- $updateInfo = pmUpdatePreview();
- } elseif ($action === 'auto_update') {
- $enable = isset($_POST['auto_update']);
- if (envSet('AUTO_UPDATE', $enable ? 'true' : 'false')) {
- $flash = ['type' => 'success', 'text' => $enable ? 'Automatic updates enabled.' : 'Automatic updates disabled.'];
- } else {
- $flash = ['type' => 'error', 'text' => 'Save failed.'];
- }
- } elseif ($action === 'update_run') {
- pmUpdateRun();
- header('Location: updater.php?updated=1');
- exit;
- }
- } catch (PmUpdateException $e) {
- $flash = ['type' => 'error', 'text' => $e->getMessage()];
- }
- }
- }
-
- if (isset($_GET['updated']) && $flash === null) {
- $flash = ['type' => 'success', 'text' => 'Update installed successfully.'];
- }
- $currentVersion = pmUpdateCurrent();
- $lastUpdate = pmUpdateLastReport();
- $autoUpdate = env('AUTO_UPDATE', 'false') === 'true';
-
- $siteName = env('SITE_NAME', 'PriviMetrics');
- $theme = env('DEFAULT_THEME', 'dark');
- ?>
- <!DOCTYPE html>
- <html lang="en" data-theme="<?= sanitize($theme) ?>">
-
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Updates - <?= sanitize($siteName) ?></title>
- <?= fontFaceStyleTag() ?>
- <script>
- (function() {
- var t = localStorage.getItem('pm_theme') || '<?= sanitize($theme) ?>';
- document.documentElement.setAttribute('data-theme', t);
-
- if (sessionStorage.getItem('pm_intro_played')) {
- document.documentElement.setAttribute('data-no-intro', '1');
- } else {
- sessionStorage.setItem('pm_intro_played', '1');
- }
- })();
- </script>
- <style>
- :root {
- --font-body: <?= fontFamilyCss('body') ?>;
- --font-heading: <?= fontFamilyCss('heading') ?>;
- --font-value: <?= fontFamilyCss('value') ?>;
- }
-
- :root[data-theme="dark"] {
- --bg-primary: #0a0a0a;
- --bg-secondary: #141414;
- --bg-tertiary: #1c1c1c;
- --bg-hover: #222222;
- --border-color: #262626;
- --text-primary: #eaeaea;
- --text-secondary: #9a9a9a;
- --text-tertiary: #6b6b6b;
- --accent: #f1484e;
- --accent-hover: #d53b40;
- --accent-soft: rgba(241, 72, 78, 0.12);
- --success: #22c55e;
- --success-soft: rgba(34, 197, 94, 0.12);
- }
-
- :root[data-theme="light"] {
- --bg-primary: #f7f7f8;
- --bg-secondary: #ffffff;
- --bg-tertiary: #f2f2f3;
- --bg-hover: #ececed;
- --border-color: #e5e5e7;
- --text-primary: #16171a;
- --text-secondary: #6b6d73;
- --text-tertiary: #9799a0;
- --accent: #f1484e;
- --accent-hover: #d53b40;
- --accent-soft: rgba(241, 72, 78, 0.08);
- --success: #16a34a;
- --success-soft: rgba(22, 163, 74, 0.1);
- }
-
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
-
- html {
- overflow-y: scroll;
- scrollbar-gutter: stable;
- }
-
- body {
- font-family: var(--font-body);
- background: var(--bg-primary);
- color: var(--text-primary);
- font-size: 14px;
- line-height: 1.5;
- }
-
- h1 {
- font-family: var(--font-heading);
- }
-
- a {
- color: inherit;
- text-decoration: none;
- }
-
- .app {
- display: flex;
- min-height: 100vh;
- }
-
- .sidebar {
- width: 250px;
- flex-shrink: 0;
- background: linear-gradient(180deg, color-mix(in srgb, var(--bg-secondary) 94%, #fff) 0%, var(--bg-secondary) 100%);
- border: 1px solid var(--border-color);
- border-radius: 16px;
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 1px 2px rgba(0, 0, 0, 0.2), 0 12px 28px -14px rgba(0, 0, 0, 0.35);
- display: flex;
- flex-direction: column;
- padding: 18px 14px;
- position: sticky;
- top: 14px;
- align-self: flex-start;
- margin: 14px 0 14px 14px;
- height: calc(100vh - 28px);
- overflow-y: auto;
- scrollbar-width: none;
- -ms-overflow-style: none;
- z-index: 10;
- }
-
- .sidebar::-webkit-scrollbar {
- display: none;
- }
-
- .brand {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 6px 8px 18px;
- margin-bottom: 12px;
- border-bottom: 1px solid var(--border-color);
- }
-
- .brand .mark {
- width: 34px;
- height: 34px;
- border-radius: 9px;
- background: var(--accent);
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 700;
- color: white;
- font-size: 14px;
- flex-shrink: 0;
- }
-
- .brand .name {
- font-family: var(--font-heading);
- font-weight: 600;
- font-size: 15px;
- }
-
- .brand .sub {
- font-size: 11px;
- color: var(--text-tertiary);
- }
-
- .nav {
- display: flex;
- flex-direction: column;
- gap: 2px;
- }
-
- .nav a {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 9px 12px;
- border-radius: 10px;
- font-size: 13.5px;
- font-weight: 500;
- color: var(--text-secondary);
- border: 1px solid transparent;
- transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
- }
-
- .nav a svg {
- width: 17px;
- height: 17px;
- flex-shrink: 0;
- }
-
- .nav a:hover {
- background: var(--bg-tertiary);
- color: var(--text-primary);
- transform: translateX(2px);
- }
-
- .nav a.active {
- background: var(--accent-soft);
- color: var(--accent);
- font-weight: 600;
- border-color: color-mix(in srgb, var(--accent) 22%, transparent);
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 1px 2px rgba(0, 0, 0, 0.18);
- transform: none;
- }
-
- .sidebar-footer {
- margin-top: auto;
- padding-top: 14px;
- border-top: 1px solid var(--border-color);
- }
-
- .sidebar-footer a {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 9px 12px;
- border-radius: 8px;
- font-size: 13px;
- color: var(--text-secondary);
- }
-
- .sidebar-footer a:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- }
-
- .sidebar-footer .user {
- font-size: 12px;
- color: var(--text-tertiary);
- padding: 6px 12px 10px;
- }
-
- .main {
- flex: 1;
- min-width: 0;
- }
-
- .topbar {
- display: flex;
- align-items: center;
- gap: 14px;
- padding: 16px 28px;
- margin: 14px 14px 0;
- border: 1px solid var(--border-color);
- border-radius: 16px;
- background: linear-gradient(180deg, color-mix(in srgb, var(--bg-secondary) 94%, #fff) 0%, var(--bg-secondary) 100%);
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 1px 2px rgba(0, 0, 0, 0.18), 0 12px 28px -14px rgba(0, 0, 0, 0.35);
- min-height: 68px;
- box-sizing: border-box;
- }
-
- .topbar h1 {
- font-size: 17px;
- font-weight: 600;
- }
-
- .mobile-topbar {
- display: none;
- }
-
- .mobile-menu-btn {
- width: 38px;
- height: 38px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--bg-tertiary);
- border: 1px solid var(--border-color);
- border-radius: 8px;
- }
-
- @media (max-width: 880px) {
- .app {
- flex-direction: column;
- }
-
- .sidebar {
- position: fixed;
- top: auto;
- bottom: calc(8px + env(safe-area-inset-bottom));
- left: calc(8px + env(safe-area-inset-left));
- right: calc(8px + env(safe-area-inset-right));
- width: auto;
- height: auto;
- margin: 0;
- flex-direction: row;
- align-items: center;
- padding: 6px;
- border-radius: 24px;
- border-color: var(--border-color);
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.04);
- overflow: visible;
- z-index: 60;
- }
-
- .sidebar .brand,
- .sidebar-footer .user {
- display: none;
- }
-
- .nav {
- flex: 3 1 0;
- min-width: 0;
- flex-direction: row;
- justify-content: space-around;
- gap: 0;
- }
-
- .sidebar-footer {
- margin: 0;
- padding: 0;
- border: none;
- flex: 1 1 0;
- min-width: 0;
- display: flex;
- }
-
- .nav a,
- .sidebar-footer a {
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 3px;
- padding: 8px 4px;
- font-size: 10px;
- font-weight: 600;
- border-radius: 18px;
- text-align: center;
- flex: 1;
- transform: none;
- }
-
- .nav a:hover {
- transform: none;
- }
-
- .nav a svg {
- width: 20px;
- height: 20px;
- }
-
- .sidebar-footer a svg {
- width: 20px;
- height: 20px;
- }
-
- .sidebar-backdrop,
- .sidebar-backdrop.show {
- display: none;
- }
-
- .mobile-menu-btn {
- display: none;
- }
-
- .main {
- background: var(--bg-secondary);
- overflow-x: clip;
- }
-
- .mobile-topbar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 16px;
- background: var(--bg-primary);
- position: sticky;
- top: 0;
- z-index: 30;
- }
-
- .mobile-topbar::after {
- content: "";
- position: absolute;
- left: 0;
- right: 0;
- top: calc(100% - 1px);
- height: 24px;
- pointer-events: none;
- background:
- radial-gradient(circle at 100% 100%, transparent 23.5px, var(--bg-primary) 24.5px) left top / 24px 24px no-repeat,
- radial-gradient(circle at 0 100%, transparent 23.5px, var(--bg-primary) 24.5px) right top / 24px 24px no-repeat;
- }
-
- .content {
- padding: 18px 16px 110px;
- }
-
- .topbar {
- padding: 14px 16px;
- margin: 12px 12px 0;
- }
- }
-
- .content {
- max-width: 720px;
- margin: 0 auto;
- padding: 28px 20px 60px;
- }
-
- .panel {
- position: relative;
- background: linear-gradient(180deg, color-mix(in srgb, var(--bg-secondary) 94%, #fff) 0%, var(--bg-secondary) 100%);
- border: 1px solid var(--border-color);
- border-radius: 16px;
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 1px 2px rgba(0, 0, 0, 0.18), 0 12px 28px -14px rgba(0, 0, 0, 0.35);
- padding: 24px;
- margin-bottom: 18px;
- opacity: 0;
- transform: translateY(10px);
- animation: panelIn 0.45s ease forwards;
- }
-
- .content .panel:nth-of-type(2) {
- animation-delay: 0.05s;
- }
-
- .content .panel:nth-of-type(3) {
- animation-delay: 0.1s;
- }
-
- .content .panel:nth-of-type(4) {
- animation-delay: 0.15s;
- }
-
- .content .panel:nth-of-type(5) {
- animation-delay: 0.2s;
- }
-
- .content .panel:nth-of-type(6) {
- animation-delay: 0.25s;
- }
-
- @keyframes panelIn {
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- @media (prefers-reduced-motion: reduce) {
- .panel {
- animation: none;
- opacity: 1;
- transform: none;
- }
- }
-
- :root[data-no-intro] .panel {
- animation: none;
- opacity: 1;
- transform: none;
- }
-
- .panel h2 {
- font-size: 15px;
- font-weight: 600;
- margin-bottom: 4px;
- }
-
- .panel .desc {
- font-size: 12.5px;
- color: var(--text-secondary);
- margin-bottom: 18px;
- }
-
- .form-group {
- margin-bottom: 16px;
- }
-
- .form-group label {
- display: block;
- font-size: 12.5px;
- font-weight: 500;
- color: var(--text-secondary);
- margin-bottom: 6px;
- }
-
- .form-group input,
- .form-group select {
- width: 100%;
- padding: 10px 12px;
- background: var(--bg-primary);
- border: 1px solid var(--border-color);
- border-radius: 10px;
- color: var(--text-primary);
- font-size: 13.5px;
- font-family: inherit;
- box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.25);
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
- }
-
- .form-group input:focus,
- .form-group select:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
- }
-
- .row {
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
- gap: 12px;
- }
-
- @media (max-width: 560px) {
- .row {
- grid-template-columns: minmax(0, 1fr);
- }
- }
-
- .checkbox-row {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 16px;
- }
-
- .checkbox-row label {
- margin: 0;
- font-size: 13px;
- color: var(--text-primary);
- font-weight: 500;
- cursor: pointer;
- }
-
- .switch {
- position: relative;
- display: inline-block;
- width: 42px;
- height: 24px;
- flex-shrink: 0;
- }
-
- .switch input {
- position: absolute;
- opacity: 0;
- width: 100%;
- height: 100%;
- margin: 0;
- cursor: pointer;
- z-index: 1;
- }
-
- .switch .track {
- position: absolute;
- inset: 0;
- background: var(--bg-tertiary);
- border: 1px solid var(--border-color);
- border-radius: 20px;
- box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3);
- transition: background 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
- }
-
- .switch .track::after {
- content: '';
- position: absolute;
- top: 2px;
- left: 2px;
- width: 18px;
- height: 18px;
- background: white;
- border-radius: 50%;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
- transition: transform 0.25s cubic-bezier(.22, 1, .36, 1);
- }
-
- .switch input:checked+.track {
- background: linear-gradient(155deg, var(--accent-hover), var(--accent));
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 0 12px color-mix(in srgb, var(--accent) 35%, transparent);
- border-color: var(--accent);
- }
-
- .switch input:checked+.track::after {
- transform: translateX(18px);
- }
-
- .switch input:focus-visible+.track {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
- }
-
- .btn {
- padding: 9px 16px;
- border-radius: 10px;
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
- border: 1px solid color-mix(in srgb, var(--border-color) 60%, var(--text-tertiary));
- background: var(--bg-tertiary);
- color: var(--text-primary);
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 1px 2px rgba(0, 0, 0, 0.18);
- transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
- }
-
- .btn:hover {
- background: var(--bg-hover);
- }
-
- .btn:active {
- transform: scale(0.97);
- }
-
- .btn.primary {
- background: linear-gradient(155deg, var(--accent-hover), var(--accent));
- border-color: var(--accent);
- color: white;
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 1px 2px rgba(0, 0, 0, 0.2);
- }
-
- .btn.primary:hover {
- background: linear-gradient(155deg, var(--accent-hover), var(--accent-hover));
- box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 40%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.14);
- transform: translateY(-1px);
- }
-
- .btn.primary:active {
- transform: translateY(0) scale(0.98);
- }
-
- .flash {
- padding: 12px 16px;
- border-radius: 10px;
- font-size: 13px;
- margin-bottom: 18px;
- }
-
- .flash.success {
- background: var(--success-soft);
- color: var(--success);
- border: 1px solid var(--success);
- }
-
- .flash.error {
- background: var(--accent-soft);
- color: var(--accent);
- border: 1px solid var(--accent);
- }
-
- .hint {
- font-size: 12px;
- color: var(--text-tertiary);
- margin-top: -8px;
- margin-bottom: 16px;
- line-height: 1.5;
- }
-
- .field-warning {
- display: none;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- color: #b45309;
- background: rgba(180, 83, 9, 0.1);
- border: 1px solid rgba(180, 83, 9, 0.35);
- border-radius: 6px;
- padding: 6px 10px;
- margin-top: 6px;
- line-height: 1.4;
- }
-
- .field-warning.show {
- display: flex;
- }
-
- .field-warning svg {
- flex-shrink: 0;
- }
-
- #external-fields {
- display: <?= env('GEO_PROVIDER', 'privacy-friendly') === 'external' ? 'block' : 'none' ?>;
- }
-
- #mysql-cred-fields {
- display: <?= env('DB_ENABLED', 'false') === 'true' ? 'block' : 'none' ?>;
- }
- .upd-h { font-size: 13px; margin: 16px 0 8px; font-family: var(--font-heading); }
- .upd-list { margin: 0 0 8px 18px; padding: 0; font-size: 13px; }
- .upd-files { list-style: none; margin: 0 0 8px; padding: 0; font-size: 13px; }
- .upd-files li { padding: 7px 0; border-bottom: 1px solid var(--border-color); }
- .upd-files small { display: block; margin-top: 3px; opacity: .7; font-size: 12px; }
- .upd-files code { word-break: break-all; }
- .upd-files .tag { display: inline-block; min-width: 62px; text-align: center; font-size: 11px; padding: 1px 7px; border-radius: 10px; margin-right: 6px; }
- .upd-files .tag.mod { background: var(--accent-soft); color: var(--accent); }
- .upd-files .tag.add { background: var(--success-soft); color: var(--success); }
- </style>
- </head>
-
- <body>
- <div class="app">
-
- <div class="sidebar-backdrop" id="backdrop" onclick="closeSidebar()"></div>
- <aside class="sidebar" id="sidebar">
- <div class="brand">
- <div class="mark">
- <img src="logo.webp" alt="Logo" width="24" height="24" loading="lazy">
- </div>
- <div>
- <div class="name"><?= sanitize($siteName) ?></div>
- <div class="sub">PriviMetrics 2</div>
- </div>
- </div>
- <nav class="nav">
- <a href="dashboard.php?view=overview">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <path d="M7 12v7M12 4v15M17 8v11"></path>
- </svg>
- Analytics
- </a>
- <a href="dashboard.php?view=sites">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"></path>
- <path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"></path>
- </svg>
- Sites
- </a>
- <a href="settings.php" class="active">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <circle cx="12" cy="12" r="3" />
- <path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" />
- </svg>
- Settings
- </a>
- </nav>
- <div class="sidebar-footer">
- <div class="user">Signed in as <strong><?= sanitize($_SESSION['admin_username'] ?? '') ?></strong></div>
- <a href="logout.php">
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4" />
- <path d="M16 17l5-5-5-5" />
- <path d="M21 12H9" />
- </svg>
- Sign out
- </a>
- </div>
- </aside>
-
- <div class="main">
- <div class="mobile-topbar">
- <div class="mobile-menu-btn" onclick="openSidebar()">
- <svg width="24" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <line x1="3" y1="12" x2="21" y2="12" />
- <line x1="3" y1="6" x2="21" y2="6" />
- <line x1="3" y1="18" x2="21" y2="18" />
- </svg>
- </div>
- <strong style="font-size:14px;"><?= sanitize($siteName) ?></strong>
- <div style="width:38px;"></div>
- </div>
-
- <div class="topbar">
- <h1>Updates</h1>
- <button type="button" class="btn" onclick="toggleTheme()" style="margin-left:auto;width:36px;height:36px;padding:0;display:flex;align-items:center;justify-content:center;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;cursor:pointer;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <circle cx="12" cy="12" r="5" />
- <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
- </svg></button>
- </div>
-
- <div class="content">
- <?php if ($flash): ?><div class="flash <?= $flash['type'] ?>"><?= sanitize($flash['text']) ?></div><?php endif; ?>
-
- <div class="panel">
- <h2>Updates</h2>
- <p class="desc">Installed version <strong><?= sanitize($currentVersion['version'] ?? '?') ?></strong><?= !empty($currentVersion['stage']) ? ' (' . sanitize($currentVersion['stage']) . ')' : '' ?>. The update server is contacted only when you press a button. Before installing, all program files are backed up to data/backups/. Your data and settings are never touched.</p>
-
- <?php if ($updateInfo): $um = $updateInfo['manifest']; $up = $updateInfo['plan'] ?? null; ?>
- <div class="flash success" style="margin-bottom:14px;">
- <?php if ($um['is_newer']): ?>
- New version <strong><?= sanitize($um['version']) ?></strong> is available (<?= sanitize($um['released_at'] ?? '') ?>, <?= sanitize(pmUpdateSize((int) ($um['size'] ?? 0))) ?>).
- <?php else: ?>
- You are on the latest version (<?= sanitize($um['version']) ?>).
- <?php endif; ?>
- </div>
- <?php if ($um['changelog']): ?>
- <h3 class="upd-h">What's new</h3>
- <ul class="upd-list"><?php foreach ($um['changelog'] as $line): ?><li><?= sanitize($line) ?></li><?php endforeach; ?></ul>
- <?php endif; ?>
- <?php if ($up): ?>
- <h3 class="upd-h">Files that would change</h3>
- <?php if (!$up['added'] && !$up['modified']): ?>
- <p class="hint">No files differ from the release.</p>
- <?php else: ?>
- <ul class="upd-files">
- <?php foreach ($up['modified'] as $f): ?>
- <li><span class="tag mod">modified</span> <code><?= sanitize($f['path']) ?></code><small><?= sanitize(pmUpdateSummarize($f)) ?></small></li>
- <?php endforeach; ?>
- <?php foreach ($up['added'] as $f): ?>
- <li><span class="tag add">new</span> <code><?= sanitize($f['path']) ?></code><small><?= sanitize(pmUpdateSummarize($f)) ?></small></li>
- <?php endforeach; ?>
- </ul>
- <?php endif; ?>
- <p class="hint"><?= (int) $up['unchanged'] ?> files unchanged<?= $up['not_in_release'] ? ', ' . count($up['not_in_release']) . ' local files not in the release (kept)' : '' ?>.</p>
- <?php endif; ?>
- <?php endif; ?>
-
- <form method="post" style="display:flex;gap:10px;flex-wrap:wrap;margin-top:14px;">
- <input type="hidden" name="csrf_token" value="<?= sanitize($csrfToken) ?>">
- <button type="submit" name="action" value="update_check" class="btn">Check for updates</button>
- <?php if ($updateInfo && $updateInfo['manifest']['is_newer']): ?>
- <button type="submit" name="action" value="update_preview" class="btn">Preview changes</button>
- <button type="submit" name="action" value="update_run" class="btn primary" onclick="return confirm('Create a backup and install version <?= sanitize($updateInfo['manifest']['version']) ?>?');">Backup & install</button>
- <?php endif; ?>
- </form>
-
- <?php if ($lastUpdate): ?>
- <h3 class="upd-h" style="margin-top:22px;">Last update: <?= sanitize($lastUpdate['from'] ?? '?') ?> → <?= sanitize($lastUpdate['to'] ?? '?') ?> <small style="font-weight:400;opacity:.7;"><?= sanitize(substr((string) ($lastUpdate['at'] ?? ''), 0, 10)) ?><?= !empty($lastUpdate['automatic']) ? ' · installed automatically' : '' ?></small></h3>
- <?php if (!empty($lastUpdate['changelog'])): ?>
- <ul class="upd-list"><?php foreach ($lastUpdate['changelog'] as $line): ?><li><?= sanitize($line) ?></li><?php endforeach; ?></ul>
- <?php endif; ?>
- <ul class="upd-files">
- <?php foreach (($lastUpdate['modified'] ?? []) as $f): ?>
- <li><span class="tag mod">modified</span> <code><?= sanitize($f['path']) ?></code><small><?= sanitize(pmUpdateSummarize($f)) ?></small></li>
- <?php endforeach; ?>
- <?php foreach (($lastUpdate['added'] ?? []) as $f): ?>
- <li><span class="tag add">new</span> <code><?= sanitize($f['path']) ?></code><small><?= sanitize(pmUpdateSummarize($f)) ?></small></li>
- <?php endforeach; ?>
- </ul>
- <p class="hint">Backup of the previous version: <code><?= sanitize($lastUpdate['backup'] ?? '') ?></code></p>
- <?php endif; ?>
- </div>
-
- <div class="panel">
- <h2>Automatic updates</h2>
- <p class="desc">When enabled, a cron job installs new versions on its own, without asking anyone. It is off by default.</p>
- <div class="field-warning show" style="display:block;margin:0 0 16px;">
- <strong>Warning:</strong> automatic updates can be dangerous for your site. New versions are installed without review, so changed or removed functions, modified files and any custom edits to program files can be overwritten without anyone being informed. A faulty release can break the site until someone notices. A backup is still created before every update in data/backups/.
- </div>
- <form method="post" id="auto-update-form">
- <input type="hidden" name="csrf_token" value="<?= sanitize($csrfToken) ?>">
- <input type="hidden" name="action" value="auto_update">
- <div class="checkbox-row">
- <span class="switch">
- <input type="checkbox" name="auto_update" id="auto_update" <?= $autoUpdate ? 'checked' : '' ?>>
- <span class="track"></span>
- </span>
- <label for="auto_update">Install updates automatically (via cron)</label>
- </div>
- <button type="submit" class="btn">Save</button>
- </form>
- <p class="hint" style="margin-top:14px;">Add this command to your cron jobs (e.g. once a day). It only works from the command line and does nothing while this option is off:</p>
- <p class="hint"><code>php <?= sanitize(__DIR__ . '/cron-update.php') ?></code></p>
- </div>
- </div>
- </div>
- </div>
- <script>
- function setTheme(t) {
- document.documentElement.setAttribute('data-theme', t);
- localStorage.setItem('pm_theme', t);
- }
-
- function toggleTheme() {
- var n = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
- setTheme(n);
- }
-
- document.getElementById('auto-update-form').addEventListener('submit', function(e) {
- var on = document.getElementById('auto_update').checked;
- if (on && <?= $autoUpdate ? 'false' : 'true' ?> && !confirm('Enable automatic updates?\n\nNew versions will be installed without your review. Functions and files may be changed or overwritten without you being informed, and a faulty release could break the site.')) {
- e.preventDefault();
- }
- });
-
- function openSidebar() {
- document.getElementById('sidebar').classList.add('open');
- document.getElementById('backdrop').classList.add('show');
- }
-
- function closeSidebar() {
- document.getElementById('sidebar').classList.remove('open');
- document.getElementById('backdrop').classList.remove('show');
- }
- </script>
- </body>
-
- </html>
-