Version Control Policy
Version and update-source settings are not editable in the app. Only code maintainers of the official NexaPG
diff --git a/frontend/src/state.jsx b/frontend/src/state.jsx
index 1b0e568..17ab7fc 100644
--- a/frontend/src/state.jsx
+++ b/frontend/src/state.jsx
@@ -29,6 +29,7 @@ export function AuthProvider({ children }) {
const [uiMode, setUiModeState] = useState(loadUiMode);
const [alertStatus, setAlertStatus] = useState({ warnings: [], alerts: [], warning_count: 0, alert_count: 0 });
const [alertToasts, setAlertToasts] = useState([]);
+ const [serviceInfo, setServiceInfo] = useState(null);
const knownAlertKeysRef = useRef(new Set());
const hasAlertSnapshotRef = useRef(false);
@@ -175,6 +176,49 @@ export function AuthProvider({ children }) {
};
}, [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 mode = nextMode === "easy" ? "easy" : "dba";
setUiModeState(mode);
@@ -193,8 +237,10 @@ export function AuthProvider({ children }) {
alertStatus,
alertToasts,
dismissAlertToast,
+ serviceInfo,
+ serviceUpdateAvailable: !!serviceInfo?.update_available,
}),
- [tokens, me, uiMode, alertStatus, alertToasts]
+ [tokens, me, uiMode, alertStatus, alertToasts, serviceInfo]
);
return {children};
}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index c79d0a0..9cdda03 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -114,6 +114,27 @@ a {
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 {
border-color: #5b4da1;
background: linear-gradient(180deg, #1c2a58, #18224a);
@@ -1279,6 +1300,51 @@ td {
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 {
margin-top: 2px;
color: #a6c0df;