BROOKO icon
BROOKO UK NETWORK
Where code meets creativity & adventure
File viewer

index.php

Type
php
Size
18.01 KB
Modified
15 May
index.php 18.01 KB
<?php
require_once __DIR__ . '/../bootstrap.php';
require_once __DIR__ . '/../lib/feature_modules.php';
requireAdmin();

$pageTitle = 'Administration';

// Helper: list update logs for Release Notes

function wp_list_update_logs(): array {
    $dir = realpath(__DIR__ . '/../assets/changelog');
    if (!$dir || !is_dir($dir)) return [];

    $logs = [];
    $archive = $dir . '/updates.zip';

    // Preferred: single archive file
    if (is_readable($archive) && class_exists('ZipArchive')) {
        $zip = new ZipArchive();
        if ($zip->open($archive) === true) {
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $stat = $zip->statIndex($i);
                if (!$stat || empty($stat['name'])) continue;
                $name = (string)$stat['name'];
                if (!preg_match('/^update_log_(.+)\.md$/', basename($name), $m)) continue;
                $ver = trim((string)$m[1]);
                $content = $zip->getFromIndex($i);
                $isLegacy = false;
                if (is_string($content) && strpos($content, 'Legacy update log') !== false && strpos($content, 'Not recorded') !== false) {
                    $isLegacy = true;
                }
                $logs[] = [
                    'version' => $ver,
                    'path'    => $archive . '::' . $name,
                    'mtime'   => (int)($stat['mtime'] ?? 0),
                    'legacy'  => $isLegacy,
                ];
            }
            $zip->close();
        }
    }

    // Fallback: legacy loose files
    if (empty($logs)) {
        foreach (glob($dir . '/update_log_*.md') ?: [] as $path) {
            $base = basename($path);
            if (!preg_match('/^update_log_(.+)\.md$/', $base, $m)) continue;
            $ver = trim((string)$m[1]);
            $content = @file_get_contents($path);
            $isLegacy = false;
            if (is_string($content) && strpos($content, 'Legacy update log') !== false && strpos($content, 'Not recorded') !== false) {
                $isLegacy = true;
            }
            $logs[] = [
                'version' => $ver,
                'path' => $path,
                'mtime' => @filemtime($path) ?: 0,
                'legacy' => $isLegacy,
            ];
        }
    }

    usort($logs, function ($a, $b) {
        $va = preg_replace('/-.*/', '', (string)$a['version']);
        $vb = preg_replace('/-.*/', '', (string)$b['version']);
        $cmp = version_compare($vb, $va);
        if ($cmp !== 0) return $cmp;
        $sa = (string)$a['version'];
        $sb = (string)$b['version'];
        $alphaA = str_contains($sa, '-alpha');
        $alphaB = str_contains($sb, '-alpha');
        if ($alphaA !== $alphaB) return $alphaB <=> $alphaA;
        return ($b['mtime'] ?? 0) <=> ($a['mtime'] ?? 0);
    });

    return $logs;
}

$updateLogs = wp_list_update_logs();
$latestLogVersion = $updateLogs[0]['version'] ?? '';

// Quick stats (best-effort)
$staffCount = 0;
$eventCount = 0;
$vanCount   = 0;
$dbInfo = '';
$appVersion = defined('APP_VERSION') ? APP_VERSION : '';
$ttConfigured = false;
$ttDevices = 0;
$ttLastUpdate = null;

try { $staffCount = (int)$pdo->query("SELECT COUNT(*) FROM users WHERE is_active = 1")->fetchColumn(); } catch (Throwable $e) {}
try { $vanCount   = (int)$pdo->query("SELECT COUNT(*) FROM vans WHERE is_active = 1")->fetchColumn(); } catch (Throwable $e) {}

// DB info + TruTrak config/cache (best-effort)
try {
    $pairs = $pdo->query("SELECT `key`,`value` FROM system_info")->fetchAll(PDO::FETCH_KEY_PAIR);
    $ttConfigured = !empty($pairs['trutrak_base_url'] ?? '')
        && !empty($pairs['trutrak_login'] ?? '')
        && !empty($pairs['trutrak_password'] ?? '')
        && !empty($pairs['trutrak_secret_key'] ?? '');
} catch (Throwable $e) {}

try {
    $ver = (string)$pdo->query('SELECT VERSION()')->fetchColumn();
    $comment = '';
    try { $comment = (string)$pdo->query('SELECT @@version_comment')->fetchColumn(); } catch (Throwable $e) {}
    $hint = strtolower(trim($ver . ' ' . $comment));
    $product = (str_contains($hint, 'mariadb') ? 'MariaDB' : 'MySQL');
    $dbInfo = trim($product . ' ' . $ver);
} catch (Throwable $e) {}

