Add service information feature with version checks
All checks were successful
PostgreSQL Compatibility Matrix / PG14 smoke (push) Successful in 8s
PostgreSQL Compatibility Matrix / PG15 smoke (push) Successful in 8s
PostgreSQL Compatibility Matrix / PG16 smoke (push) Successful in 8s
PostgreSQL Compatibility Matrix / PG17 smoke (push) Successful in 8s
PostgreSQL Compatibility Matrix / PG18 smoke (push) Successful in 8s

This commit introduces a new "Service Information" section displaying runtime details, installed version, and update status for the NexaPG application. It includes backend API endpoints, database schema changes, and a corresponding frontend page that allows users to check for updates against the official repository. The `.env` example now includes an `APP_VERSION` variable, and related documentation has been updated.
This commit is contained in:
2026-02-13 08:54:13 +01:00
parent fd24a3a548
commit 0445a72764
12 changed files with 462 additions and 1 deletions

View File

@@ -8,6 +8,7 @@ import { TargetDetailPage } from "./pages/TargetDetailPage";
import { QueryInsightsPage } from "./pages/QueryInsightsPage";
import { AlertsPage } from "./pages/AlertsPage";
import { AdminUsersPage } from "./pages/AdminUsersPage";
import { ServiceInfoPage } from "./pages/ServiceInfoPage";
function Protected({ children }) {
const { tokens } = useAuth();
@@ -61,6 +62,14 @@ function Layout({ children }) {
</span>
<span className="nav-label">Alerts</span>
</NavLink>
<NavLink to="/service-info" className={navClass}>
<span className="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zm0-11v6m0-10h.01" />
</svg>
</span>
<span className="nav-label">Service Information</span>
</NavLink>
{me?.role === "admin" && (
<>
<div className="sidebar-nav-spacer" aria-hidden="true" />
@@ -150,6 +159,7 @@ export function App() {
<Route path="/targets/:id" element={<TargetDetailPage />} />
<Route path="/query-insights" element={<QueryInsightsPage />} />
<Route path="/alerts" element={<AlertsPage />} />
<Route path="/service-info" element={<ServiceInfoPage />} />
<Route path="/admin/users" element={<AdminUsersPage />} />
</Routes>
</Layout>

View File

@@ -0,0 +1,133 @@
import React, { useEffect, useState } from "react";
import { apiFetch } from "../api";
import { useAuth } from "../state";
function formatUptime(seconds) {
const total = Math.max(0, Number(seconds || 0));
const d = Math.floor(total / 86400);
const h = Math.floor((total % 86400) / 3600);
const m = Math.floor((total % 3600) / 60);
const s = total % 60;
if (d > 0) return `${d}d ${h}h ${m}m`;
if (h > 0) return `${h}h ${m}m ${s}s`;
return `${m}m ${s}s`;
}
export function ServiceInfoPage() {
const { tokens, refresh } = useAuth();
const [info, setInfo] = useState(null);
const [message, setMessage] = useState("");
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
const load = async () => {
setError("");
const data = await apiFetch("/service/info", {}, tokens, refresh);
setInfo(data);
};
useEffect(() => {
load().catch((e) => setError(String(e.message || e)));
}, []);
const checkNow = async () => {
try {
setBusy(true);
setError("");
setMessage("");
const result = await apiFetch("/service/info/check", { method: "POST" }, tokens, refresh);
await load();
if (result.last_check_error) {
setMessage(`Version check finished with warning: ${result.last_check_error}`);
} else if (result.update_available) {
setMessage(`Update available: ${result.latest_version}`);
} else {
setMessage("Version check completed. No update detected.");
}
} catch (e) {
setError(String(e.message || e));
} finally {
setBusy(false);
}
};
if (!info) {
return <div className="card">Loading service information...</div>;
}
return (
<div>
<h2>Service Information</h2>
<p className="muted">Runtime details, installed version, and update check status for this NexaPG instance.</p>
{error && <div className="card error">{error}</div>}
{message && <div className="test-connection-result ok">{message}</div>}
<div className="grid three">
<div className="card">
<h3>Application</h3>
<div className="overview-kv">
<span>App Name</span>
<strong>{info.app_name}</strong>
<span>Environment</span>
<strong>{info.environment}</strong>
<span>API Prefix</span>
<strong>{info.api_prefix}</strong>
</div>
</div>
<div className="card">
<h3>Runtime</h3>
<div className="overview-kv">
<span>Host</span>
<strong>{info.hostname}</strong>
<span>Python</span>
<strong>{info.python_version}</strong>
<span>Uptime</span>
<strong>{formatUptime(info.uptime_seconds)}</strong>
</div>
</div>
<div className="card">
<h3>Version Status</h3>
<div className="overview-kv">
<span>Current NexaPG Version</span>
<strong>{info.app_version}</strong>
<span>Latest Known Version</span>
<strong>{info.latest_version || "-"}</strong>
<span>Update Status</span>
<strong className={info.update_available ? "lag-bad" : "pill primary"}>
{info.update_available ? "Update available" : "Up to date"}
</strong>
<span>Last Check</span>
<strong>{info.last_checked_at ? new Date(info.last_checked_at).toLocaleString() : "never"}</strong>
</div>
<div className="form-actions" style={{ marginTop: 12 }}>
<button type="button" className="secondary-btn" disabled={busy} onClick={checkNow}>
Check for Updates
</button>
</div>
</div>
</div>
<div className="card">
<h3>Release Source</h3>
<p className="muted">
Update checks run against the official NexaPG repository. This source is fixed in code and cannot be changed
via UI.
</p>
<div className="overview-kv">
<span>Source Repository</span>
<strong>{info.update_source}</strong>
<span>Latest Reference Type</span>
<strong>{info.latest_ref || "-"}</strong>
</div>
</div>
<div className="card">
<h3>Version Control Policy</h3>
<p className="muted">
Version and update-source settings are not editable in the app. Only code maintainers of the official NexaPG
repository can change that behavior.
</p>
</div>
</div>
);
}