feat: version update check — notify when newer version is on GitHub (v0.11.8 Beta)

Backend: GET /api/system/version-check fetches frontend/package.json from
the main branch on GitHub, caches result in app_settings for 24 hours,
and compares semver against the running version. Fails silently (no banner)
for air-gapped or firewalled deployments.

Frontend: dismissible blue update banner in SecurityBanners (rendered
between TopBar and main content) — only visible when a newer version is
detected. Shows target version and docker compose update command.
This commit is contained in:
2GT-Rich
2026-05-17 19:38:25 -07:00
parent bf62b9e10f
commit b1e4944823
8 changed files with 143 additions and 12 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mikrotik-manager-frontend",
"version": "0.11.7-beta",
"version": "0.11.8-beta",
"private": true,
"type": "module",
"scripts": {
+48 -8
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { AlertTriangle, ShieldAlert } from 'lucide-react';
import { authApi } from '../services/api';
import { AlertTriangle, ShieldAlert, ArrowUpCircle, X } from 'lucide-react';
import { authApi, systemApi } from '../services/api';
interface SecurityStatus {
warnings: string[];
@@ -9,29 +9,69 @@ interface SecurityStatus {
export default function SecurityBanners() {
const [status, setStatus] = useState<SecurityStatus | null>(null);
const [updateInfo, setUpdateInfo] = useState<{ latest: string } | null>(null);
const [updateDismissed, setUpdateDismissed] = useState(false);
useEffect(() => {
authApi.securityStatus()
.then((r) => setStatus(r.data))
.catch(() => {});
systemApi.versionCheck()
.then((r) => {
if (r.data.update_available && r.data.latest) {
setUpdateInfo({ latest: r.data.latest });
}
})
.catch(() => {});
}, []);
if (!status || status.warnings.length === 0) return null;
const noWarnings = !status || status.warnings.length === 0;
const noUpdate = !updateInfo || updateDismissed;
if (noWarnings && noUpdate) return null;
const hasSecretWarning =
const hasSecretWarning = !!status && (
status.warnings.includes('jwt_secret_default') ||
status.warnings.includes('encryption_key_default');
const hasAdminWarning = status.warnings.includes('admin_password_default');
status.warnings.includes('encryption_key_default')
);
const hasAdminWarning = !!status && status.warnings.includes('admin_password_default');
const secretNames = [
status.warnings.includes('jwt_secret_default') && 'JWT_SECRET',
status.warnings.includes('encryption_key_default') && 'ENCRYPTION_KEY',
status?.warnings.includes('jwt_secret_default') && 'JWT_SECRET',
status?.warnings.includes('encryption_key_default') && 'ENCRYPTION_KEY',
]
.filter(Boolean)
.join(' and ');
return (
<div>
{updateInfo && !updateDismissed && (
<div className="flex items-center justify-between gap-4 bg-blue-50 dark:bg-blue-900/20 border-b border-blue-200 dark:border-blue-800 px-4 py-2 text-sm text-blue-800 dark:text-blue-200">
<div className="flex items-center gap-2">
<ArrowUpCircle className="w-4 h-4 shrink-0" />
<span>
<span className="font-semibold">Update available:</span>{' '}
v{updateInfo.latest} is on GitHub.{' '}
<a
href="https://github.com/2GT-Media-Group-LLC/mikrotik-manager"
target="_blank"
rel="noreferrer"
className="underline hover:no-underline"
>
View release
</a>{' '}
and run <code className="font-mono text-xs">docker compose pull &amp;&amp; docker compose up -d</code> to update.
</span>
</div>
<button
onClick={() => setUpdateDismissed(true)}
className="shrink-0 text-blue-500 hover:text-blue-700 dark:hover:text-blue-300"
aria-label="Dismiss"
>
<X className="w-4 h-4" />
</button>
</div>
)}
{hasSecretWarning && (
<div className="flex items-center gap-2 bg-yellow-50 dark:bg-yellow-900/20 border-b border-yellow-200 dark:border-yellow-800 px-4 py-2 text-sm text-yellow-800 dark:text-yellow-200">
<AlertTriangle className="w-4 h-4 shrink-0" />
+6
View File
@@ -343,6 +343,12 @@ export const deviceToolsApi = {
),
};
// ─── System ───────────────────────────────────────────────────────────────────
export const systemApi = {
versionCheck: () =>
api.get<{ current: string; latest: string | null; update_available: boolean }>('/system/version-check'),
};
// ─── Events ───────────────────────────────────────────────────────────────────
export const eventsApi = {
list: (params?: {
+1 -1
View File
@@ -1 +1 @@
export const APP_VERSION = 'v0.11.7 Beta';
export const APP_VERSION = 'v0.11.8 Beta';