try {
    $ttDevices = (int)$pdo->query("SELECT COUNT(*) FROM trutrak_device_cache")->fetchColumn();
    $ttLastUpdate = $pdo->query("SELECT MAX(updated_at) FROM trutrak_device_cache")->fetchColumn();
} catch (Throwable $e) {}

// Calendar schema varies across alpha builds – try the modern schema first, then fall back.
try {
    $eventCount = (int)$pdo->query("SELECT COUNT(*) FROM calendar_events WHERE start_at >= NOW()")->fetchColumn();
} catch (Throwable $e) {
    try { $eventCount = (int)$pdo->query("SELECT COUNT(*) FROM calendar_events WHERE event_date >= CURDATE()")->fetchColumn(); } catch (Throwable $e2) {}
}

$featureMetrics = function_exists('wp_feature_dashboard_metrics') ? wp_feature_dashboard_metrics() : [
    'rota_today' => 0,
    'holiday_pending' => 0,
    'holiday_mine' => 0,
    'incidents_open' => 0,
    'contacts_total' => 0,
    'alerts_active' => 0,
    'alerts_unread' => 0,
];

$featureHubCards = [
    ['icon' => 'πŸ‘₯', 'title' => 'Staff & Permissions', 'count' => $staffCount, 'label' => 'users and role access', 'link' => app_url('admin/staff')],
    ['icon' => 'πŸ“…', 'title' => 'Calendar Admin', 'count' => $eventCount, 'label' => 'settings and categories', 'link' => app_url('admin/calendar')],
    ['icon' => 'πŸ“', 'title' => 'TruTrak Admin', 'count' => $ttDevices, 'label' => $ttConfigured ? 'connector and mapping' : 'connector not set', 'link' => app_url('admin/trutrak')],
    ['icon' => 'πŸ“€', 'title' => 'Exports', 'count' => null, 'label' => 'csv downloads', 'link' => app_url('admin/exports')],
    ['icon' => '☁️', 'title' => 'Integrations', 'count' => null, 'label' => wp_onedrive_is_configured() ? 'OneDrive ready' : 'OneDrive setup', 'link' => app_url('admin/integrations')],
    ['icon' => '🧰', 'title' => 'Maintenance', 'count' => null, 'label' => 'branding and updater', 'link' => app_url('admin/maintenance')],
    ['icon' => 'πŸ“‹', 'title' => 'Logs', 'count' => null, 'label' => 'activity audit', 'link' => app_url('admin/logs')],
];

$extraHead = '<style>
.feature-hub-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;}
.feature-hub-card{display:flex;flex-direction:column;gap:14px;height:100%;padding:18px;border:1px solid var(--border);border-radius:16px;background:rgba(255,255,255,0.02);}
.feature-hub-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;}
.feature-hub-icon{width:48px;height:48px;border-radius:14px;display:flex;align-items:center;justify-content:center;background:rgba(255,140,26,.12);font-size:1.4rem;}
.feature-hub-title{margin:0 0 6px 0;font-size:1rem;font-weight:800;}
.feature-hub-meta{color:var(--text-muted);font-size:.92rem;}
.feature-hub-count{font-size:1.55rem;font-weight:900;line-height:1;color:var(--text-main);}
@media (max-width:1100px){.feature-hub-grid{grid-template-columns:repeat(2,minmax(0,1fr));}}
@media (max-width:640px){.feature-hub-grid{grid-template-columns:1fr;}}
</style>';

include __DIR__ . '/../partials/header.php';
?>

<div class="content-header">
    <h1 class="content-title">βš™οΈ Administration</h1>
    <p class="content-subtitle">System setup, integrations, permissions, exports and maintenance</p>
</div>

<div class="grid-3" style="margin-top:16px;">
    <div class="card">
        <div class="d-flex justify-between align-center">
            <div>
                <p class="text-muted" style="margin:0;font-size:.9rem;">App Version</p>
                <h2 style="margin:8px 0 0 0;font-size:1.6rem;font-weight:900;line-height:1.1;"><?= htmlspecialchars($appVersion ?: 'β€”') ?></h2>
            </div>
            <div style="font-size:2.2rem;opacity:.6;">πŸ“¦</div>
        </div>
    </div>
    <div class="card">
        <div class="d-flex justify-between align-center">
            <div>
                <p class="text-muted" style="margin:0;font-size:.9rem;">Database</p>
                <h2 style="margin:8px 0 0 0;font-size:1.35rem;font-weight:900;line-height:1.1;"><?= htmlspecialchars($dbInfo ?: 'β€”') ?></h2>
            </div>
            <div style="font-size:2.2rem;opacity:.6;">πŸ—„οΈ</div>
        </div>
    </div>
    <div class="card">
        <div class="d-flex justify-between align-center">
            <div>
                <p class="text-muted" style="margin:0;font-size:.9rem;">TruTrak Cache</p>
                <h2 style="margin:8px 0 0 0;font-size:1.6rem;font-weight:900;line-height:1.1;"><?= (int)$ttDevices ?> devices</h2>
                <div class="text-muted" style="margin-top:6px;font-size:.9rem;">
                    <?= $ttConfigured ? 'Configured' : 'Not configured' ?>
                    <?= $ttLastUpdate ? 'β€’ Updated ' . htmlspecialchars($ttLastUpdate) : '' ?>
                </div>
            </div>
            <div style="font-size:2.2rem;opacity:.6;">πŸ“</div>
        </div>
    </div>
