Add service update notification and version check enhancements
All checks were successful
PostgreSQL Compatibility Matrix / PG14 smoke (push) Successful in 9s
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 9s
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
Introduced a front-end mechanism to notify users of available service updates and enhanced the service info page to reflect update status dynamically. Removed backend audit log writes for version checks to streamline operations and improve performance. Updated styling to visually highlight update notifications.
This commit is contained in:
@@ -11,7 +11,6 @@ from app.core.db import get_db
|
|||||||
from app.core.deps import get_current_user
|
from app.core.deps import get_current_user
|
||||||
from app.models.models import ServiceInfoSettings, User
|
from app.models.models import ServiceInfoSettings, User
|
||||||
from app.schemas.service_info import ServiceInfoCheckResult, ServiceInfoOut
|
from app.schemas.service_info import ServiceInfoCheckResult, ServiceInfoOut
|
||||||
from app.services.audit import write_audit_log
|
|
||||||
from app.services.service_info import (
|
from app.services.service_info import (
|
||||||
UPSTREAM_REPO_WEB,
|
UPSTREAM_REPO_WEB,
|
||||||
fetch_latest_from_upstream,
|
fetch_latest_from_upstream,
|
||||||
@@ -71,6 +70,7 @@ async def check_service_version(
|
|||||||
user: User = Depends(get_current_user),
|
user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
) -> ServiceInfoCheckResult:
|
) -> ServiceInfoCheckResult:
|
||||||
|
_ = user
|
||||||
row = await _get_or_create_service_settings(db)
|
row = await _get_or_create_service_settings(db)
|
||||||
check_time = utcnow()
|
check_time = utcnow()
|
||||||
latest, latest_ref, error = await fetch_latest_from_upstream()
|
latest, latest_ref, error = await fetch_latest_from_upstream()
|
||||||
@@ -85,17 +85,6 @@ async def check_service_version(
|
|||||||
row.update_available = False
|
row.update_available = False
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(row)
|
await db.refresh(row)
|
||||||
await write_audit_log(
|
|
||||||
db,
|
|
||||||
"service.info.check",
|
|
||||||
user.id,
|
|
||||||
{
|
|
||||||
"latest_version": row.latest_version,
|
|
||||||
"latest_ref": row.release_check_url,
|
|
||||||
"update_available": row.update_available,
|
|
||||||
"last_check_error": row.last_check_error,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return ServiceInfoCheckResult(
|
return ServiceInfoCheckResult(
|
||||||
latest_version=row.latest_version,
|
latest_version=row.latest_version,
|
||||||
latest_ref=(row.release_check_url or None),
|
latest_ref=(row.release_check_url or None),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function Protected({ children }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Layout({ children }) {
|
function Layout({ children }) {
|
||||||
const { me, logout, uiMode, setUiMode, alertToasts, dismissAlertToast } = useAuth();
|
const { me, logout, uiMode, setUiMode, alertToasts, dismissAlertToast, serviceUpdateAvailable } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const navClass = ({ isActive }) => `nav-btn${isActive ? " active" : ""}`;
|
const navClass = ({ isActive }) => `nav-btn${isActive ? " active" : ""}`;
|
||||||
|
|
||||||
@@ -62,7 +62,10 @@ function Layout({ children }) {
|
|||||||
</span>
|
</span>
|
||||||
<span className="nav-label">Alerts</span>
|
<span className="nav-label">Alerts</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink to="/service-info" className={navClass}>
|
<NavLink
|
||||||
|
to="/service-info"
|
||||||
|
className={({ isActive }) => `nav-btn${isActive ? " active" : ""}${serviceUpdateAvailable ? " update-available" : ""}`}
|
||||||
|
>
|
||||||
<span className="nav-icon" aria-hidden="true">
|
<span className="nav-icon" aria-hidden="true">
|
||||||
<svg viewBox="0 0 24 24">
|
<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" />
|
<path d="M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zm0-11v6m0-10h.01" />
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function formatUptime(seconds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceInfoPage() {
|
export function ServiceInfoPage() {
|
||||||
const { tokens, refresh } = useAuth();
|
const { tokens, refresh, serviceInfo } = useAuth();
|
||||||
const [info, setInfo] = useState(null);
|
const [info, setInfo] = useState(null);
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
@@ -30,6 +30,10 @@ export function ServiceInfoPage() {
|
|||||||
load().catch((e) => setError(String(e.message || e)));
|
load().catch((e) => setError(String(e.message || e)));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (serviceInfo) setInfo(serviceInfo);
|
||||||
|
}, [serviceInfo]);
|
||||||
|
|
||||||
const checkNow = async () => {
|
const checkNow = async () => {
|
||||||
try {
|
try {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
@@ -56,14 +60,28 @@ export function ServiceInfoPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="service-page">
|
||||||
<h2>Service Information</h2>
|
<h2>Service Information</h2>
|
||||||
<p className="muted">Runtime details, installed version, and update check status for this NexaPG instance.</p>
|
<p className="muted">Runtime details, installed version, and update check status for this NexaPG instance.</p>
|
||||||
{error && <div className="card error">{error}</div>}
|
{error && <div className="card error">{error}</div>}
|
||||||
{message && <div className="test-connection-result ok">{message}</div>}
|
{message && <div className="test-connection-result ok service-msg">{message}</div>}
|
||||||
|
|
||||||
|
<div className={`card service-hero ${info.update_available ? "update" : "ok"}`}>
|
||||||
|
<div>
|
||||||
|
<strong className="service-hero-title">
|
||||||
|
{info.update_available ? `Update available: ${info.latest_version}` : "Service is up to date"}
|
||||||
|
</strong>
|
||||||
|
<p className="muted service-hero-sub">
|
||||||
|
Automatic release checks run every 30 seconds. Source: official NexaPG upstream releases.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" className="secondary-btn" disabled={busy} onClick={checkNow}>
|
||||||
|
Check Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid three">
|
<div className="grid three">
|
||||||
<div className="card">
|
<div className="card service-card">
|
||||||
<h3>Application</h3>
|
<h3>Application</h3>
|
||||||
<div className="overview-kv">
|
<div className="overview-kv">
|
||||||
<span>App Name</span>
|
<span>App Name</span>
|
||||||
@@ -74,7 +92,7 @@ export function ServiceInfoPage() {
|
|||||||
<strong>{info.api_prefix}</strong>
|
<strong>{info.api_prefix}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card">
|
<div className="card service-card">
|
||||||
<h3>Runtime</h3>
|
<h3>Runtime</h3>
|
||||||
<div className="overview-kv">
|
<div className="overview-kv">
|
||||||
<span>Host</span>
|
<span>Host</span>
|
||||||
@@ -85,7 +103,7 @@ export function ServiceInfoPage() {
|
|||||||
<strong>{formatUptime(info.uptime_seconds)}</strong>
|
<strong>{formatUptime(info.uptime_seconds)}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card">
|
<div className="card service-card">
|
||||||
<h3>Version Status</h3>
|
<h3>Version Status</h3>
|
||||||
<div className="overview-kv">
|
<div className="overview-kv">
|
||||||
<span>Current NexaPG Version</span>
|
<span>Current NexaPG Version</span>
|
||||||
@@ -93,21 +111,16 @@ export function ServiceInfoPage() {
|
|||||||
<span>Latest Known Version</span>
|
<span>Latest Known Version</span>
|
||||||
<strong>{info.latest_version || "-"}</strong>
|
<strong>{info.latest_version || "-"}</strong>
|
||||||
<span>Update Status</span>
|
<span>Update Status</span>
|
||||||
<strong className={info.update_available ? "lag-bad" : "pill primary"}>
|
<strong className={info.update_available ? "service-status-update" : "service-status-ok"}>
|
||||||
{info.update_available ? "Update available" : "Up to date"}
|
{info.update_available ? "Update available" : "Up to date"}
|
||||||
</strong>
|
</strong>
|
||||||
<span>Last Check</span>
|
<span>Last Check</span>
|
||||||
<strong>{info.last_checked_at ? new Date(info.last_checked_at).toLocaleString() : "never"}</strong>
|
<strong>{info.last_checked_at ? new Date(info.last_checked_at).toLocaleString() : "never"}</strong>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<div className="card">
|
<div className="card service-card">
|
||||||
<h3>Release Source</h3>
|
<h3>Release Source</h3>
|
||||||
<p className="muted">
|
<p className="muted">
|
||||||
Update checks run against the official NexaPG repository. This source is fixed in code and cannot be changed
|
Update checks run against the official NexaPG repository. This source is fixed in code and cannot be changed
|
||||||
@@ -121,7 +134,7 @@ export function ServiceInfoPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card">
|
<div className="card service-card">
|
||||||
<h3>Version Control Policy</h3>
|
<h3>Version Control Policy</h3>
|
||||||
<p className="muted">
|
<p className="muted">
|
||||||
Version and update-source settings are not editable in the app. Only code maintainers of the official NexaPG
|
Version and update-source settings are not editable in the app. Only code maintainers of the official NexaPG
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function AuthProvider({ children }) {
|
|||||||
const [uiMode, setUiModeState] = useState(loadUiMode);
|
const [uiMode, setUiModeState] = useState(loadUiMode);
|
||||||
const [alertStatus, setAlertStatus] = useState({ warnings: [], alerts: [], warning_count: 0, alert_count: 0 });
|
const [alertStatus, setAlertStatus] = useState({ warnings: [], alerts: [], warning_count: 0, alert_count: 0 });
|
||||||
const [alertToasts, setAlertToasts] = useState([]);
|
const [alertToasts, setAlertToasts] = useState([]);
|
||||||
|
const [serviceInfo, setServiceInfo] = useState(null);
|
||||||
const knownAlertKeysRef = useRef(new Set());
|
const knownAlertKeysRef = useRef(new Set());
|
||||||
const hasAlertSnapshotRef = useRef(false);
|
const hasAlertSnapshotRef = useRef(false);
|
||||||
|
|
||||||
@@ -175,6 +176,49 @@ export function AuthProvider({ children }) {
|
|||||||
};
|
};
|
||||||
}, [tokens?.accessToken, tokens?.refreshToken]);
|
}, [tokens?.accessToken, tokens?.refreshToken]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tokens?.accessToken) {
|
||||||
|
setServiceInfo(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
|
const request = async (path, method = "GET") => {
|
||||||
|
const doFetch = async (accessToken) =>
|
||||||
|
fetch(`${API_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
let res = await doFetch(tokens.accessToken);
|
||||||
|
if (res.status === 401 && tokens.refreshToken) {
|
||||||
|
const refreshed = await refresh();
|
||||||
|
if (refreshed?.accessToken) {
|
||||||
|
res = await doFetch(refreshed.accessToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const runServiceCheck = async () => {
|
||||||
|
await request("/service/info/check", "POST");
|
||||||
|
const info = await request("/service/info", "GET");
|
||||||
|
if (mounted && info) setServiceInfo(info);
|
||||||
|
};
|
||||||
|
|
||||||
|
runServiceCheck().catch(() => {});
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
runServiceCheck().catch(() => {});
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, [tokens?.accessToken, tokens?.refreshToken]);
|
||||||
|
|
||||||
const setUiMode = (nextMode) => {
|
const setUiMode = (nextMode) => {
|
||||||
const mode = nextMode === "easy" ? "easy" : "dba";
|
const mode = nextMode === "easy" ? "easy" : "dba";
|
||||||
setUiModeState(mode);
|
setUiModeState(mode);
|
||||||
@@ -193,8 +237,10 @@ export function AuthProvider({ children }) {
|
|||||||
alertStatus,
|
alertStatus,
|
||||||
alertToasts,
|
alertToasts,
|
||||||
dismissAlertToast,
|
dismissAlertToast,
|
||||||
|
serviceInfo,
|
||||||
|
serviceUpdateAvailable: !!serviceInfo?.update_available,
|
||||||
}),
|
}),
|
||||||
[tokens, me, uiMode, alertStatus, alertToasts]
|
[tokens, me, uiMode, alertStatus, alertToasts, serviceInfo]
|
||||||
);
|
);
|
||||||
return <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
|
return <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,27 @@ a {
|
|||||||
background: linear-gradient(180deg, #74e8ff, #25bdf3);
|
background: linear-gradient(180deg, #74e8ff, #25bdf3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-btn.update-available {
|
||||||
|
border-color: #c7962f;
|
||||||
|
background: linear-gradient(180deg, #3e2f14, #2f240f);
|
||||||
|
color: #ffecc4;
|
||||||
|
box-shadow: inset 0 0 0 1px #f6c75a38, 0 8px 20px #2d1d0680;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn.update-available .nav-icon {
|
||||||
|
border-color: #d3a240;
|
||||||
|
background: linear-gradient(180deg, #5a441a, #433312);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn.update-available:hover {
|
||||||
|
border-color: #ffd46e;
|
||||||
|
background: linear-gradient(180deg, #523d18, #3b2d12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn.update-available::before {
|
||||||
|
background: linear-gradient(180deg, #ffe4a3, #e0ac3e);
|
||||||
|
}
|
||||||
|
|
||||||
.nav-btn.admin-nav {
|
.nav-btn.admin-nav {
|
||||||
border-color: #5b4da1;
|
border-color: #5b4da1;
|
||||||
background: linear-gradient(180deg, #1c2a58, #18224a);
|
background: linear-gradient(180deg, #1c2a58, #18224a);
|
||||||
@@ -1279,6 +1300,51 @@ td {
|
|||||||
color: #9eb8d6;
|
color: #9eb8d6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.service-page .service-msg {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero.ok {
|
||||||
|
border-color: #2f8f63;
|
||||||
|
background: linear-gradient(90deg, #123827, #102e42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero.update {
|
||||||
|
border-color: #dfab3e;
|
||||||
|
background: linear-gradient(90deg, #4a3511, #2f2452);
|
||||||
|
box-shadow: 0 12px 28px #2b1f066b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero-title {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero-sub {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card {
|
||||||
|
box-shadow: 0 10px 24px #0416343d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status-ok {
|
||||||
|
color: #6ef0ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status-update {
|
||||||
|
color: #ffd77e;
|
||||||
|
}
|
||||||
|
|
||||||
.alerts-subtitle {
|
.alerts-subtitle {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
color: #a6c0df;
|
color: #a6c0df;
|
||||||
|
|||||||
Reference in New Issue
Block a user