Watchtower NOC Dashboard
Overview
Watchtower is a self-hosted Network Operations Center dashboard that provides unified visibility into network infrastructure, virtualization platforms, and system health. It aggregates data from multiple monitoring systems into a single interactive topology visualization with live status updates via WebSocket.
50+ commits, TypeScript 52% / Python 43%
The Numbers: Cost Avoidance
Enterprise monitoring platforms carry significant licensing costs. For a 200-endpoint departmental network, here's what commercial alternatives would cost:
SolarWinds pricing: Vendr Β· Netdata Β· ManageEngine
Architecture
- Cluster nodes (expand/collapse)
- Device status indicators
- Animated link edges
- L2/L3 view toggle
Features
Draggable network map with expandable clusters, device status indicators, and animated connection links
ReactFlow + Zustand + WebSocketVisual switch port layout with real-time status colors and hover tooltips
Custom React componentsScheduled WAN health tests with CSV history and external link coloring
Ookla CLI + Redis cacheAggregate traffic across ports matching a pattern (e.g., all lab ports)
SNMP ifOctets aggregationWebSocket pushes live device status, health metrics, and link changes without polling
FastAPI WebSocket + APSchedulerAutomatic discovery of physical connections via CDP/LLDP, merged with static topology.yaml
LibreNMS API + Redis cacheSwitch between Physical (cable connections) and Logical (VLAN relationships) topology views
ReactFlow layout transformsLive dashboard of VMs, containers, and storage pools for selected hypervisor nodes
Proxmox API integrationFind switch ports by searching SNMP descriptions (ifAlias)
LibreNMS API queryExport topology as Mermaid diagram for documentation with built-in viewer
Frontend Mermaid generatorReal-time alerts from device status changes with acknowledgment
Derived from device stateData Flow Example
How a device status change propagates through the system:
Feature Deep Dive: WebSocket Real-Time Updates
Bidirectional WebSocket connection that pushes device status changes, alert notifications, and speedtest results from the backend to all connected dashboard clients without requiring page refreshes or polling.
NOC operators need immediate notification when device status changes. Instead of waiting 30-60 seconds for the next poll cycle, the WebSocket pushes updates in real-time: a switch going down appears on the dashboard within seconds. This enables faster incident response and reduces mean time to detection (MTTD).
Architecture
active_connections: list[WebSocket] Message Types: Server β Client
Connection Lifecycle
active_connections.append(ws)
active_connections.remove(ws)
Reconnection Strategy
delay = min(1000 * 2^attempts, 30000) Device Hostname Matching
"cat-1.corp.local" "cat-1" Component Structure
URL Construction
const WS_URL = `${
window.location.protocol === 'https:' ? 'wss:' : 'ws:'
}//${window.location.host}/ws/updates` Debug Helper (Browser Console)
window.watchtower in App.tsx:window.watchtower.setDeviceDown('cat-1') Simulate device going down window.watchtower.setDeviceUp('cat-1') Simulate device coming back up window.watchtower.listDevices() List all devices and their status window.watchtower.setSpeedtestDown() Red external links window.watchtower.setSpeedtestDegraded() Yellow external links window.watchtower.setSpeedtestNormal() Green external links window.watchtower.getStore() Get full store state Implementation Details
Feature Deep Dive: Interactive Topology Canvas
The core visualization engine that renders the physical network topology using ReactFlow. Features expandable device clusters, automatic collision detection, drag-and-drop positioning with localStorage persistence, and dynamic edge routing that adapts to expanded/collapsed states.
NOC operators need to see the entire network at a glance while being able to drill down into specific clusters. The canvas provides an interactive map where clusters can be expanded to show individual devices, nodes can be dragged to custom positions (persisted across sessions), and the layout automatically adjusts to prevent overlapping nodes.
Architecture
Visual Layout: Collapsed View
Default view shows clusters as single nodes with device status indicators:
Visual Layout: Expanded View
When a cluster is expanded, individual devices are shown in a grid layout:
Node Types
Collision Detection Algorithm
NODE_SIZES = { cluster: 180Γ120, device: 160Γ80, external: 140Γ100 }PADDING = 40px box = { x: node.x - PADDING, y: node.y - PADDING, width: size + PADDING*2, height: size + PADDING*2 } boxesOverlap(a, b) = !(a.right < b.x || b.right < a.x || a.bottom < b.y || b.bottom < a.y) IF overlapX < overlapY β push horizontally ELSE β push vertically WHILE hasOverlap AND iteration < 50: recalculate, check pairs, apply push Position Persistence
watchtower-node-positions {
"cluster-core": { "x": 450, "y": 150 },
"cluster-servers": { "x": 450, "y": 450 },
"cat-1": { "x": 350, "y": 120 },
"cat-2": { "x": 550, "y": 120 },
"external-Internet": { "x": 80, "y": 150 }
} dragging === false) Event Handlers
Grid Layout for Expanded Devices
cols = min(3, deviceCount), rows = ceil(deviceCount / cols) spacingX = 200px, spacingY = 120px startX = cluster.x - gridWidth / 2 x = startX + (i % cols) * spacingX Edge Routing
Edges dynamically connect based on cluster expansion state:
Edges are deduplicated: only one edge per node pair using sorted key.
Component Hierarchy
Implementation Details
Feature Deep Dive: CDP/LLDP Link Discovery
Automatic discovery of physical network connections by polling CDP (Cisco Discovery Protocol) and LLDP (Link Layer Discovery Protocol) neighbor data from LibreNMS. Discovered links are merged with static topology.yaml connections to create a complete network map.
Manually documenting every cable connection in a network is tedious and error-prone. CDP/LLDP discovery automates this: switches advertise their neighbors, LibreNMS collects this data via SNMP, and Watchtower polls it to build the topology graph automatically. Static connections in topology.yaml fill gaps for devices that don't support CDP/LLDP (firewalls, servers, APs).
Architecture
/api/v0/resources/links [{
"id": 123,
"local_device_id": 42,
"local_port": "TenGigabitEthernet1/0/1",
"remote_device_id": 43,
"remote_hostname": "cat-2.corp.local",
"remote_port": "TenGigabitEthernet1/0/1",
"protocol": "cdp"
}] topo_to_librenms: {"cat-1": 42, "cat-2": 43} Data Models
class LibreNMSLink(BaseModel):
id: int | None
local_device_id: int
local_port_id: int | None
local_port: str | None
remote_hostname: str | None
remote_port: str | None
remote_device_id: int | None
protocol: str | None # "cdp", "lldp" class Connection(BaseModel):
id: str # "cdp-cat-1-Te1-0-1"
source: ConnectionEndpoint
target: ConnectionEndpoint
connection_type: ConnectionType
speed: int # Mbps
status: ConnectionStatus
utilization: float | None Port Name Resolution
/resources/links returns local_port_id (integer)
but NOT the actual port name like "TenGigabitEthernet1/0/1"
{"port": [{...}]} (list inside "port" key)
Deduplication Logic
seen_ports: set[tuple[str, str]] = set()
# When processing CDP/LLDP link:
if local_port:
seen_ports.add((source_topo_id, local_port))
if remote_port:
seen_ports.add((target_topo_id, remote_port))
# When processing static connection:
if (source_device, source_port) in seen_ports:
continue # Skip - already discovered Static Connection Merging
connections:
- id: "fw-to-core"
source:
device: pa-3410
port: ethernet1/1
target:
device: cat-1
port: TenGigabitEthernet1/0/48
connection_type: uplink
speed: 10000 trunk Inter-switch links (CDP/LLDP default) access End-device connections uplink Upstream connections stack Stacking cables peer HA/cluster peer links management Management network Connection Status Derivation
source_device = devices.get(source_topo_id)
target_device = devices.get(target_topo_id)
IF source.status == DOWN OR target.status == DOWN:
conn_status = DOWN
ELIF source.status == UNKNOWN OR target.status == UNKNOWN:
conn_status = UNKNOWN
ELSE:
conn_status = UP Component Structure
LibreNMS API Endpoints
{
"links": [{
"id": 123,
"local_device_id": 42,
"local_port_id": 1001,
"remote_device_id": 43,
"remote_hostname": "cat-2.corp.local",
"remote_port": "Te1/0/1",
"protocol": "cdp"
}]
} {
"port": [{
"port_id": 1001,
"device_id": 42,
"ifName": "TenGigabitEthernet1/0/1",
"ifDescr": "TenGigabitEthernet1/0/1",
"ifAlias": "Uplink to cat-2",
"ifOperStatus": "up"
}]
} Implementation Details
Limitations
Feature Deep Dive: Speedtest Widget
This feature monitors internet connectivity health by running periodic Ookla speed tests and displaying results in a real-time dashboard widget. It also affects the visual status of external WAN links on the topology map.
Provides at-a-glance WAN health visibility for NOC operators. Instead of manually running speed tests when users report "slow internet," the dashboard continuously monitors and logs connection quality. Historical CSV data enables capacity planning and ISP SLA verification.
How It Works
speedtest:
enabled: true
interval_minutes: 5 # Run test every 5 minutes
server_id: null # null = auto-select closest
interface: "eth0" # Specific NIC (optional)
thresholds:
degraded_download_mbps: 200 # Yellow if below
degraded_ping_ms: 50 # Yellow if above
down_download_mbps: 10 # Red if below
logging:
enabled: true
path: "/opt/watchtower/data/speedtest.csv" poll_speedtest() job runs at configured intervalrun_speedtest() async function/usr/local/bin/speedtest --format=json --accept-license --accept-gdprGET /api/speedtest Returns cached result with status indicator POST /api/speedtest/trigger Manual test trigger (60-second cooldown enforced) GET /api/speedtest/export Download CSV history file {
"timestamp": "2026-01-27T14:30:00Z",
"download_mbps": 487.23,
"upload_mbps": 52.41,
"ping_ms": 12.3,
"jitter_ms": 2.1,
"packet_loss_pct": 0,
"server_id": 12345,
"server_name": "Spectrum",
"server_location": "Tampa, FL",
"result_url": "https://www.speedtest.net/result/xxxxx",
"status": "success",
"indicator": "normal" // Derived from thresholds
} Status Indicator Logic
The widget displays a colored status dot based on configurable thresholds:
normal β Green Download β₯ 200 Mbps AND ping β€ 50ms degraded β Yellow Download < 200 Mbps OR ping > 50ms down β Red Download < 10 Mbps OR test error def get_status(result, thresholds):
if result.status.startswith("error"):
return "down"
if download < 10: # Critical threshold
return "down"
if download < 200 or ping > 50: # Warning thresholds
return "degraded"
return "normal" External Link Coloring
The speedtest status also affects the topology visualization:
Data Flow
CSV Log Format
timestamp,download_mbps,upload_mbps,ping_ms,jitter_ms,packet_loss_pct,server_id,server_name,server_location,result_url,status
2026-01-27T14:30:00Z,487.23,52.41,12.3,2.1,0,12345,Spectrum,"Tampa, FL",https://speedtest.net/result/...,success
2026-01-27T14:35:00Z,491.87,51.22,11.8,1.9,0,12345,Spectrum,"Tampa, FL",https://speedtest.net/result/...,success Technical Details
Why This Approach
Feature Deep Dive: Port Group Monitoring
This feature aggregates real-time traffic across multiple switch ports that share a common SNMP description pattern. Built to monitor computer labs where dozens of workstations connect through various switches but need to be viewed as a single logical group.
The Digital Media Mac Pro Lab has ~140 workstation ports spread across multiple access switches. Each port has an SNMP description (ifAlias) containing "digital-media" (the department identifier). Instead of monitoring 140 individual ports, this feature sums all their traffic into a single dashboard widget showing aggregate bandwidth consumption.
How It Works
port_groups:
- name: "Digital Media Labs"
description: "Digital Media Mac Pro Lab"
match_alias: "digital-media" # Pattern to match
thresholds:
warning_mbps: 500 # Yellow threshold
critical_mbps: 800 # Red threshold
logging:
enabled: true
path: "/opt/watchtower/data/port_groups.csv" {
"name": "Digital Media Labs",
"description": "Digital Media Mac Pro Lab",
"port_count": 140, // Total matching ports
"active_port_count": 87, // Ports currently up
"in_mbps": 360.0, // Inbound Mbps
"out_mbps": 96.0, // Outbound Mbps
"total_mbps": 456.0, // Combined
"status": "ok", // Based on thresholds
"thresholds": { "warning_mbps": 500, "critical_mbps": 800 }
} Technical Details
Why This Approach
Feature Deep Dive: Diagnosing Slowdowns
When users report "the internet is slow," the question is: is it the ISP or something internal? Both the Speedtest Widget and Port Group Monitoring export CSV logs. Comparing these two data sources pinpoints whether the bottleneck is upstream (ISP) or within the campus network.
The Diagnostic Question
WAN link is degraded but internal network has capacity. Problem is with the upstream provider.
ISP link is healthy but lab ports are saturated. Congestion is on the access or distribution layer.
How It Works
/opt/watchtower/data/speedtest.csv /opt/watchtower/data/port_groups.csv Example Analysis
timestamp,download_mbps,upload_mbps,latency_ms
2026-01-27T14:00:00,850.2,410.5,12
2026-01-27T14:15:00,180.4,95.2,45 β degraded
2026-01-27T14:30:00,175.8,88.1,52 β degraded
2026-01-27T14:45:00,820.1,405.3,14 timestamp,group,active_ports,in_mbps,out_mbps,total_mbps
2026-01-27T14:00:00,Digital Media Labs,87,360.0,96.0,456.0
2026-01-27T14:15:00,Digital Media Labs,85,340.2,88.4,428.6
2026-01-27T14:30:00,Digital Media Labs,82,310.5,82.1,392.6
2026-01-27T14:45:00,Digital Media Labs,88,375.2,102.3,477.5 At 14:15 and 14:30, speedtest dropped from ~850 Mbps to ~180 Mbps while lab traffic stayed consistent around 400-430 Mbps. The labs weren't saturating anything; the WAN link was degraded. Time to call the ISP.
Why This Matters
Feature Deep Dive: Port Search
This feature allows NOC operators to quickly find switch ports across the entire network by searching SNMP port descriptions. Instead of logging into individual switches, users can search for devices like "printer", "podium", or "voip" and instantly see all matching ports with their status and traffic.
Network admins label switch ports with descriptions (SNMP ifAlias) like "Conference Room Podium" or "Break Room Printer". When troubleshooting, they need to quickly find which switch and port a device is connected to. This feature searches all ports across all managed switches in seconds.
How It Works
{
"query": "printer",
"total": 12,
"ports": [
{
"port_id": 4521,
"device_id": 15,
"device_hostname": "sw-bldg1-fl2",
"ifName": "Gi1/0/24",
"ifAlias": "Front Office Printer",
"ifDescr": "GigabitEthernet1/0/24",
"ifSpeed": 1000000000,
"ifOperStatus": "up",
"ifAdminStatus": "up",
"in_mbps": 0.12,
"out_mbps": 0.45
}
// (more results)
]
} Bonus: Alias Discovery
The /api/ports/aliases endpoint helps users discover what search terms exist in the network:
GET /api/ports/aliases Extracts words from all ifAlias fields, counts frequency (skips words <3 chars and pure numbers), returns top 50 sorted by count. Helps users discover what types of devices are labeled without guessing.
Technical Details
UI States
Speed Formatting
10G+ β "10G"
1G β "1G"
100M β "100M"
10M β "10M"
else β "{x}M" Why This Approach
Feature Deep Dive: Cisco Port Grid
This feature renders a visual representation of physical switch ports that mirrors the actual front panel of a Cisco Catalyst switch: odd ports (1, 3, 5...) on top and even ports (2, 4, 6...) on the bottom, grouped into banks of 24 and organized by line card/module.
Network engineers standing at a rack can immediately correlate dashboard status with physical ports. Port 23 is always top-right of a bank, port 48 is always bottom-right. This layout matches what they see on the actual Cisco Catalyst hardware, eliminating mental translation.
Data Flow
GET /api/v0/devices/{id}/ports
Returns array of interfaces:
[
{ name: "Gi1/0/1", ifOperStatus: "up", ifSpeed: 1000000000 },
{ name: "Gi1/0/2", ifOperStatus: "down" },
{ name: "Te1/1/1", ifOperStatus: "up", ifSpeed: 10000000000 }
] Input: "Gi1/0/24"
ββ β β βββ Port number (24)
ββ β βββββ Module/slot (0)
ββ βββββββ Stack number (1)
ββββββββββ Type prefix (Gi = Gigabit)
ββββββββββ Interface type
Output: {
type: 'gigabit',
typePrefix: 'Gi',
stack: 1,
module: 0,
port: 24,
sortKey: 10024, // (1 Γ 10000) + (0 Γ 100) + 24
isUplink: false,
original: 'Gi1/0/24'
} {
modules: [
{
moduleId: 0,
label: "Slot 0",
banks: [
{ label: "1-24", oddPorts: [12 ports], evenPorts: [12 ports] },
{ label: "25-48", oddPorts: [12 ports], evenPorts: [12 ports] }
]
}
],
uplinks: [GridPort array], // 10G+ SFP ports
other: [Interface array] // VLANs, mgmt, loopback
} Interface Type Support
Hu HundredGigabitEthernet 100G Yes Fo FortyGigabitEthernet 40G Yes Twe TwentyFiveGigE 25G Yes Te TenGigabitEthernet 10G Yes Gi GigabitEthernet 1G No Fa FastEthernet 100M No Visual Rendering
Port Detail Panel
Clicking a port opens an expanded detail view:
Component Hierarchy
Technical Details
Why This Layout
Feature Deep Dive: Proxmox Panel
When clicking a Proxmox hypervisor node in the topology, this panel displays a detailed dashboard showing the node's health, all VMs and containers with live CPU/RAM metrics, and storage pool utilization with color-coded capacity warnings.
NOC operators need quick visibility into what's running on each Proxmox node without logging into the Proxmox web UI. This panel shows running/stopped VMs, resource consumption, and storage capacity in the same sidebar used for network device details.
Data Flow
{
"node": {
"node": "proxmox1", "status": "online",
"cpu": 23.5, "memory": 67.2,
"maxcpu": 32, "maxmem": 137438953472
},
"vms": [
{ "vmid": 100, "name": "dc1", "status": "running",
"cpu": 2.3, "memory": 45.0 }
],
"lxcs": [
{ "vmid": 200, "name": "librenms", "status": "running",
"cpu": 5.1, "memory": 32.0 }
],
"storage": [
{ "storage": "local-zfs", "type": "zfspool",
"used": 500000000000, "total": 2000000000000,
"used_percent": 25.0 }
],
"vms_running": 8, "vms_total": 10,
"lxcs_running": 12, "lxcs_total": 12
} Visual Layout
Data Sources
GET /api2/json/nodes β Node health (cached in CACHE_PROXMOX) GET /api2/json/nodes/{node}/qemu β VMs per node (cached in CACHE_PROXMOX_VMS) GET /api2/json/nodes/{node}/lxc β Containers per node (cached) GET /api2/json/nodes/{node}/storage β Storage pools (fetched on-demand) Node Matching Logic
The backend uses multiple strategies to match clicked nodes flexibly:
Technical Details
API Endpoints
GET /api/vms All running VMs/LXCs with summary stats GET /api/vms/summary Just the counts (total_running, total_qemu, total_lxc) GET /api/vms/node/{node_name} Full node detail with VMs, LXCs, storage (used by panel) Feature Deep Dive: L2/L3 Topology Toggle
Toggles between two fundamentally different network visualizations: L2 Physical View shows CDP/LLDP discovered switch connections and device clusters, while L3 Logical View groups devices by VLAN membership with gateway routing relationships.
Network engineers need to see the network from two perspectives. The Physical (L2) view shows "what's plugged into what": actual cable connections discovered via CDP/LLDP. The Logical (L3) view shows "what talks to what": devices grouped by their VLAN assignments with inter-VLAN gateways highlighted.
Architecture
- ClusterNode for each cluster
- DeviceNode when expanded
- ExternalNode for WAN links
- PhysicalLinkEdge for CDP/LLDP
- Load VLANs from Redis
- Map LibreNMS IDs β topology IDs
- Group devices by VLAN
- Identify gateway devices
Visual Layout
VlanGroupNode Component
Gateway Detection Algorithm
For each VLAN membership: device_vlans[device_id].add(vlan_id) IF len(device_vlans[device]) > 1: gateway_devices.append(device) device.is_gateway = device_id in gateway_devices L3 Edge Connection Algorithm
gatewayToVlans[gateway_device].append(vlan_id) For each pair (vlan_i, vlan_j): Create edge vlan-i β vlan-j Data Models
class Vlan:
vlan_id: int # e.g., 10, 20, 100
vlan_name: str | None # e.g., "Management"
device_count: int
class VlanMembership:
device_id: str # Topology device ID
librenms_device_id: int
port_name: str | None # e.g., "Gi1/0/24"
vlan_id: int
is_untagged: bool
class L3TopologyNode:
device_id: str
display_name: str
status: str # "up", "down", "unknown"
is_gateway: bool # True if multi-VLAN
vlan_ids: list[int]
class L3TopologyVlanGroup:
vlan_id: int
vlan_name: str | None
devices: list[L3TopologyNode]
gateway_devices: list[str] type ViewMode = 'l2' | 'l3'
interface L3Topology {
vlans: Vlan[]
memberships: VlanMembership[]
vlan_groups: L3TopologyVlanGroup[]
gateway_devices: string[]
}
// Zustand Store (nocStore.ts)
interface NocState {
viewMode: ViewMode
l3Topology: L3Topology | null
selectedVlans: Set<number>
setViewMode: (mode: ViewMode) => void
setL3Topology: (t: L3Topology | null) => void
toggleVlanFilter: (id: number) => void
clearVlanFilter: () => void
} Data Flow
GET /resources/vlans Returns all VLANs with device_id mappings {
"vlans": [{ "vlan_id": 10, "vlan_name": "Management", "device_count": 8 }],
"memberships": [{ "device_id": "cat-1", "vlan_id": 10, "port_name": "Gi1/0/1" }],
"vlan_groups": [{
"vlan_id": 10,
"vlan_name": "Management",
"devices": [{ "device_id": "cat-1", "is_gateway": true, "status": "up" }],
"gateway_devices": ["cat-1", "cat-2"]
}],
"gateway_devices": ["cat-1", "cat-2", "cat-3"]
} Component Hierarchy
Technical Details
UI States
Tech Stack
Feature Deep Dive: Mermaid Topology Export
Generates a Mermaid flowchart diagram from the live network topology data, allowing users to either
view an interactive visualization in a modal or download a .mmd file for use in
documentation, wikis, or presentations.
NOC operators and network engineers need to document or share the network topology without giving direct dashboard access. The Mermaid export creates portable diagram code that renders in GitHub READMEs, Confluence, Notion, Obsidian, and any Mermaid-compatible tool. The visualize modal provides immediate in-app viewing with pan/zoom controls.
Architecture
- Get topology from store
- generateMermaidDiagram(topology)
- setMermaidDiagram(content)
- setShowMermaidModal(true)
- mermaid.render('mermaid-diagram', diagram)
- Parse SVG output
- Inject into container
- Pan/zoom controls
- Get topology from store
- generateMermaidDiagram(topology)
- downloadFile(content, filename)
Visual Layout
Mermaid Generation Algorithm
Topology object from store: { clusters, devices, connections, external_links } lines = ['flowchart TB'] subgraph {clusterId}["{clusterName}"] {externalId}(("{label}")) // circle/stadium shape A ---|"Gi0/1 β Gi0/2"| B β’ Without ports: A --- B {sourceId} -.-> {targetId} // dashed arrow lines.join('\\n') Generated Mermaid Syntax
flowchart TB
subgraph core_network["Core Network"]
cat_1{{"Core Switch 1<br/><small>10.2.10.1</small>"}}
cat_2{{"Core Switch 2<br/><small>10.2.10.2</small>"}}
end
subgraph firewalls["Firewalls"]
pa_3410[/"PA-3410<br/><small>10.2.60.3</small>"\]
end
ext_Internet(("Internet"))
cat_1 ---|"Gi0/1 β Gi0/1"| cat_2
cat_1 ---|"Te1/0/1 β eth1"| pa_3410
pa_3410 -.-> ext_Internet Device Shape Mapping
{{"label"}} ⬑ [/"label"\] β’ ["label"] β (("label")) β Connection Style Mapping
A --- B ββ (solid) A ---|"Gi0/1 β Gi0/2"| B ββ with label A -.-> B ββ (dashed arrow) ID Sanitization
function sanitizeMermaidId(id: string): string {
// Mermaid only allows alphanumeric and dashes
// Replace all other characters with underscore
return id.replace(/[^a-zA-Z0-9-]/g, '_')
}
// Examples:
// "cat-1.corp.local" β "cat_1_corp_local"
// "Core Switch #1" β "Core_Switch__1"
// "10.2.10.1" β "10_2_10_1" MermaidModal Component
Download Function
function downloadFile(content: string, filename: string) {
// 1. Create Blob from content
const blob = new Blob([content], { type: 'text/plain' })
// 2. Create object URL
const url = URL.createObjectURL(blob)
// 3. Create temporary anchor element
const a = document.createElement('a')
a.href = url
a.download = filename // e.g., "topology-2026-01-27.mmd"
// 4. Trigger download
document.body.appendChild(a)
a.click()
// 5. Cleanup
document.body.removeChild(a)
URL.revokeObjectURL(url)
} Mermaid Theme Configuration
primaryColor: '#3b82f6' Nodes primaryTextColor: '#e5e7eb' Text lineColor: '#6b7280' Edges background: '#0d1117' GitHub dark clusterBkg: '#161b22' Subgraphs htmlLabels: true Allow <br/>, <small>curve: 'basis' Smooth edgesnodeSpacing: 50rankSpacing: 80Technical Details
UI States
Tech Stack
File Output Format
flowchart TB
subgraph core_network["Core Network"]
cat_1{{"Core Switch 1<br/><small>10.2.10.1</small>"}}
cat_2{{"Core Switch 2<br/><small>10.2.10.2</small>"}}
end
subgraph access_layer["Access Layer"]
sw_bldg1{{"Building 1 Switch<br/><small>10.2.20.1</small>"}}
end
ext_Internet(("Internet"))
cat_1 ---|"Te1/0/1 β Te1/0/1"| cat_2
cat_1 --- sw_bldg1
cat_1 -.-> ext_Internet Feature Deep Dive: Alert System
Real-time alerting system that derives alerts from device status and LibreNMS monitoring data. Features a multi-tier notification approach: bell icon with count badge, toast notifications, and full-screen critical overlay for urgent incidents requiring immediate attention.
NOC operators need immediate visibility when network devices go down. The alert system provides escalating notification levels: a subtle count badge for awareness, toast popups for new events, and an unmissable full-screen overlay with audio for critical outages. Alerts auto-clear when devices recover, with optional acknowledgment to track incident response.
Architecture
_get_device_down_alerts() _get_librenms_alerts() _acknowledged_alerts: set[str] Visual Layout
Data Models
class AlertSeverity(str, Enum):
CRITICAL = "critical" # Device down
WARNING = "warning" # Degraded
INFO = "info" # Informational
RECOVERY = "recovery" # Device back up
class AlertStatus(str, Enum):
ACTIVE = "active"
ACKNOWLEDGED = "acknowledged"
RESOLVED = "resolved"
class Alert(BaseModel):
id: str # "device-down-core-sw-1"
device_id: str # Topology device ID
severity: AlertSeverity
message: str
details: str | None
status: AlertStatus
timestamp: datetime
acknowledged_at: datetime | None
acknowledged_by: str | None interface Alert {
id: string
device_id: string
severity: 'critical' | 'warning' | 'info' | 'recovery'
message: string
timestamp: string
status: 'active' | 'acknowledged' | 'resolved'
}
interface Toast {
id: string // "toast-{alertId}-{timestamp}"
alert: Alert
dismissed: boolean
}
// alertStore.ts
interface AlertState {
alerts: Alert[]
toasts: Toast[]
criticalOverlay: Alert | null
// + action methods
} Alert Generation Flow
id: "device-down-{device_id}" message: "Device unreachable: {name}" Acknowledgment System
Component Hierarchy
Alert Lifecycle
Severity Styling
Critical Overlay Features
API Endpoints
GET /api/alerts List all alerts (sorted by time) GET /api/alerts?status=active Filter by status GET /api/alert/{id} Get single alert details POST /api/alert/{id}/acknowledge Mark as acknowledged POST /api/alert/{id}/resolve Mark as resolved Technical Details
Roadmap: Remote Notifications
Remote notification support to alert operators even when away from the dashboard. Critical alerts will trigger notifications through multiple channels for redundancy.
Tech Stack
Key Technical Decisions
API Surface
/api/topology /api/topology/l3 /api/alerts /api/vms /api/vms/node/{name} /api/speedtest /api/port-groups /api/port-groups/export /api/ports/search /api/ports/aliases /ws/topology Pushes on cache changes Development Roadmap
- FastAPI backend
- React frontend
- Basic topology
- L2/L3 toggle
- VLAN filtering
- Cluster nodes
- LibreNMS polling
- Proxmox panel
- Netdisco data
- CDP/LLDP links
- Dynamic topology
- Collision detection
- Speedtest
- Port groups
- Port search
- Mermaid export
- CSV logging
- Collapsible widgets
- JWT auth
- User sessions
- Role-based access
- Config editor
- Polling controls
- Theme options
- Discord webhooks
- Email alerts
- Escalation rules
Current Status
- β FastAPI backend with Redis caching
- β React frontend with ReactFlow topology
- β LibreNMS + Netdisco + Proxmox integrations
- β WebSocket real-time updates
- β CDP/LLDP auto-discovery
- β L2/L3 topology view toggle
- β Cisco-style port grid visualization
- β Speedtest widget with CSV export
- β Port group traffic monitoring
- β Mermaid diagram export
- β JWT authentication
- β Settings UI