</div>


<div class="card" style="margin-top:16px;">
    <div class="card-header">
        <div>
            <h3 class="card-title">Administration Tools</h3>
            <p class="text-muted" style="margin:6px 0 0 0;">System setup only. Day-to-day rota, clock records, holidays, incidents, contacts, alerts and vehicles stay under Management.</p>
        </div>
    </div>
    <div class="feature-hub-grid">
        <?php foreach ($featureHubCards as $card): ?>
            <div class="feature-hub-card">
                <div class="feature-hub-head">
                    <div>
                        <h4 class="feature-hub-title"><?= e($card['title']) ?></h4>
                        <?php if ($card['count'] !== null): ?>
                            <div class="feature-hub-count"><?= (int)$card['count'] ?></div>
                        <?php endif; ?>
                        <div class="feature-hub-meta"><?= e((string)$card['label']) ?></div>
                    </div>
                    <div class="feature-hub-icon"><?= $card['icon'] ?></div>
                </div>
                <div style="margin-top:auto;">
                    <a href="<?= e($card['link']) ?>" class="btn btn-secondary">Open</a>
                </div>
            </div>
        <?php endforeach; ?>
    </div>
</div>

<div class="card" style="margin-top:24px;">
    <div class="card-header">
        <h3 class="card-title">πŸ“ Release Notes</h3>
        <p class="text-muted" style="margin:6px 0 0 0;">View what changed in each update (stored in a single <code>assets/changelog/updates.zip</code> archive). Legacy versions without recorded notes are labelled.</p>
    </div>
    <div class="card-body">
        <?php if (!empty($updateLogs)): ?>
            <div class="grid-2" style="gap:12px;align-items:end;">
                <div>
                    <label class="text-muted" style="display:block;font-size:.85rem;margin-bottom:6px;">Latest</label>
                    <button type="button" class="btn btn-secondary btn-block" id="btnViewLatest">πŸ“ View Latest (<?= htmlspecialchars($latestLogVersion ?: 'β€”') ?>)</button>
                </div>
                <div>
                    <label class="text-muted" style="display:block;font-size:.85rem;margin-bottom:6px;">History</label>
                    <select id="updateSelect" style="width:100%;">
                        <?php
                            $withNotes = array_values(array_filter($updateLogs, fn($x) => empty($x['legacy'])));
                            $legacy    = array_values(array_filter($updateLogs, fn($x) => !empty($x['legacy'])));
                        ?>
                        <?php if (!empty($withNotes)): ?>
                            <optgroup label="Release notes available">
                                <?php foreach ($withNotes as $l): ?>
                                    <option value="<?= htmlspecialchars($l['version']) ?>" <?= ($l['version'] === $latestLogVersion) ? 'selected' : '' ?>><?= htmlspecialchars($l['version']) ?></option>
                                <?php endforeach; ?>
                            </optgroup>
                        <?php endif; ?>
                        <?php if (!empty($legacy)): ?>
                            <optgroup label="Legacy (no notes recorded)">
                                <?php foreach ($legacy as $l): ?>
                                    <option value="<?= htmlspecialchars($l['version']) ?>"><?= htmlspecialchars($l['version']) ?> (legacy)</option>
                                <?php endforeach; ?>
                            </optgroup>
                        <?php endif; ?>
                    </select>
                    <div style="margin-top:10px;">
                        <button type="button" class="btn btn-secondary btn-block" id="btnViewSelected">πŸ•’ View Selected</button>
                    </div>
                </div>
            </div>
        <?php else: ?>
            <div class="text-muted">No update logs found yet in <code>assets/changelog</code>.</div>
        <?php endif; ?>
    </div>
</div>

