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
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:
@@ -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>
|
||||
|
||||
133
frontend/src/pages/ServiceInfoPage.jsx
Normal file
133
frontend/src/pages/ServiceInfoPage.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user