mirror of
https://github.com/myronblair/novacpx
synced 2026-07-28 05:03:14 -05:00
Add service versions panel, version auto-tracking, Fail2Ban sidebar, streaming service switch
- .github/workflows/version-bump.yml: auto-increment patch version on push to main/beta - admin/index.php: show version under logo from VERSION file - system.php: service-versions endpoint (catalog of 22 services with version/description/status) - admin.js: updates page shows Installed Services table with current/latest/status/description - admin.js: loadServiceVersions() lazy-loaded after page render via setTimeout - admin/index.php: separate Fail2Ban sidebar entry (was merged into Firewall label) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
name: Auto Version Bump
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- beta
|
||||||
|
paths-ignore:
|
||||||
|
- 'VERSION'
|
||||||
|
- '.github/**'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bump:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Bump version
|
||||||
|
id: bump
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.ref_name }}"
|
||||||
|
CURRENT=$(cat VERSION)
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "${CURRENT%%-*}"
|
||||||
|
PATCH=${PATCH:-0}
|
||||||
|
|
||||||
|
if [ "$BRANCH" = "main" ]; then
|
||||||
|
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
|
||||||
|
else
|
||||||
|
# beta branch: append -beta.N
|
||||||
|
BETA_NUM=$(echo "$CURRENT" | grep -oP '(?<=beta\.)\d+' || echo "0")
|
||||||
|
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}-beta.$((BETA_NUM + 1))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$NEW_VERSION" > VERSION
|
||||||
|
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Bumped $CURRENT → $NEW_VERSION"
|
||||||
|
|
||||||
|
- name: Commit version
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add VERSION
|
||||||
|
git commit -m "chore: bump version to ${{ steps.bump.outputs.version }} [skip ci]"
|
||||||
|
git push
|
||||||
@@ -370,6 +370,77 @@ BASH;
|
|||||||
Response::success(['service' => $svc, 'status' => $status]);
|
Response::success(['service' => $svc, 'status' => $status]);
|
||||||
})(),
|
})(),
|
||||||
|
|
||||||
|
// ── Installed service versions with latest available ──────────────────────
|
||||||
|
'service-versions' => (function() {
|
||||||
|
Auth::getInstance()->require('admin');
|
||||||
|
|
||||||
|
// pkg => [label, description, systemd service name]
|
||||||
|
$catalog = [
|
||||||
|
'apache2' => ['Apache 2', 'Web server — serves all hosted websites on ports 80/443', 'apache2'],
|
||||||
|
'nginx' => ['Nginx', 'High-performance web server / reverse proxy', 'nginx'],
|
||||||
|
'php8.3' => ['PHP 8.3', 'Server-side scripting runtime (CLI + FPM)', 'php8.3-fpm'],
|
||||||
|
'php8.2' => ['PHP 8.2', 'PHP 8.2 runtime (legacy support)', 'php8.2-fpm'],
|
||||||
|
'php8.1' => ['PHP 8.1', 'PHP 8.1 runtime (legacy support)', 'php8.1-fpm'],
|
||||||
|
'php7.4' => ['PHP 7.4', 'PHP 7.4 runtime (end-of-life compatibility)', 'php7.4-fpm'],
|
||||||
|
'mysql-server' => ['MySQL', 'Relational database server (MariaDB/MySQL) for hosted sites', 'mysql'],
|
||||||
|
'mariadb-server' => ['MariaDB', 'MySQL-compatible database server fork', 'mariadb'],
|
||||||
|
'postfix' => ['Postfix', 'MTA — routes and delivers email for hosted domains', 'postfix'],
|
||||||
|
'dovecot-core' => ['Dovecot', 'IMAP/POP3 server — lets users retrieve email via mail clients', 'dovecot'],
|
||||||
|
'rspamd' => ['Rspamd', 'Spam filter that scores and rejects unwanted email', 'rspamd'],
|
||||||
|
'proftpd' => ['ProFTPD', 'FTP server for account file transfers', 'proftpd'],
|
||||||
|
'vsftpd' => ['vsftpd', 'Lightweight FTP server', 'vsftpd'],
|
||||||
|
'bind9' => ['BIND9', 'Authoritative DNS server — serves zone files for hosted domains', 'named'],
|
||||||
|
'fail2ban' => ['Fail2Ban', 'Intrusion prevention — bans IPs after repeated failed logins', 'fail2ban'],
|
||||||
|
'ufw' => ['UFW', 'Uncomplicated Firewall — iptables front-end for port management', null],
|
||||||
|
'certbot' => ['Certbot', "Let's Encrypt client — automates SSL certificate issuance", null],
|
||||||
|
'sqlite3' => ['SQLite3', 'Embedded SQL database — stores NovaCPX panel data', null],
|
||||||
|
'redis-server' => ['Redis', 'In-memory data store — used for caching and queuing', 'redis'],
|
||||||
|
'memcached' => ['Memcached', 'Memory object cache', 'memcached'],
|
||||||
|
'nodejs' => ['Node.js', 'JavaScript runtime — used by some web applications', null],
|
||||||
|
'git' => ['Git', 'Version control system — used for site deployments', null],
|
||||||
|
'curl' => ['curl', 'HTTP client — used by health checks and API calls', null],
|
||||||
|
];
|
||||||
|
|
||||||
|
$dpkgOut = shell_exec("dpkg-query -W -f='\${Package} \${Version}\\n' 2>/dev/null") ?: '';
|
||||||
|
$installed = [];
|
||||||
|
foreach (explode("\n", trim($dpkgOut)) as $line) {
|
||||||
|
[$pkg, $ver] = array_pad(explode(' ', $line, 2), 2, '');
|
||||||
|
if ($pkg) $installed[$pkg] = trim($ver);
|
||||||
|
}
|
||||||
|
|
||||||
|
$aptOut = shell_exec("apt-cache policy " . implode(' ', array_keys($catalog)) . " 2>/dev/null") ?: '';
|
||||||
|
$latest = [];
|
||||||
|
$curPkg = null;
|
||||||
|
foreach (explode("\n", $aptOut) as $line) {
|
||||||
|
if (preg_match('/^(\S+):$/', trim($line), $m)) { $curPkg = $m[1]; continue; }
|
||||||
|
if ($curPkg && preg_match('/Candidate:\s+(.+)/', $line, $m)) {
|
||||||
|
$latest[$curPkg] = trim($m[1]);
|
||||||
|
$curPkg = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$services = [];
|
||||||
|
foreach ($catalog as $pkg => [$label, $desc, $svc]) {
|
||||||
|
$ver = $installed[$pkg] ?? null;
|
||||||
|
if (!$ver) continue; // skip not-installed
|
||||||
|
$latestVer = $latest[$pkg] ?? 'unknown';
|
||||||
|
$status = $svc ? trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: 'inactive') : null;
|
||||||
|
$upToDate = ($latestVer === 'unknown' || $latestVer === '(none)') ? null
|
||||||
|
: (version_compare($ver, $latestVer, '>='));
|
||||||
|
$services[] = [
|
||||||
|
'pkg' => $pkg,
|
||||||
|
'label' => $label,
|
||||||
|
'desc' => $desc,
|
||||||
|
'installed' => $ver,
|
||||||
|
'latest' => $latestVer,
|
||||||
|
'up_to_date' => $upToDate,
|
||||||
|
'status' => $status,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::success(['services' => $services]);
|
||||||
|
})(),
|
||||||
|
|
||||||
// ── Service control (start/stop/restart) ──────────────────────────────────
|
// ── Service control (start/stop/restart) ──────────────────────────────────
|
||||||
'service' => (function() use ($body, $db) {
|
'service' => (function() use ($body, $db) {
|
||||||
Auth::getInstance()->require('admin');
|
Auth::getInstance()->require('admin');
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ require_once dirname(__DIR__) . '/_branding.php';
|
|||||||
<aside class="sidebar" id="sidebar">
|
<aside class="sidebar" id="sidebar">
|
||||||
<div class="sidebar-brand">
|
<div class="sidebar-brand">
|
||||||
<?= novacpx_logo_html('<svg class="logo-icon" viewBox="0 0 40 40" fill="none"><circle cx="20" cy="20" r="18" stroke="url(#lg1)" stroke-width="2"/><path d="M12 28 L20 8 L28 28" stroke="url(#lg2)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M14 22 H26" stroke="url(#lg2)" stroke-width="2" stroke-linecap="round"/><defs><linearGradient id="lg1" x1="2" y1="2" x2="38" y2="38"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient><linearGradient id="lg2" x1="12" y1="8" x2="28" y2="28"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient></defs></svg>') ?>
|
<?= novacpx_logo_html('<svg class="logo-icon" viewBox="0 0 40 40" fill="none"><circle cx="20" cy="20" r="18" stroke="url(#lg1)" stroke-width="2"/><path d="M12 28 L20 8 L28 28" stroke="url(#lg2)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M14 22 H26" stroke="url(#lg2)" stroke-width="2" stroke-linecap="round"/><defs><linearGradient id="lg1" x1="2" y1="2" x2="38" y2="38"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient><linearGradient id="lg2" x1="12" y1="8" x2="28" y2="28"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient></defs></svg>') ?>
|
||||||
<span class="logo-text">Nova<strong>CPX</strong> <small style="font-size:.65rem;color:var(--text-muted)">Admin</small></span>
|
<span class="logo-text">Nova<strong>CPX</strong> <small style="font-size:.65rem;color:var(--text-muted)">Admin</small>
|
||||||
|
<span id="panel-version-badge" style="display:block;font-size:.6rem;color:var(--text-muted);font-weight:400;line-height:1;margin-top:2px">v<?= trim(@file_get_contents(NOVACPX_ROOT . '/VERSION') ?: NOVACPX_VERSION) ?></span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
|
|||||||
@@ -275,7 +275,7 @@
|
|||||||
const ncpxCount = ncpx.updates_available || 0;
|
const ncpxCount = ncpx.updates_available || 0;
|
||||||
const osCount = os.upgradable || 0;
|
const osCount = os.upgradable || 0;
|
||||||
|
|
||||||
return `
|
const html = `
|
||||||
<div class="page-header mb-3">
|
<div class="page-header mb-3">
|
||||||
<h2 class="page-title">Updates</h2>
|
<h2 class="page-title">Updates</h2>
|
||||||
<p class="text-muted text-sm">Manage NovaCPX panel updates and OS package upgrades.</p>
|
<p class="text-muted text-sm">Manage NovaCPX panel updates and OS package upgrades.</p>
|
||||||
@@ -317,6 +317,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Installed Services -->
|
||||||
|
<div class="card mb-3" id="svc-versions-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<svg class="icon-sm mr-1"><use href="/assets/img/nova-icons.svg#ni-server"/></svg>
|
||||||
|
Installed Services
|
||||||
|
</span>
|
||||||
|
<button class="btn btn-ghost btn-sm ml-auto" onclick="loadServiceVersions()">↻ Refresh</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="svc-versions-body">
|
||||||
|
<div class="loading">Loading service inventory…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- OS Updates -->
|
<!-- OS Updates -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -349,8 +363,35 @@
|
|||||||
` : `<p class="text-muted">All OS packages are current.</p>`}
|
` : `<p class="text-muted">All OS packages are current.</p>`}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
setTimeout(loadServiceVersions, 80);
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.loadServiceVersions = async () => {
|
||||||
|
const body = document.getElementById('svc-versions-body');
|
||||||
|
if (!body) return;
|
||||||
|
body.innerHTML = '<div class="loading">Scanning installed services…</div>';
|
||||||
|
const r = await Nova.api('system', 'service-versions');
|
||||||
|
const svcs = r?.data?.services || [];
|
||||||
|
if (!svcs.length) { body.innerHTML = '<p class="text-muted">No tracked services found.</p>'; return; }
|
||||||
|
const statusDot = s => s === 'active'
|
||||||
|
? '<span style="color:var(--green);font-size:.75rem">● running</span>'
|
||||||
|
: s === null ? '<span style="color:var(--text-muted);font-size:.75rem">—</span>'
|
||||||
|
: '<span style="color:var(--red);font-size:.75rem">● stopped</span>';
|
||||||
|
body.innerHTML = `<div style="overflow-x:auto"><table class="table">
|
||||||
|
<thead><tr><th>Service</th><th>Description</th><th>Installed</th><th>Latest</th><th>Status</th><th>State</th></tr></thead>
|
||||||
|
<tbody>${svcs.map(s => `<tr>
|
||||||
|
<td><strong>${Nova.escHtml(s.label)}</strong><br><code style="font-size:.7rem">${Nova.escHtml(s.pkg)}</code></td>
|
||||||
|
<td style="font-size:.82rem;color:var(--text-muted);max-width:280px">${Nova.escHtml(s.desc)}</td>
|
||||||
|
<td><code style="font-size:.8rem">${Nova.escHtml(s.installed)}</code></td>
|
||||||
|
<td><code style="font-size:.8rem;color:${s.up_to_date === false ? 'var(--yellow)' : 'var(--text-muted)'}">${Nova.escHtml(s.latest)}</code></td>
|
||||||
|
<td>${s.up_to_date === true ? Nova.badge('current','green') : s.up_to_date === false ? Nova.badge('update available','yellow') : '<span style="color:var(--text-muted);font-size:.8rem">—</span>'}</td>
|
||||||
|
<td>${statusDot(s.status)}</td>
|
||||||
|
</tr>`).join('')}</tbody>
|
||||||
|
</table></div>`;
|
||||||
|
};
|
||||||
|
|
||||||
// ── Audit Log ──────────────────────────────────────────────────────────────
|
// ── Audit Log ──────────────────────────────────────────────────────────────
|
||||||
async function auditLog(opts = {}) {
|
async function auditLog(opts = {}) {
|
||||||
const { page = 1, user = '', action = '', date_from = '', date_to = '' } = opts;
|
const { page = 1, user = '', action = '', date_from = '', date_to = '' } = opts;
|
||||||
|
|||||||
Reference in New Issue
Block a user