mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
Updates page: serve cached results instantly, nightly cron refreshes cache
- check-novacpx-update and check-os-update return cached data (12h TTL) immediately instead of running slow git fetch / apt-get update on page load - Cache stored in settings table (update_cache_novacpx, update_cache_os) - Updates page shows "Cached · last checked X ago" when serving cache - "Refresh now" button forces a live re-check and updates cache - bin/cache-update-check.php: standalone cron script that warms cache nightly - Cron registered at 2am daily on panel server Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,17 @@ match ($action) {
|
||||
// ── Check OS updates ─────────────────────────────────────────────────────
|
||||
'check-os-update' => (function() use ($db) {
|
||||
Auth::getInstance()->require('admin');
|
||||
$force = !empty($_GET['force']);
|
||||
$cached = $db->fetchOne("SELECT value, updated_at FROM settings WHERE `key`='update_cache_os'");
|
||||
$age = $cached ? (time() - strtotime($cached['updated_at'])) : PHP_INT_MAX;
|
||||
|
||||
if (!$force && $cached && $age < 43200) {
|
||||
$data = json_decode($cached['value'], true) ?: [];
|
||||
$data['cached'] = true;
|
||||
$data['cached_at'] = $cached['updated_at'];
|
||||
Response::success($data);
|
||||
}
|
||||
|
||||
shell_exec('apt-get update -qq 2>/dev/null');
|
||||
$out = shell_exec('apt-get -s upgrade 2>/dev/null | grep "^Inst " | head -50') ?: '';
|
||||
$packages = array_values(array_filter(array_map(function($line) {
|
||||
@@ -114,12 +125,14 @@ match ($action) {
|
||||
}, explode("\n", trim($out)))));
|
||||
$security = array_filter($packages, fn($p) => str_contains($p['name'] ?? '', 'security') ||
|
||||
(bool)shell_exec("apt-get -s upgrade 2>/dev/null | grep -c \"^Inst {$p['name']}.*security\" 2>/dev/null"));
|
||||
Response::success([
|
||||
$result = [
|
||||
'upgradable' => count($packages),
|
||||
'security_updates' => count($security),
|
||||
'packages' => $packages,
|
||||
'last_checked' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
];
|
||||
$db->execute("INSERT INTO settings(`key`,value,updated_at) VALUES('update_cache_os',?,datetime('now')) ON CONFLICT(`key`) DO UPDATE SET value=excluded.value,updated_at=excluded.updated_at", [json_encode($result)]);
|
||||
Response::success($result);
|
||||
})(),
|
||||
|
||||
// ── Start OS update (background job) ─────────────────────────────────────
|
||||
@@ -194,21 +207,27 @@ BASH;
|
||||
// ── Check NovaCPX update ─────────────────────────────────────────────────
|
||||
'check-novacpx-update' => (function() use ($db) {
|
||||
Auth::getInstance()->require('admin');
|
||||
$force = !empty($_GET['force']);
|
||||
$cached = $db->fetchOne("SELECT value, updated_at FROM settings WHERE `key`='update_cache_novacpx'");
|
||||
$age = $cached ? (time() - strtotime($cached['updated_at'])) : PHP_INT_MAX;
|
||||
|
||||
if (!$force && $cached && $age < 43200) {
|
||||
$data = json_decode($cached['value'], true) ?: [];
|
||||
$data['cached'] = true;
|
||||
$data['cached_at'] = $cached['updated_at'];
|
||||
Response::success($data);
|
||||
}
|
||||
|
||||
$srcDir = '/opt/novacpx-src';
|
||||
if (!is_dir($srcDir)) Response::error('Source repo not found at /opt/novacpx-src');
|
||||
// Use sudo git so www-data can access root-owned repo
|
||||
$fetchOut = shell_exec("sudo git -C " . escapeshellarg($srcDir) . " fetch origin 2>&1");
|
||||
$logOut = shell_exec("sudo git -C " . escapeshellarg($srcDir) . " log HEAD..origin/main --oneline 2>/dev/null") ?: '';
|
||||
$updates = array_values(array_filter(explode("\n", trim($logOut))));
|
||||
$branch = trim(shell_exec("sudo git -C " . escapeshellarg($srcDir) . " branch --show-current 2>/dev/null") ?: 'main');
|
||||
$commit = trim(shell_exec("sudo git -C " . escapeshellarg($srcDir) . " rev-parse --short HEAD 2>/dev/null") ?: '');
|
||||
Response::success([
|
||||
'updates_available' => count($updates),
|
||||
'current_commit' => $commit,
|
||||
'branch' => $branch,
|
||||
'commits' => $updates,
|
||||
'fetch_output' => trim($fetchOut ?: ''),
|
||||
]);
|
||||
shell_exec("sudo git -C " . escapeshellarg($srcDir) . " fetch origin 2>/dev/null");
|
||||
$logOut = shell_exec("sudo git -C " . escapeshellarg($srcDir) . " log HEAD..origin/main --oneline 2>/dev/null") ?: '';
|
||||
$updates = array_values(array_filter(explode("\n", trim($logOut))));
|
||||
$branch = trim(shell_exec("sudo git -C " . escapeshellarg($srcDir) . " branch --show-current 2>/dev/null") ?: 'main');
|
||||
$commit = trim(shell_exec("sudo git -C " . escapeshellarg($srcDir) . " rev-parse --short HEAD 2>/dev/null") ?: '');
|
||||
$result = ['updates_available' => count($updates), 'current_commit' => $commit, 'branch' => $branch, 'commits' => $updates];
|
||||
$db->execute("INSERT INTO settings(`key`,value,updated_at) VALUES('update_cache_novacpx',?,datetime('now')) ON CONFLICT(`key`) DO UPDATE SET value=excluded.value,updated_at=excluded.updated_at", [json_encode($result)]);
|
||||
Response::success($result);
|
||||
})(),
|
||||
|
||||
// ── Apply NovaCPX update ─────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* NovaCPX nightly update cache warmer.
|
||||
* Runs as root via cron — populates update_cache_novacpx and update_cache_os
|
||||
* in the panel settings table so the Updates page loads instantly.
|
||||
*/
|
||||
$cfgFile = '/etc/novacpx/config.ini';
|
||||
$cfg = @parse_ini_file($cfgFile, true) ?: [];
|
||||
$dbPath = $cfg['database']['path'] ?? '/var/lib/novacpx/panel.db';
|
||||
$srcDir = '/opt/novacpx-src';
|
||||
|
||||
try {
|
||||
$pdo = new PDO("sqlite:{$dbPath}", null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
} catch (Exception $e) {
|
||||
fwrite(STDERR, "DB open failed: {$e->getMessage()}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function cache(PDO $pdo, string $key, array $data): void {
|
||||
$json = json_encode($data);
|
||||
$pdo->prepare("INSERT INTO settings(`key`,value,updated_at) VALUES(?,?,datetime('now'))
|
||||
ON CONFLICT(`key`) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at")
|
||||
->execute([$key, $json]);
|
||||
}
|
||||
|
||||
// ── NovaCPX panel update check ────────────────────────────────────────────────
|
||||
echo "[novacpx] Fetching remote commits…\n";
|
||||
if (is_dir($srcDir . '/.git')) {
|
||||
shell_exec("git -C " . escapeshellarg($srcDir) . " fetch origin 2>/dev/null");
|
||||
$logOut = shell_exec("git -C " . escapeshellarg($srcDir) . " log HEAD..origin/main --oneline 2>/dev/null") ?: '';
|
||||
$updates = array_values(array_filter(explode("\n", trim($logOut))));
|
||||
$branch = trim(shell_exec("git -C " . escapeshellarg($srcDir) . " branch --show-current 2>/dev/null") ?: 'main');
|
||||
$commit = trim(shell_exec("git -C " . escapeshellarg($srcDir) . " rev-parse --short HEAD 2>/dev/null") ?: '');
|
||||
cache($pdo, 'update_cache_novacpx', [
|
||||
'updates_available' => count($updates),
|
||||
'current_commit' => $commit,
|
||||
'branch' => $branch,
|
||||
'commits' => $updates,
|
||||
]);
|
||||
echo "[novacpx] Done — " . count($updates) . " commit(s) available.\n";
|
||||
} else {
|
||||
echo "[novacpx] Skipped — source repo not found at {$srcDir}.\n";
|
||||
}
|
||||
|
||||
// ── OS package update check ───────────────────────────────────────────────────
|
||||
echo "[os] Running apt-get update…\n";
|
||||
shell_exec('apt-get update -qq 2>/dev/null');
|
||||
$out = shell_exec('apt-get -s upgrade 2>/dev/null | grep "^Inst " | head -50') ?: '';
|
||||
$packages = array_values(array_filter(array_map(function ($line) {
|
||||
if (preg_match('/^Inst (\S+).*\[(\S+)\].*\((\S+)/', $line, $m)) return ['name' => $m[1], 'from' => $m[2], 'to' => $m[3]];
|
||||
if (preg_match('/^Inst (\S+)\s+\((\S+)/', $line, $m)) return ['name' => $m[1], 'from' => '', 'to' => $m[2]];
|
||||
return null;
|
||||
}, explode("\n", trim($out)))));
|
||||
$security = count(array_filter($packages, fn($p) => str_contains($p['name'] ?? '', 'security')));
|
||||
cache($pdo, 'update_cache_os', [
|
||||
'upgradable' => count($packages),
|
||||
'security_updates' => $security,
|
||||
'packages' => $packages,
|
||||
'last_checked' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
echo "[os] Done — " . count($packages) . " package(s) upgradable.\n";
|
||||
@@ -263,11 +263,12 @@
|
||||
}
|
||||
|
||||
// ── Updates ────────────────────────────────────────────────────────────────
|
||||
async function updates() {
|
||||
async function updates(force = false) {
|
||||
const qp = force ? { force: 1 } : {};
|
||||
const [ver, ncpxCheck, osCheck] = await Promise.all([
|
||||
Nova.api('system', 'version'),
|
||||
Nova.api('system', 'check-novacpx-update'),
|
||||
Nova.api('system', 'check-os-update'),
|
||||
Nova.api('system', 'check-novacpx-update', { params: qp }),
|
||||
Nova.api('system', 'check-os-update', { params: qp }),
|
||||
]);
|
||||
const v = ver?.data || {};
|
||||
const ncpx = ncpxCheck?.data || {};
|
||||
@@ -278,7 +279,10 @@
|
||||
const html = `
|
||||
<div class="page-header mb-3">
|
||||
<h2 class="page-title">Updates</h2>
|
||||
<p class="text-muted text-sm">Manage NovaCPX panel updates and OS package upgrades.</p>
|
||||
<div style="display:flex;align-items:center;gap:1rem;margin-left:auto">
|
||||
${(ncpx.cached || os.cached) ? `<span class="text-muted text-sm">Cached · last checked ${Nova.relTime(ncpx.cached_at || os.cached_at)}</span>` : ''}
|
||||
<button class="btn btn-ghost btn-sm" onclick="forceRefreshUpdates()">↻ Refresh now</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NovaCPX Panel Updates -->
|
||||
@@ -368,6 +372,13 @@
|
||||
return html;
|
||||
}
|
||||
|
||||
window.forceRefreshUpdates = () => {
|
||||
const content = document.getElementById('page-content');
|
||||
if (!content) return;
|
||||
content.innerHTML = '<div style="padding:2rem;color:var(--text-muted);text-align:center">Checking for updates…</div>';
|
||||
updates(true).then(html => { if (html) content.innerHTML = html; });
|
||||
};
|
||||
|
||||
window.loadServiceVersions = async () => {
|
||||
const body = document.getElementById('svc-versions-body');
|
||||
if (!body) return;
|
||||
|
||||
Reference in New Issue
Block a user