<!-- Release Notes Modal -->
<div id="changelogModal" class="modal-overlay" style="display:none;">
    <div class="modal" role="dialog" aria-modal="true" aria-labelledby="changelogTitle">
        <div class="modal-header">
            <h3 class="modal-title" id="changelogTitle">Update Log</h3>
            <button class="modal-close" type="button" aria-label="Close" onclick="closeChangelog()">Γ—</button>
        </div>
        <div class="modal-body">
            <div id="changelogLegacyBanner" style="display:none; padding:12px 14px; border:1px solid rgba(255, 183, 3, .35); background: rgba(255, 183, 3, .08); border-radius: 12px; margin-bottom: 12px;">
                <div style="font-weight:700; margin-bottom:4px;">Legacy build β€” no release notes recorded</div>
                <div class="text-muted" style="font-size:.9rem; line-height:1.35;">This version existed historically but detailed release notes were not captured at the time.</div>
                <div style="margin-top:10px;">
                    <button type="button" class="btn btn-secondary" id="btnToggleLegacyLog" style="padding:6px 10px; font-size:.85rem;">Show raw placeholder</button>
                </div>
            </div>
            <pre id="changelogText" style="white-space: pre-wrap; margin:0;">Loading…</pre>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-secondary" onclick="closeChangelog()">Close</button>
        </div>
    </div>
</div>

<script>
let __legacyRawLog = '';

async function loadLog(version){
    const titleEl = document.getElementById('changelogTitle');
    const bannerEl = document.getElementById('changelogLegacyBanner');
    const toggleEl = document.getElementById('btnToggleLegacyLog');
    const textEl  = document.getElementById('changelogText');
    if (!titleEl || !textEl) return;

    // Reset UI
    titleEl.textContent = 'Update Log β€” ' + version;
    textEl.textContent = 'Loading…';
    __legacyRawLog = '';
    if (bannerEl) bannerEl.style.display = 'none';
    if (toggleEl) {
        toggleEl.style.display = 'none';
        toggleEl.dataset.state = 'hidden';
        toggleEl.textContent = 'Show raw placeholder';
    }

    try {
        const res = await fetch('/api/changelog?v=' + encodeURIComponent(version), { credentials: 'same-origin' });
        const data = await res.json();
        if (!(data && data.ok)) {
            textEl.textContent = (data && data.error) ? data.error : 'Unable to load update log.';
            return;
        }

        const isLegacy = !!data.legacy;
        if (isLegacy) {
            titleEl.textContent = 'Update Log β€” ' + version + ' (legacy)';
            if (bannerEl) bannerEl.style.display = 'block';
            __legacyRawLog = data.content || '';
            textEl.textContent = 'No release notes were recorded for this legacy build.';

            if (toggleEl) {
                toggleEl.style.display = 'inline-flex';
                toggleEl.onclick = () => {
                    const shown = toggleEl.dataset.state === 'shown';
                    if (shown) {
                        textEl.textContent = 'No release notes were recorded for this legacy build.';
                        toggleEl.dataset.state = 'hidden';
                        toggleEl.textContent = 'Show raw placeholder';
                    } else {
                        textEl.textContent = __legacyRawLog || '(empty)';
                        toggleEl.dataset.state = 'shown';
                        toggleEl.textContent = 'Hide raw placeholder';
                    }
                };
            }
        } else {
            textEl.textContent = data.content || '(empty)';
        }
    } catch(e) {
        textEl.textContent = 'Unable to load update log.';
    }
}
function openChangelog(){
    const o = document.getElementById('changelogModal');
    if (!o) return;
    o.style.display = 'flex';
    document.body.style.overflow = 'hidden';
}
function closeChangelog(){
    const o = document.getElementById('changelogModal');
    if (!o) return;
    o.style.display = 'none';
    document.body.style.overflow = '';
}

(function(){
    const btnLatest = document.getElementById('btnViewLatest');
    const btnSel    = document.getElementById('btnViewSelected');
    const sel       = document.getElementById('updateSelect');
    const latest    = <?= json_encode($latestLogVersion ?: '') ?>;

    if (btnLatest) {
        btnLatest.addEventListener('click', async () => {
            if (!latest) return;
            await loadLog(latest);
            openChangelog();
        });
    }
    if (btnSel && sel) {
        btnSel.addEventListener('click', async () => {
            const v = sel.value;
            if (!v) return;
            await loadLog(v);
            openChangelog();
        });
    }

    // Close on overlay click + ESC
    const o = document.getElementById('changelogModal');
    if (o) {
        o.addEventListener('click', (e) => { if (e.target === o) closeChangelog(); });
    }
    document.addEventListener('keydown', (e) => {
        if (e.key !== 'Escape') return;
        const cl = document.getElementById('changelogModal');
        if (cl && cl.style.display === 'flex') closeChangelog();
    });
})();
</script>


<?php include __DIR__ . '/../partials/footer.php'; ?>