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:
2026-06-09 22:23:44 +00:00
parent 0c32c8a018
commit 09bd0820a5
3 changed files with 111 additions and 19 deletions
+34 -15
View File
@@ -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 ─────────────────────────────────────────────────
+62
View File
@@ -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";
+15 -4
View File
@@ -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;