feat: RouterBOOT upgrade support, update UI overhaul, discovery fixes (v0.10.6 Beta)
RouterOS vs RouterBOOT distinction: - Rename "Firmware Updates" → "RouterOS Updates" everywhere (Config tab, dashboard banner, devices list tooltip) to accurately reflect what is being updated - Add RouterBOOT bootloader upgrade support as a distinct operation: new DeviceCollector methods checkRouterboardUpgrade() and installRouterboardUpgrade() using /system/routerboard/print and /system/routerboard/upgrade - New API endpoints: POST /api/devices/:id/check-routerboard and POST /api/devices/:id/install-routerboard - Daily poller now also checks RouterBOOT upgrade status while already connected during firmware check cycle, with first-discovery alerts Config tab update panel: - Combine RouterOS and RouterBOOT into a single "Software & Firmware Updates" card with two subsections separated by a divider - Pre-populate both sections from stored DB state on load — no longer requires a manual "Check" click to see a known pending update - Enforce update order: when both are pending, show a blue info banner explaining RouterOS must be installed first, and disable the RouterBOOT "Upgrade & Reboot" button until the RouterOS update clears - Fix GET /api/devices/:id to include firmware_update_available, latest_ros_version, routerboard_upgrade_available, upgrade_firmware_version (these were missing from the detail endpoint) Dashboard & devices list: - Combined amber banner now shows two distinct rows: RouterOS (N) with device links and RouterBOOT (N) with device links — one dismiss for both - Devices list: amber ArrowUpCircle = RouterOS update, blue Cpu icon = RouterBOOT upgrade, each with distinct tooltips - Fix GET /api/devices (list) to include routerboard_upgrade_available and upgrade_firmware_version (these were missing, causing RouterBOOT notifications to never appear on the dashboard) DB migration: - Add routerboard_upgrade_available BOOLEAN DEFAULT FALSE - Add upgrade_firmware_version VARCHAR(20) Discovery system fixes: - Fix duplicate entries: replace single-key (mac || ip || identity) dedup with three cross-referenced indexes so a neighbor seen via LLDP by one device and via MNDP by another always merges into one row - Fix offline devices persisting: handleDeviceFailure now deletes topology_links for the offline device (same pattern as clients) - Add periodic stale-link sweep every 15 min that removes topology_links rows older than 20 min as a safety net for devices that go offline without cleanly triggering handleDeviceFailure
This commit is contained in:
@@ -5,7 +5,7 @@ import {
|
||||
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
PieChart, Pie, Cell, Legend,
|
||||
} from 'recharts';
|
||||
import { Router, Users, AlertTriangle, Wifi, Activity, TrendingUp, MapPin, ArrowUpCircle, X, Signal } from 'lucide-react';
|
||||
import { Router, Users, AlertTriangle, Wifi, Activity, TrendingUp, MapPin, ArrowUpCircle, Cpu, X, Signal } from 'lucide-react';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { metricsApi, eventsApi, devicesApi, clientsApi } from '../services/api';
|
||||
@@ -274,9 +274,13 @@ export default function DashboardPage() {
|
||||
});
|
||||
|
||||
const [dismissedFirmwareIds, setDismissedFirmwareIds] = useState<number[]>([]);
|
||||
const devicesWithUpdates = devices.filter(
|
||||
const devicesWithRosUpdates = devices.filter(
|
||||
(d) => d.firmware_update_available && !dismissedFirmwareIds.includes(d.id)
|
||||
);
|
||||
const devicesWithRbUpgrades = devices.filter(
|
||||
(d) => d.routerboard_upgrade_available && !dismissedFirmwareIds.includes(d.id)
|
||||
);
|
||||
const showUpdateBanner = devicesWithRosUpdates.length > 0 || devicesWithRbUpgrades.length > 0;
|
||||
|
||||
// Device type distribution for pie chart
|
||||
const deviceTypeLabel: Record<string, string> = {
|
||||
@@ -300,30 +304,63 @@ export default function DashboardPage() {
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Dashboard</h1>
|
||||
|
||||
{/* Firmware update banner */}
|
||||
{devicesWithUpdates.length > 0 && (
|
||||
{/* Updates available banner */}
|
||||
{showUpdateBanner && (
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-700/50 dark:bg-amber-900/20 px-4 py-3 flex items-start gap-3">
|
||||
<ArrowUpCircle className="w-4 h-4 text-amber-500 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1 text-sm text-amber-800 dark:text-amber-300">
|
||||
<span className="font-semibold">Firmware update{devicesWithUpdates.length > 1 ? 's' : ''} available</span>
|
||||
{' — '}
|
||||
{devicesWithUpdates.map((d, i) => (
|
||||
<span key={d.id}>
|
||||
{i > 0 && ', '}
|
||||
<button
|
||||
onClick={() => navigate(`/devices/${d.id}`)}
|
||||
className="underline underline-offset-2 hover:text-amber-900 dark:hover:text-amber-200 font-medium"
|
||||
>
|
||||
{d.name}
|
||||
{d.latest_ros_version && (
|
||||
<span className="font-normal"> ({d.latest_ros_version})</span>
|
||||
)}
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<div className="flex-1 text-sm text-amber-800 dark:text-amber-300 space-y-1">
|
||||
<span className="font-semibold">Updates available</span>
|
||||
{devicesWithRosUpdates.length > 0 && (
|
||||
<div className="flex items-baseline gap-1.5 flex-wrap">
|
||||
<span className="font-medium flex items-center gap-1">
|
||||
<ArrowUpCircle className="w-3 h-3" />
|
||||
RouterOS ({devicesWithRosUpdates.length}):
|
||||
</span>
|
||||
{devicesWithRosUpdates.map((d, i) => (
|
||||
<span key={d.id}>
|
||||
{i > 0 && <span className="text-amber-600 dark:text-amber-500"> · </span>}
|
||||
<button
|
||||
onClick={() => navigate(`/devices/${d.id}?tab=config`)}
|
||||
className="underline underline-offset-2 hover:text-amber-900 dark:hover:text-amber-200"
|
||||
>
|
||||
{d.name}
|
||||
{d.latest_ros_version && (
|
||||
<span className="font-normal opacity-80"> ({d.latest_ros_version})</span>
|
||||
)}
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{devicesWithRbUpgrades.length > 0 && (
|
||||
<div className="flex items-baseline gap-1.5 flex-wrap">
|
||||
<span className="font-medium flex items-center gap-1">
|
||||
<Cpu className="w-3 h-3" />
|
||||
RouterBOOT ({devicesWithRbUpgrades.length}):
|
||||
</span>
|
||||
{devicesWithRbUpgrades.map((d, i) => (
|
||||
<span key={d.id}>
|
||||
{i > 0 && <span className="text-amber-600 dark:text-amber-500"> · </span>}
|
||||
<button
|
||||
onClick={() => navigate(`/devices/${d.id}?tab=config`)}
|
||||
className="underline underline-offset-2 hover:text-amber-900 dark:hover:text-amber-200"
|
||||
>
|
||||
{d.name}
|
||||
{d.upgrade_firmware_version && (
|
||||
<span className="font-normal opacity-80"> ({d.upgrade_firmware_version})</span>
|
||||
)}
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setDismissedFirmwareIds((ids) => [...ids, ...devicesWithUpdates.map((d) => d.id)])}
|
||||
onClick={() => setDismissedFirmwareIds((ids) => [
|
||||
...ids,
|
||||
...devicesWithRosUpdates.map((d) => d.id),
|
||||
...devicesWithRbUpgrades.map((d) => d.id),
|
||||
])}
|
||||
className="p-0.5 rounded hover:bg-amber-200 dark:hover:bg-amber-800/50 text-amber-500 flex-shrink-0"
|
||||
title="Dismiss"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user