diff --git a/panel/api/endpoints/system.php b/panel/api/endpoints/system.php index 07c5077..def395a 100644 --- a/panel/api/endpoints/system.php +++ b/panel/api/endpoints/system.php @@ -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 ───────────────────────────────────────────────── diff --git a/panel/bin/cache-update-check.php b/panel/bin/cache-update-check.php new file mode 100644 index 0000000..158bff4 --- /dev/null +++ b/panel/bin/cache-update-check.php @@ -0,0 +1,62 @@ +#!/usr/bin/env php + 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"; diff --git a/panel/public/assets/js/admin.js b/panel/public/assets/js/admin.js index a4ced3a..7bf78d2 100644 --- a/panel/public/assets/js/admin.js +++ b/panel/public/assets/js/admin.js @@ -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 = ` @@ -368,6 +372,13 @@ return html; } + window.forceRefreshUpdates = () => { + const content = document.getElementById('page-content'); + if (!content) return; + content.innerHTML = '
Checking for updates…
'; + updates(true).then(html => { if (html) content.innerHTML = html; }); + }; + window.loadServiceVersions = async () => { const body = document.getElementById('svc-versions-body'); if (!body) return;