feat: show New Update Available badge when GitHub has newer commits

- Add checkForUpdates() with 10-minute cached GitHub API check
- Add fetchLatestCommitInfo() to get latest commit SHA + message from GitHub API
- Save deployed commit SHA to writable/deployed_commit.txt after each deploy
- Invalidate update cache on successful deploy so next check is always fresh
- Button turns yellow + red NEW badge when update available
- Show commit message and short SHA diff (deployed → latest) in alert
- Show green 'Up to date' with deployed SHA when already current
- GitHub API rate limit safe: checks once per 10 mins via file cache
This commit is contained in:
DevSoftTech Server 2026-08-04 04:03:09 +05:30
parent 41f07357d2
commit dcc366444b
2 changed files with 133 additions and 11 deletions

View File

@ -7,10 +7,19 @@ use RuntimeException;
class SystemOperations
{
private string $historyPath;
private string $deployedShaPath;
private string $updateCachePath;
private const GITHUB_OWNER = 'infodevsofttech-admin';
private const GITHUB_REPO = 'devsofttech_hms_ci4';
private const GITHUB_BRANCH = 'main';
private const UPDATE_CACHE_TTL = 600; // 10 minutes
public function __construct()
{
$this->historyPath = WRITEPATH . 'system_update_history.json';
$this->historyPath = WRITEPATH . 'system_update_history.json';
$this->deployedShaPath = WRITEPATH . 'deployed_commit.txt';
$this->updateCachePath = WRITEPATH . 'github_update_cache.json';
}
public function collectStatus(): array
@ -35,8 +44,9 @@ class SystemOperations
$status['network'] = $this->readNetworkStatus();
$status['internet'] = $this->readInternetStatus();
$status['services'] = $this->readServiceStatus();
$status['raid'] = $this->readRaidStatus();
$status['last_update'] = $this->getLatestHistoryEntry();
$status['raid'] = $this->readRaidStatus();
$status['last_update'] = $this->getLatestHistoryEntry();
$status['update_check'] = $this->checkForUpdates();
return $status;
}
@ -53,12 +63,12 @@ class SystemOperations
$this->purgeStaleRunning();
try {
$githubOwner = 'infodevsofttech-admin';
$githubRepo = 'devsofttech_hms_ci4';
$branch = 'main';
$zipUrl = "https://github.com/{$githubOwner}/{$githubRepo}/archive/refs/heads/{$branch}.zip";
$tempDir = sys_get_temp_dir();
$zipFile = $tempDir . '/hms_update_' . time() . '.zip';
$zipUrl = "https://github.com/" . self::GITHUB_OWNER . "/" . self::GITHUB_REPO . "/archive/refs/heads/" . self::GITHUB_BRANCH . ".zip";
$tempDir = sys_get_temp_dir();
$zipFile = $tempDir . '/hms_update_' . time() . '.zip';
// Fetch latest commit info for tracking (fail-safe, non-blocking)
$latestInfo = $this->fetchLatestCommitInfo();
file_put_contents($logFile, '[' . date('Y-m-d H:i:s') . "] Downloading: {$zipUrl}\n", FILE_APPEND);
@ -95,15 +105,22 @@ class SystemOperations
@unlink($zipFile);
$successMsg = 'HMS updated successfully from GitHub (direct deployment)';
$shaLine = !empty($latestInfo['sha']) ? "\nCommit: " . substr($latestInfo['sha'], 0, 7) . ($latestInfo['message'] ? ' — ' . $latestInfo['message'] : '') : '';
$this->appendHistory([
'timestamp' => date('Y-m-d H:i:s'),
'type' => 'update',
'status' => 'success',
'message' => $successMsg,
'detail' => "Downloaded: {$zipSize}\nSource: {$zipUrl}\nCompleted: " . date('Y-m-d H:i:s'),
'detail' => "Downloaded: {$zipSize}\nSource: {$zipUrl}{$shaLine}\nCompleted: " . date('Y-m-d H:i:s'),
]);
file_put_contents($logFile, '[' . date('Y-m-d H:i:s') . "] SUCCESS: {$successMsg}\n", FILE_APPEND);
// Save deployed SHA so update checker knows what version is live
if (!empty($latestInfo['sha'])) {
file_put_contents($this->deployedShaPath, $latestInfo['sha']);
}
@unlink($this->updateCachePath); // invalidate cache so next check is fresh
return ['ok' => true, 'message' => $successMsg];
} catch (\Throwable $e) {
@ -386,6 +403,85 @@ class SystemOperations
return $info;
}
public function checkForUpdates(): array
{
$result = [
'has_update' => false,
'deployed_sha' => '',
'latest_sha' => '',
'latest_message' => '',
'latest_date' => '',
'error' => '',
];
// Read cached result if fresh enough
if (file_exists($this->updateCachePath)) {
$cached = @json_decode((string) file_get_contents($this->updateCachePath), true);
if (is_array($cached) && isset($cached['fetched_at']) && (time() - (int)$cached['fetched_at']) < self::UPDATE_CACHE_TTL) {
$result['latest_sha'] = $cached['sha'] ?? '';
$result['latest_message'] = $cached['message'] ?? '';
$result['latest_date'] = $cached['date'] ?? '';
$result['deployed_sha'] = $this->getDeployedSha();
$result['has_update'] = $result['latest_sha'] !== '' && $result['latest_sha'] !== $result['deployed_sha'];
return $result;
}
}
$info = $this->fetchLatestCommitInfo();
if (!empty($info['error'])) {
$result['error'] = $info['error'];
return $result;
}
// Cache the result
file_put_contents($this->updateCachePath, json_encode([
'fetched_at' => time(),
'sha' => $info['sha'],
'message' => $info['message'],
'date' => $info['date'],
]));
$result['latest_sha'] = $info['sha'];
$result['latest_message'] = $info['message'];
$result['latest_date'] = $info['date'];
$result['deployed_sha'] = $this->getDeployedSha();
$result['has_update'] = $result['latest_sha'] !== '' && $result['latest_sha'] !== $result['deployed_sha'];
return $result;
}
private function fetchLatestCommitInfo(): array
{
$apiUrl = "https://api.github.com/repos/" . self::GITHUB_OWNER . "/" . self::GITHUB_REPO . "/commits/" . self::GITHUB_BRANCH;
$ctx = stream_context_create(['http' => [
'timeout' => 8,
'header' => "User-Agent: HMS-CI4-Updater/1.0\r\nAccept: application/vnd.github.v3+json\r\n",
'ignore_errors' => true,
]]);
$body = @file_get_contents($apiUrl, false, $ctx);
if ($body === false) {
return ['sha' => '', 'message' => '', 'date' => '', 'error' => 'Could not reach GitHub API'];
}
$data = json_decode($body, true);
if (!is_array($data) || empty($data['sha'])) {
return ['sha' => '', 'message' => '', 'date' => '', 'error' => 'Unexpected GitHub API response'];
}
$message = substr((string)($data['commit']['message'] ?? ''), 0, 72);
$message = strtok($message, "\n"); // first line only
return [
'sha' => (string)$data['sha'],
'message' => (string)$message,
'date' => (string)($data['commit']['committer']['date'] ?? ''),
'error' => '',
];
}
private function getDeployedSha(): string
{
if (!file_exists($this->deployedShaPath)) return '';
return trim((string)@file_get_contents($this->deployedShaPath));
}
public function getHistory(): array
{
if (! file_exists($this->historyPath)) {

View File

@ -74,7 +74,33 @@
<h5 class="mb-0">Maintenance Actions</h5>
</div>
<div class="card-body d-grid gap-2">
<button class="btn btn-success" id="btnUpdateDirectGithub" title="Download and deploy latest from GitHub">Deploy from GitHub</button>
<?php
$uc = $status['update_check'] ?? [];
$hasUpdate = !empty($uc['has_update']);
$latestMsg = esc($uc['latest_message'] ?? '');
$latestSha = substr($uc['latest_sha'] ?? '', 0, 7);
$deployedSha = substr($uc['deployed_sha'] ?? '', 0, 7);
?>
<?php if ($hasUpdate): ?>
<div class="alert alert-warning py-2 px-3 mb-1 d-flex align-items-center gap-2" style="font-size:13px;">
<i class="bi bi-arrow-up-circle-fill text-warning fs-5"></i>
<div>
<strong>New update available!</strong>
<?php if ($latestMsg): ?><br><span class="text-muted"><?= $latestMsg ?></span><?php endif; ?>
<?php if ($latestSha): ?><br><code><?= esc($deployedSha ?: 'none') ?> → <?= esc($latestSha) ?></code><?php endif; ?>
</div>
</div>
<?php elseif (empty($uc['error']) && $deployedSha): ?>
<div class="text-success small mb-1 d-flex align-items-center gap-1">
<i class="bi bi-check-circle-fill"></i> Up to date <code class="ms-1"><?= esc($deployedSha) ?></code>
</div>
<?php endif; ?>
<button class="btn <?= $hasUpdate ? 'btn-warning' : 'btn-success' ?> position-relative" id="btnUpdateDirectGithub" title="Download and deploy latest from GitHub">
<i class="bi bi-cloud-download me-1"></i>Deploy from GitHub
<?php if ($hasUpdate): ?>
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size:10px;">NEW</span>
<?php endif; ?>
</button>
</div>
</div>
</div>