diff --git a/README.md b/README.md index 856ed02..75030f3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A self-hosted, full-stack network management platform for MikroTik devices. Monitor, configure, and manage your entire MikroTik infrastructure — routers, switches, and wireless access points — from a single web interface. -![Version](https://img.shields.io/badge/version-0.16.10_Beta-blue) +![Version](https://img.shields.io/badge/version-0.16.11_Beta-blue) ![License](https://img.shields.io/badge/license-AGPLv3-blue) ![Docker](https://img.shields.io/badge/docker-compose-2496ED?logo=docker&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-5.3-3178C6?logo=typescript&logoColor=white) diff --git a/backend/package.json b/backend/package.json index fd22951..83e1708 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-backend", - "version": "0.16.10-beta", + "version": "0.16.11-beta", "description": "Mikrotik Network Management Platform - Backend API", "main": "dist/index.js", "scripts": { diff --git a/backend/src/routes/system.ts b/backend/src/routes/system.ts index e30c9fa..5431e2b 100644 --- a/backend/src/routes/system.ts +++ b/backend/src/routes/system.ts @@ -1,12 +1,27 @@ import { Router } from 'express'; import type { Request, Response } from 'express'; +import { readFileSync } from 'fs'; +import { join } from 'path'; import { requireAuth } from '../middleware/auth'; import { query } from '../config/database'; const router = Router(); router.use(requireAuth); -const CURRENT_VERSION = process.env.npm_package_version || '0.0.0'; +// The container runs `node dist/index.js` (not via npm), so npm_package_version +// is unset there — read the bundled package.json instead, falling back to it. +// Without this, current stays "0.0.0" and every release looks like an update. +function resolveCurrentVersion(): string { + if (process.env.npm_package_version) return process.env.npm_package_version; + try { + const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8')) as { version?: string }; + return pkg.version || '0.0.0'; + } catch { + return '0.0.0'; + } +} + +const CURRENT_VERSION = resolveCurrentVersion(); const RAW_URL = 'https://raw.githubusercontent.com/2GT-Media-Group-LLC/mikrotik-manager/main/frontend/package.json'; const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours diff --git a/frontend/package.json b/frontend/package.json index ee6da2b..3547636 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-frontend", - "version": "0.16.10-beta", + "version": "0.16.11-beta", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/components/SecurityBanners.tsx b/frontend/src/components/SecurityBanners.tsx index 90ed3e7..3c76f13 100644 --- a/frontend/src/components/SecurityBanners.tsx +++ b/frontend/src/components/SecurityBanners.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; -import { AlertTriangle, ShieldAlert, ArrowUpCircle, X } from 'lucide-react'; -import { authApi, systemApi } from '../services/api'; +import { AlertTriangle, ShieldAlert } from 'lucide-react'; +import { authApi } from '../services/api'; interface SecurityStatus { warnings: string[]; @@ -9,83 +9,34 @@ interface SecurityStatus { export default function SecurityBanners() { const [status, setStatus] = useState(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(() => {}); }, []); const noWarnings = !status || status.warnings.length === 0; - const noUpdate = !updateInfo || updateDismissed; - if (noWarnings && noUpdate) return null; + if (noWarnings) return null; - const hasSecretWarning = !!status && ( - status.warnings.includes('jwt_secret_default') || - status.warnings.includes('encryption_key_default') - ); + const hasSecretsNotPersisted = !!status && status.warnings.includes('secrets_not_persisted'); 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', - ] - .filter(Boolean) - .join(' and '); - return (
- {updateInfo && !updateDismissed && ( -
-
- - - Update available:{' '} - v{updateInfo.latest} is on GitHub.{' '} - - View release - {' '} - and run docker compose pull && docker compose up -d to update. - -
- -
- )} - {hasSecretWarning && ( + {hasSecretsNotPersisted && (
Security warning:{' '} - {secretNames}{' '} - {status.warnings.filter((w) => - ['jwt_secret_default', 'encryption_key_default'].includes(w) - ).length > 1 - ? 'are' - : 'is'}{' '} - using default values. Set strong secrets in your{' '} - .env file before - exposing this to any network. + Auto-generated secrets could not be saved, so they will change on + every restart — logging users out and potentially orphaning stored + device credentials. Mount a writable volume at{' '} + SECRETS_DIR (default{' '} + /app/data) or set{' '} + JWT_SECRET and{' '} + ENCRYPTION_KEY in your{' '} + .env.
)} diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 0e26390..24d26a6 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -11,7 +11,7 @@ import { } from 'lucide-react'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; -import { metricsApi, eventsApi, devicesApi, clientsApi, trafficApi, operationsApi, topologyApi } from '../services/api'; +import { metricsApi, eventsApi, devicesApi, clientsApi, trafficApi, operationsApi, topologyApi, systemApi } from '../services/api'; import type { OpsAttentionItem, OpsCapacityRow, OpsActivityItem } from '../services/api'; import TerminalModal from '../components/TerminalModal'; import { useSocket } from '../hooks/useSocket'; @@ -611,6 +611,14 @@ function OperationsView({ const capacity: OpsCapacityRow[] = insights?.capacity ?? []; const activity: OpsActivityItem[] = insights?.activity ?? []; + // Platform update availability (checked against GitHub, cached server-side) + const { data: versionInfo } = useQuery({ + queryKey: ['version-check'], + queryFn: () => systemApi.versionCheck().then(r => r.data), + staleTime: 60 * 60_000, + refetchOnWindowFocus: false, + }); + // Fleet security posture rollup (live per-device; cached 5 min, lazy) const { data: posture } = useQuery({ queryKey: ['ops-security', onlineDevices.map(d => d.id).join(',')], @@ -724,6 +732,32 @@ function OperationsView({
+ {/* Platform update available */} + {versionInfo?.update_available && versionInfo.latest && ( +
+ +
+
+ Platform update available — v{versionInfo.latest} +
+
+ You're on v{versionInfo.current}. Run{' '} + docker compose pull && docker compose up -d{' '} + to update. +
+
+ + Release notes → + +
+ )} + {/* Things to handle + Quick actions */}
{/* Things to handle */} diff --git a/frontend/src/version.ts b/frontend/src/version.ts index c0ee880..ea61e2f 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1 +1 @@ -export const APP_VERSION = 'v0.16.10 Beta'; +export const APP_VERSION = 'v0.16.11 Beta';