diff --git a/README.md b/README.md index 3a1474c..1b62696 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.3_Beta-blue) +![Version](https://img.shields.io/badge/version-0.16.4_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) @@ -178,8 +178,9 @@ Each service supports multi-device management with conflict detection: | **DNS** | Upstream servers, static records (A/AAAA/CNAME/MX/NS/PTR/TXT/SRV), cache flush, DoH | | **NTP** | Server (broadcast/manycast), client (unicast/multicast), sync status | | **WireGuard** | Interface management, peer configuration, public key display, RX/TX stats | -| **Syslog** | Logging actions (remote/memory/disk/echo) and routing rules; single-device or push-to-all with per-entry coverage; enable/disable rules | +| **Logging** | Syslog actions (remote/memory/disk/echo) and routing rules; single-device or push-to-all with per-entry coverage; enable/disable rules | | **NetFlow** | One-toggle Traffic Flow (NetFlow v9/IPFIX) export per device, pointed at the built-in collector; live export status per device | +| **Discovery & SNMP** | Fleet-wide LLDP enable/disable and SNMP v1/v2c/v3 configuration with per-device status tables, scoped to all devices, routers only, or switches only | ### Traffic Analytics (NetFlow) - **Built-in NetFlow/IPFIX collector** — receives Traffic Flow exports from your routers on UDP 2055 (no external tools needed); decoder supports both NetFlow v9 and IPFIX diff --git a/backend/package.json b/backend/package.json index 6f3b437..107afcf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-backend", - "version": "0.16.3-beta", + "version": "0.16.4-beta", "description": "Mikrotik Network Management Platform - Backend API", "main": "dist/index.js", "scripts": { diff --git a/frontend/package.json b/frontend/package.json index e4097a7..68973ce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-frontend", - "version": "0.16.3-beta", + "version": "0.16.4-beta", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ae63220..d4856aa 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,13 +11,8 @@ import TopologyPage from './pages/TopologyPage'; import BackupsPage from './pages/BackupsPage'; import FirmwarePage from './pages/FirmwarePage'; import SettingsPage from './pages/SettingsPage'; -import SwitchesOverviewPage from './pages/SwitchesOverviewPage'; -import SwitchesSettingsPage from './pages/SwitchesSettingsPage'; -import RoutersPage from './pages/RoutersPage'; -import RouterSettingsPage from './pages/RouterSettingsPage'; import WirelessPage from './pages/WirelessPage'; import WirelessSettingsPage from './pages/WirelessSettingsPage'; -import WirelessClientsPage from './pages/WirelessClientsPage'; import GuestWifiPage from './pages/GuestWifiPage'; import ClientDetailPage from './pages/ClientDetailPage'; import NetworkServicesOverviewPage from './pages/NetworkServicesOverviewPage'; @@ -27,6 +22,7 @@ import NetworkServicesNTPPage from './pages/NetworkServicesNTPPage'; import NetworkServicesWireGuardPage from './pages/NetworkServicesWireGuardPage'; import NetworkServicesSyslogPage from './pages/NetworkServicesSyslogPage'; import NetworkServicesNetflowPage from './pages/NetworkServicesNetflowPage'; +import NetworkServicesDiscoveryPage from './pages/NetworkServicesDiscoveryPage'; import TrafficAnalyticsPage from './pages/TrafficAnalyticsPage'; import SecurityPage from './pages/SecurityPage'; @@ -60,13 +56,14 @@ export default function App() { } /> } /> } /> - } /> - } /> - } /> - } /> + {/* Legacy routes — consolidated in the v0.16.4 UI reorganization */} + } /> + } /> + } /> + } /> + } /> } /> } /> - } /> } /> } /> } /> @@ -75,6 +72,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 008461a..49846cd 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -4,49 +4,41 @@ import { useQuery } from '@tanstack/react-query'; import { LayoutDashboard, Router, Users, Bell, GitBranch, HardDrive, Settings, Network, ChevronLeft, ChevronRight, Layers, ChevronDown, SlidersHorizontal, X, Wifi, - Server, Globe, Clock, Shield, FileText, Activity, BarChart3, Ticket, ArrowUpCircle, + Server, Globe, Clock, Shield, FileText, Activity, BarChart3, Ticket, ArrowUpCircle, Radio, + LayoutGrid, } from 'lucide-react'; import clsx from 'clsx'; import { APP_VERSION } from '../../version'; import { devicesApi } from '../../services/api'; -const navItems = [ +const monitorItems = [ { to: '/dashboard', icon: LayoutDashboard, label: 'Dashboard' }, - { to: '/devices', icon: Router, label: 'Devices' }, - { to: '/clients', icon: Users, label: 'Clients' }, - { to: '/traffic', icon: Activity, label: 'Traffic' }, - { to: '/security', icon: Shield, label: 'Security' }, - { to: '/events', icon: Bell, label: 'Events' }, { to: '/topology', icon: GitBranch, label: 'Topology' }, - { to: '/backups', icon: HardDrive, label: 'Backups' }, - { to: '/firmware', icon: ArrowUpCircle, label: 'Firmware' }, -]; - -const switchSubItems = [ - { to: '/switches', icon: Layers, label: 'Overview' }, - { to: '/switches/settings', icon: SlidersHorizontal, label: 'Settings' }, -]; - -const routerSubItems = [ - { to: '/routers', icon: Router, label: 'Overview' }, - { to: '/routers/settings', icon: SlidersHorizontal, label: 'Settings' }, + { to: '/traffic', icon: Activity, label: 'Traffic' }, + { to: '/events', icon: Bell, label: 'Events' }, + { to: '/security', icon: Shield, label: 'Security' }, ]; const wirelessSubItems = [ { to: '/wireless', icon: Wifi, label: 'Overview' }, - { to: '/wireless/clients', icon: Users, label: 'Clients' }, + { to: '/wireless/settings', icon: SlidersHorizontal, label: 'Radios & SSIDs' }, { to: '/wireless/guest', icon: Ticket, label: 'Guest WiFi' }, - { to: '/wireless/settings', icon: SlidersHorizontal, label: 'Settings' }, ]; const networkServicesSubItems = [ - { to: '/network-services', icon: Network, label: 'Overview' }, - { to: '/network-services/dhcp', icon: Server, label: 'DHCP' }, - { to: '/network-services/dns', icon: Globe, label: 'DNS' }, - { to: '/network-services/ntp', icon: Clock, label: 'NTP' }, - { to: '/network-services/wireguard', icon: Shield, label: 'WireGuard' }, - { to: '/network-services/syslog', icon: FileText, label: 'Syslog' }, + { to: '/network-services', icon: Network, label: 'Overview' }, + { to: '/network-services/dhcp', icon: Server, label: 'DHCP' }, + { to: '/network-services/dns', icon: Globe, label: 'DNS' }, + { to: '/network-services/ntp', icon: Clock, label: 'NTP' }, + { to: '/network-services/wireguard', icon: Shield, label: 'WireGuard' }, + { to: '/network-services/syslog', icon: FileText, label: 'Logging' }, { to: '/network-services/netflow', icon: BarChart3, label: 'NetFlow' }, + { to: '/network-services/discovery', icon: Radio, label: 'Discovery & SNMP' }, +]; + +const operationsItems = [ + { to: '/firmware', icon: ArrowUpCircle, label: 'Firmware' }, + { to: '/backups', icon: HardDrive, label: 'Backups' }, ]; interface SidebarProps { @@ -91,6 +83,20 @@ function NavItem({ ); } +function SectionLabel({ label, isCollapsed }: { label: string; isCollapsed: boolean }) { + if (isCollapsed) { + return
; + } + return ( +
+ {label} +
+ ); +} + function GroupHeader({ icon: Icon, label, @@ -119,6 +125,39 @@ function GroupHeader({ ); } +/** Collapsed-sidebar icon link that represents a whole group. */ +function CollapsedGroupLink({ + to, + icon: Icon, + label, + groupActive, + onClick, +}: { + to: string; + icon: React.ElementType; + label: string; + groupActive: boolean; + onClick: () => void; +}) { + return ( + + clsx( + 'hidden md:flex items-center justify-center rounded-[6px] text-[13px] font-medium transition-colors duration-150 px-2 py-[7px] border-l-2', + isActive || groupActive + ? 'bg-surface-3 text-ink border-accent' + : 'text-ink-2 hover:bg-surface-3 hover:text-ink border-transparent' + ) + } + > + + + ); +} + export default function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) { const [collapsed, setCollapsed] = useState(false); const location = useLocation(); @@ -135,21 +174,20 @@ export default function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) { const onlineCount = devices.filter(d => d.status === 'online').length; - const switchesActive = location.pathname.startsWith('/switches'); - const [switchesOpen, setSwitchesOpen] = useState(switchesActive); - const effectiveSwitchesOpen = switchesOpen || switchesActive; - - const routersActive = location.pathname.startsWith('/routers'); - const [routersOpen, setRoutersOpen] = useState(routersActive); - const effectiveRoutersOpen = routersOpen || routersActive; + // Collapsible groups: until the user touches the chevron (pref === null) a + // group is open exactly when its section is active; after that the manual + // choice wins, so collapse always works — even inside the active section. + const devicesActive = location.pathname.startsWith('/devices'); + const [devicesOpenPref, setDevicesOpenPref] = useState(null); + const devicesOpen = devicesOpenPref ?? devicesActive; const wirelessActive = location.pathname.startsWith('/wireless'); - const [wirelessOpen, setWirelessOpen] = useState(wirelessActive); - const effectiveWirelessOpen = wirelessOpen || wirelessActive; + const [wirelessOpenPref, setWirelessOpenPref] = useState(null); + const wirelessOpen = wirelessOpenPref ?? wirelessActive; const networkServicesActive = location.pathname.startsWith('/network-services'); - const [networkServicesOpen, setNetworkServicesOpen] = useState(networkServicesActive); - const effectiveNetworkServicesOpen = networkServicesOpen || networkServicesActive; + const [networkServicesOpenPref, setNetworkServicesOpenPref] = useState(null); + const networkServicesOpen = networkServicesOpenPref ?? networkServicesActive; const isCollapsed = collapsed; @@ -157,6 +195,16 @@ export default function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) { onMobileClose(); }; + // Devices sub-links deep-link into the type-filtered device list. + const deviceTypeLinks = [ + { to: '/devices', icon: LayoutGrid, label: 'All Devices' }, + ...(hasRouters ? [{ to: '/devices?type=RTR', icon: Router, label: 'Routers' }] : []), + ...(hasSwitches ? [{ to: '/devices?type=SW', icon: Layers, label: 'Switches' }] : []), + ...(hasWireless ? [{ to: '/devices?type=AP', icon: Wifi, label: 'Wireless APs' }] : []), + ]; + // With a single device type the sub-links add nothing over the plain entry. + const showDevicesGroup = deviceTypeLinks.length > 2; + return (