diff --git a/README.md b/README.md
index 658780f..57357e3 100644
--- a/README.md
+++ b/README.md
@@ -1,186 +1,78 @@
-# MikroTik Manager
+
MikroTik Manager
-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.
+
+ A self-hosted control plane for your entire MikroTik fleet.
+ Monitor, configure, and safely change routers, switches, and access points — from one web interface.
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
---
-## Screenshots
+## Contents
-
-
-
-
-
-
-### Dashboard
-
-
-
-
-
-### Device Management
-
-
-
-
- 
- Device List
- |
-
- 
- Device Overview
- |
-
-
-
- 
- Switch Ports & Throughput
- |
-
- 
- Hardware Monitor
- |
-
-
-
-### Wireless
-
-
-
-
-
-### Client Tracking
-
-
-
-
- 
- Client List
- |
-
- 
- Client Detail View
- |
-
-
-
-### Network Topology
-
-
-
-
-
-### Events & Backups
-
-
-
-
- 
- Event Log
- |
-
- 
- Backup Management
- |
-
-
+- [What it is](#what-it-is)
+- [What makes it different](#what-makes-it-different)
+- [Screenshots](#screenshots)
+- [Get running in five minutes](#get-running-in-five-minutes)
+- [Features](#features)
+ - [Safety and change protection](#safety-and-change-protection)
+ - [Monitoring and operations](#monitoring-and-operations)
+ - [Switching, VLANs and wireless](#switching-vlans-and-wireless)
+ - [Firewall and network services](#firewall-and-network-services)
+ - [Visibility and troubleshooting](#visibility-and-troubleshooting)
+ - [Fleet operations](#fleet-operations)
+ - [Access control and platform](#access-control-and-platform)
+- [Reference](#reference)
+- [Contributing](#contributing)
+- [License](#license)
---
-## Features
+## What it is
-### Dashboard
-- Live KPI cards: total devices, online/offline count, connected wireless clients, active alerts, fleet-wide 30-day availability %
-- Device type distribution chart
-- Firmware update notifications with per-device details
-- Historical client count graph (1h → 30d range)
-- **Operations view** — a second dashboard mode focused on running the network:
- - **Things to handle** — a server-side insights engine that surfaces actionable issues across the fleet: offline devices, firmware/RouterBOOT updates, CPU/memory pressure, devices missing recent backups, connectivity flapping, and WiFi quality problems (weak-signal clients, co-channel overlap, high TX-retry radios), severity-ordered
- - **Quick actions that actually run** — Run discovery, Back up all online devices, Sync config (pull `/export` + re-collect), and Open terminal (device picker → live in-browser SSH terminal), each with inline progress/result
- - **Capacity & health** — per-device CPU/memory meters sorted by pressure
- - **Security posture rollup** — fleet average hardening score, high-severity finding count, and the lowest-scoring devices, deep-linking to the Security Center
- - **Recent activity feed** — merged config changes, user actions, and alerts
- - **Anomaly insights** (Mist-style) — compares each device's last 30 minutes of client-count and CPU against its **same-hour-of-day 14-day baseline** and flags ≥2.5σ deviations, plus error-log burst detection — surfaced alongside rogue-AP alerts in Things to handle
+MikroTik Manager gives a whole fleet of RouterOS devices a single management surface: live monitoring,
+client tracking, VLAN and wireless configuration, firewall management, firmware rollouts, backups, and
+traffic analytics — without logging into WinBox on each box in turn.
-### Device Management
-- Add, edit, and delete MikroTik devices (routers, switches, wireless APs)
-- Automatic polling: status, model, firmware version, RouterOS version
-- Firmware update availability detection
-- Per-device notes, rack location, and physical address with map support
-- Device credential encryption at rest
-- **Encrypted API (api-ssl)** — devices whose API runs on the SSL port (8729) are reached over TLS automatically; RouterOS's default self-signed certificates are accepted, so the credentials and session are encrypted in transit without any manual certificate trust
-- **Bulk device add** — "Try All" discovered devices runs as a server-side background job (survives browser tab close) with live progress and cancel support
-- CPU load and historical sparkline displayed correctly for all device types, including hardware switches that report 0% CPU via ASIC offloading
-- **Device tags** — colored labels for organizing and filtering devices; full tag management in Settings
-- **30-day availability tracking** — per-device uptime %, outage count, and longest outage duration recorded automatically; visible on the device Overview tab and the fleet dashboard
+It runs entirely on your own hardware via Docker Compose, talks to devices over the RouterOS API (and
+SSH where RouterOS requires it), and stores nothing outside your network.
-### Routers
-- Routing table viewer
-- Interface overview with IP assignments
-- Router-specific settings and configuration
+---
-### Firewall & Security
-A Meraki/UniFi-grade firewall experience built on the full RouterOS feature set:
-- **Dedicated Security Center** — a top-level section with fleet-wide hardening scores, a per-device posture list, and a "Common Findings" rollup that aggregates identical issues across every device; "Manage" deep-links into a device's Security tab. Firewall/NAT, bandwidth, and connections are available on every managed RouterOS device (routers, switches, and APs)
-- **Friendly rule builder** — Allow/Drop/Reject action chips, Any/Address/Address-List source & destination pickers, well-known port presets (HTTPS/HTTP/SSH/DNS/RDP/SMB), connection-state chips, per-rule logging, and a live plain-English preview of every rule
-- **Address lists as reusable objects** — define `LAN`, `Trusted`, `Blocklist` etc. once and reference them from any rule (the Meraki/UniFi "groups" concept)
-- **Rule reordering** — move rules up/down (order is decisive in RouterOS) with a single click
-- **Hit counters & dead-rule insight** — per-rule packet/byte counters surface which rules are matching and flag zero-hit rules; one-click counter reset
-- **NAT wizards** — guided Port Forward, Masquerade (internet sharing), and 1:1 NAT flows instead of raw fields, plus a Custom mode
-- **Safe-apply lockout guard** — refuses to apply an unscoped `input`-chain drop/reject that would lock the platform/admin out of the device, requiring explicit confirmation
-- **Security posture audit** — per-device hardening checklist (insecure services like telnet/ftp/www/api, missing input-chain firewall, SNMP exposure, outdated RouterOS/RouterBOOT firmware) with a score and one-click remediation, plus a management-services table
-- **Bandwidth control** — simple-queue management with up/down caps per IP, subnet, or interface, and a one-click "Limit this client" action on the client detail page
-- **Active connections viewer** — live connection-tracking table (source/destination, protocol, state, rate, bytes) with search
+## What makes it different
-### Lockout Protection & Config Health
-RouterOS applies every command immediately and independently. There is no transaction and no
-cross-object validation, so it will accept a change that severs its own management path — MikroTik's
-documented advice for enabling VLAN filtering is literally to have a serial console ready. Three
-layers close that gap:
+Plenty of tools can *show* you a MikroTik fleet. The hard part is **changing** one safely.
-- **Change Guard (device-side auto-revert)** — before a risky change, the device saves a restore
- point and arms a scheduler to reapply it. The change is applied, then reachability is proven on a
- brand-new connection. Confirmed → the scheduler is disarmed; unreachable → the device restores
- itself and comes back on its own. All over the RouterOS API, so it works without SSH credentials.
- Covers 12 change types: bridge VLAN filtering, port PVID/tagging, bridge VLAN add/update/delete,
- IP address add/remove, route add/remove, bond create/delete, and management service toggles
-- **Lockout prediction** — rather than blocklisting "dangerous" operations (which only catches what
- someone thought of), the platform reads live device state, resolves the actual management path,
- simulates the proposed change against it, and reports any invariant that flips from satisfied to
- violated. The ingress port is taken from the bridge forwarding table and the manager's own address
- from the device's connection tracking, so the warning names the real mechanism: *"management
- arrives untagged on sfp28-1 (PVID 1) — the gateway's MAC is learned there — but VLAN 1 has no
- bridge VLAN entry listing bridge1 as an untagged member."* A predicted lockout is refused with a
- 409 and an explanation of the hop chain; overriding a critical verdict requires typing the device
- name, and the change then runs under Change Guard anyway
-- **Config Health (standing audit)** — a scheduled, read-only audit for configurations RouterOS
- accepted and will never complain about, encoding MikroTik's documented Layer2 misconfigurations:
- an IP address or VLAN interface on a bridge slave port, a VLAN interface added as a bridge port,
- a bond slave that is also a bridge port, MTU above L2MTU, a multi-VLAN bridge entry with untagged
- ports, several bridges competing for hardware offload, a port belonging to no VLAN on a filtering
- bridge, a PVID that `frame-type=admit-only-vlan-tagged` makes inert, and management that survives
- only on a dynamic VLAN entry. Each finding explains what it actually does to the network, how to
- fix it, how long it has been present, and links the MikroTik documentation. Findings appear on the
- device's Security tab and in the dashboard's "Things to handle"
+### RouterOS will happily lock you out of your own device
-- **Capability probe** — `POST /api/devices/:id/change-guard/probe` reports what safety mechanisms a
- device actually supports, tested on the device rather than assumed. Non-destructive: it creates and
- removes a harmless scheduler and leaves nothing behind. It also establishes whether `/system history`
- can revert a change without a reboot — on RouterOS 7.23 over the binary API, `/system/history/print`
- works and a bare `/undo` reverts the newest action, while `/system/history/undo` and `/undo numbers=`
- do not exist. Undo is therefore untargeted, and it cannot replace auto-revert (a dead-man switch has
- to fire once the manager has already lost contact), so binary restore remains the default
+RouterOS applies every command immediately and independently. There is no transaction, no rollback,
+and no cross-object validation — so it will accept a change that severs the very path you are managing
+it over. MikroTik's own documented advice for enabling VLAN filtering is, literally, to have a serial
+console ready.
-#### What you see when auto-revert fires
+That is the problem this project takes seriously. Three layers address it, and they are the reason
+this platform exists rather than another dashboard.
-Verified end to end on a CRS running RouterOS 7.23.3, by deliberately deleting a switch's own
-management address and letting the safety net recover it unattended:
+### 🛡️ Change Guard — the device rescues itself
+
+Before applying a risky change, the device saves a restore point and arms a scheduler to reapply it.
+The change goes in, then reachability is proven **on a brand-new connection**. Confirmed, and the
+scheduler is disarmed. Unreachable, and the device restores itself and comes back — unattended, with
+nobody driving to the site.
+
+Verified end to end on a CRS running RouterOS 7.23.3 by deliberately deleting a switch's own
+management address:
```
T+0 restore point saved, revert scheduler armed, change applied
@@ -190,485 +82,450 @@ T+120s device restores its own backup and reboots
T+3m device is back, change undone, scheduler and restore point gone
```
-The request returns **HTTP 200** with `guard.auto_reverting: true` and *"Contact with the device was
-lost while applying this change. It is restoring itself and should come back shortly."* — not an
-error. This matters: the most dangerous changes sever the very connection carrying them, so the API
-call times out even though the change succeeded on the device. Reachability, not the exception,
-decides the outcome; a change that throws while the device is still reachable is a genuine failure
-and is disarmed immediately so nothing reboots for nothing.
+It runs entirely over the RouterOS API, so it works on devices with no SSH credentials configured, and
+it covers twelve change types — bridge VLAN filtering, port PVID and tagging, bridge VLAN add/update/
+delete, IP address add/remove, route add/remove, bond create/delete, and management service toggles.
-Two consequences worth knowing before you rely on it:
+### 🔮 Lockout prediction — a warning that names the actual mechanism
-- **Binary mode reboots the device** to restore. That is the cost of a restore that recovers deleted
- interfaces exactly. Script mode (`/export` + `/import`) avoids the reboot but is additive, so it
- cannot recover a deletion.
-- **The restore point is saved before the scheduler is armed**, so a rollback returns a configuration
- containing neither the scheduler nor the change. It is self-cleaning, and a revert loop is
- impossible.
+Rather than blocklisting operations someone once thought were dangerous, the platform reads live device
+state, works out how the manager actually reaches the device, simulates your change against it, and
+reports any invariant that flips from satisfied to violated.
-Guard history is recorded per device in `device_change_guards`: `committed` (change kept),
-`reverted` (device left to rescue itself), `failed` (change rejected, nothing applied).
+The ingress port comes from the bridge forwarding table and the manager's own address from the device's
+connection tracking, so the warning is specific enough to act on:
-Configure under **Settings → Change Guard** (`change_guard_enabled`, `change_guard_mode`,
-`change_guard_timeout_sec`) and **Config Health** (`config_health_enabled`,
-`config_health_interval_min`, default 60 minutes).
+> **This change is predicted to cut management access to 2GT-NW-100G.**
+> Management arrives untagged on `sfp28-1` (PVID 1) — the gateway's MAC is learned there — but VLAN 1
+> has no bridge VLAN entry listing `bridge1` as an untagged member.
-### Switches
-- VLAN management (create, edit, delete VLANs)
-- Per-port configuration and VLAN membership
-- Switch overview with port status
-- **Per-port connected clients** — selecting a port shows the clients actually connected to it (hostname, MAC, IP, VLAN, connection time) with links to each client's page. Crucially, it distinguishes *physically connected* clients from the raw MAC table: **uplink/trunk ports are auto-detected** (via an LLDP/MNDP topology neighbor, MACs spanning multiple VLANs, or a high MAC count) and show an "uplink" explainer instead of every MAC reachable through the port — with a one-click disclosure to view the full MAC table when you need it
-- **Copy VLANs from another switch** — 3-step wizard that copies VLAN IDs and names from any other managed switch onto the current device, with manual per-VLAN port assignment (tagged/untagged) using a click-to-cycle interface chip grid, conflict detection with per-VLAN skip/overwrite control, and a review summary before any changes are applied
+A predicted lockout is refused outright. Overriding a critical verdict means typing the device name,
+and the change still runs under Change Guard.
-### Wireless
-- Per-AP SSID management — create, edit, enable/disable, delete wireless interfaces
-- **Bulk SSID deployment** — push an SSID configuration to all managed APs simultaneously
-- Security profile management (WPA2/WPA3, PSK, EAP)
-- Hardware radio information and band filtering (RouterOS 7 wifi package + legacy wlan package)
-- Scheduled and on-demand **spectral scans** per radio
-- Scheduled and on-demand **AP scans** (nearby access point discovery)
-- Real-time radio monitoring
-- Wireless client tracking with vendor lookup
-- **Device fingerprinting** — every client is classified into a device category (server/NAS, computer, phone, TV, camera, printer, game console, voice assistant, smart-home, IoT, network gear…) from its OUI vendor and hostname, shown as an icon on the Clients list. The fingerprint is fully **overridable per client** on the client detail page, and the override persists across re-polls
-- **RF Health** (fleet-wide on the Wireless page and per-AP on the Radios tab):
- - **Channel usage map** across 2.4 / 5 / 6 GHz, computed from each radio's operating channel (live-resolved from `monitor` on the wifi package when channels are auto), highlighting in-use channels and **co-channel overlap**
- - **AP deployment density** — connected clients plotted across the −90…−30 dBm signal scale, with a coverage-gap warning when too many clients connect at weak RSSI
- - **AP radio TX retries** histogram (0%→35%+) with a band selector, derived from per-client CCQ (legacy `wireless` driver)
- - **WiFi connectivity success** funnel (Association / Authentication / DHCP) derived from device logs
-- **Rogue & neighbor AP detection** — cross-references stored AP-scan results against your own SSIDs and radio MACs. A foreign BSSID broadcasting one of *your* SSIDs is flagged as a **rogue / evil-twin** (and raised in Operations → Things to handle); everything else is a ranked neighbor-network inventory
+### 🩺 Config Health — finds what RouterOS accepted but never applied
-### CAPsMAN
-Access points provisioned centrally by CAPsMAN hold none of their own wireless configuration — the
-controller owns it — so reading an AP directly returns blank SSID, security and band. The platform
-now recognises that instead of showing an access point that looks broken:
+A standing, read-only audit for configurations RouterOS takes without complaint and then quietly
+ignores. It encodes MikroTik's documented Layer 2 misconfigurations — an IP address or VLAN interface
+on a bridge slave port, a bond slave that is also a bridge port, MTU above L2MTU, a PVID that
+`frame-type=admit-only-vlan-tagged` renders inert, management surviving only on a dynamic VLAN entry,
+and more.
-- **Role detection** — each device is classified as standalone, CAP, controller, or controller-with-
- local-radios (a supported MikroTik arrangement) from `/interface/wifi/capsman` and `/interface/wifi/cap`
-- **Provisioned interfaces are labelled** — a CAPsMAN badge on the Radios tab, and an explanation that
- the blank fields live on the controller, with the controller's MAC. SSID, mode and channel are
- recovered from RouterOS's status line where present
-- **CAPsMAN panel on the Wireless page** — every controller with the access points it provisions,
- grouped per AP: radios, hardware type, live channel, provisioned SSIDs with client counts, named
- configurations, and the provisioning rules that bind them
-- **CAPs are matched to managed devices by radio MAC**, taken from the controller's own
- `/interface/wifi/radio` list. A MAC is hardware identity and globally unique, so unlike an IP it
- cannot mean two devices on two segments. A CAP that is *not* in the fleet is surfaced rather than
- hidden, since that is usually why an AP appears to be missing
-- **Controllers are no longer excluded** — the wireless section previously required
- `device_type = 'wireless_ap'`, which hid a dedicated controller (normally classified as a router)
+Every finding explains what it does to your network, how to fix it, how long it has been there, and
+links the relevant MikroTik documentation.
-**Read-only for now.** Editing a CAPsMAN configuration applies it to every access point bound to it
-at once, so provisioning and config edits are deliberately not exposed until they carry the same
-prediction-and-revert protection that device changes do. Legacy `/caps-man` (the pre-RouterOS 7
-controller) is a separate API tree and is not covered yet.
+### And the rest of the toolbox
-### Guest WiFi (Hotspot)
-A one-click captive-portal guest network on RouterOS Hotspot, under **Wireless → Guest WiFi**:
-- **Guided setup wizard** — creates the whole guest network in one pass: a new **guest SSID** (a virtual AP on every physical radio), **VLAN segregation** (guest SSIDs tagged onto the bridge with an L3 VLAN interface for the portal — or a dedicated guest bridge when no VLAN is given), IP pool, DHCP, hotspot profile, portal server, a bandwidth-limited guest user profile, and an optional masquerade NAT rule. Idempotent, with honest warnings (e.g. bridge VLAN-filtering disabled). Can also target an existing interface.
-- **Vouchers** — batch-generate access codes (`XXXX-XXXX`, no ambiguous characters) with validity hours, data caps, and a speed profile; **printable voucher sheets** (3-up cards with code, limits, and connection instructions) and a used/unused status table
-- **Guests online** — live session table (code, IP, MAC, session time, down/up bytes) with one-click disconnect
-- **Walled garden** — sites reachable before login, managed inline; per-server enable/disable
-
-### Network Services
-Each service supports multi-device management with conflict detection:
-
-| Service | Capabilities |
+| | |
|---|---|
-| **DHCP** | IPv4 & IPv6 servers, address pools, static leases, live lease table |
-| **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 |
-| **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
-- **Per-client usage accounting** — flows are attributed to known clients (IP → MAC) so every client shows real upload/download totals; "Data (today)" column on the Clients page and an App Traffic card on each client's detail page
-- **Application breakdown** — flows are classified by protocol and port into readable categories (HTTPS, QUIC, DNS, SSH, Email, WireGuard, …) with fleet-wide and per-client views
-- **Top talkers** — ranked client bandwidth usage over 1h/24h/7d/30d ranges on the dedicated Traffic page
-- **Automatic deduplication** — a flow crossing two managed routers is exported twice; the collector keeps only the best exporter per client each window so totals are never double-counted
-- **NAT-tolerant ingest** — when routers export from behind NAT or a VPN/Tailscale subnet router, packets arrive from an address that matches no managed device; the collector accepts these as per-source "unidentified" exporters (configurable) so flows are still attributed to clients instead of silently dropped, and the NetFlow page shows a banner naming the NAT'd source. Per-device export status/flow counters require un-NAT'd sources — exempt `udp/2055` from masquerade on the gateway to restore device identity (and exact cross-device dedup)
-- **Fully UI-configurable** — enable the collector, set its address/port/version in the NetFlow page, then toggle export per device; the platform pushes `/ip traffic-flow` configuration to each router via the RouterOS API
-- Configurable retention for detailed time-series (default 30 days) and daily rollups (default 365 days)
-
-### Network Topology
-- **LLDP-authoritative** — LLDP links are treated as ground truth; CDP/MNDP links to the same neighbor are automatically suppressed, eliminating spurious "Shared Segment" nodes
-- Bidirectional LLDP pairs merged into a single canonical edge with both port names labeled
-- Auto-discovered network map using LLDP, CDP, and MNDP neighbor data
-- Interactive node graph with device type icons and protocol-priority link deduplication
-- **Manual link drawing** — Connect Mode lets you drag between any two devices to draw a persistent connection for devices with no auto-discovered neighbors; connections are stored in the database and survive page reloads
-- **Orphan node detection** — devices with no known connections are grouped in a dedicated row with an orange warning banner prompting Connect Mode usage
-- Manual links render as purple dashed edges with a midpoint delete button
-
-### Device Network Tools
-Per-device diagnostic and testing tools accessible from the device detail Tools tab:
-
-| Tool | Description |
-|---|---|
-| **Ping** | ICMP reachability test with RTT and loss metrics |
-| **Traceroute** | Hop-by-hop path trace to any destination |
-| **IP Scan** | ARP sweep of a subnet to discover live hosts |
-| **Wake-on-LAN** | Send a magic packet from the MikroTik device to wake a host |
-| **Packet Capture** | Start the RouterOS sniffer, capture for 5–60 seconds, download a `.pcap` file directly to your browser (opens in Wireshark). Requires SSH credentials on the device. |
-| **Bandwidth Test** | Measure throughput between two devices. Select any managed device as the target — the bandwidth-test server is automatically enabled on the target before the test and disabled afterward. Manual IP mode available for non-managed targets. |
-
-### Firmware Orchestration
-Staged fleet-wide RouterOS upgrades, under the top-level **Firmware** section:
-- **Fleet versions** — current RouterOS, latest-known version, and pending RouterBOOT upgrades per device; **Check all for updates** queries every online device live
-- **Release notes in-app** — when an update is available, a "What's new" link on the device's firmware section (and the version on the Firmware page) opens MikroTik's official changelog for that exact version in a modal, with a link out to mikrotik.com
-- **Staged rollouts in waves** — select updatable devices and assign each to a wave (wave 1 = **canary**); the orchestrator runs one rollout at a time, devices sequentially, through a verified pipeline: **pre-upgrade backup → install → ride out the reboot → verify it returned healthy on the new version → next device**
-- **Halt on failure** stops the entire remaining rollout if any device fails, so a bad build never reaches the fleet; a reboot that comes back on the *old* version counts as a failure
-- **Schedule** rollouts for a future time (pair with a maintenance window); live wave-grouped progress with animated per-device status, `from → to` versions, error detail, and **Cancel** (never interrupts an in-flight flash)
-
-### Backups
-- Trigger RouterOS backups on demand via SSH
-- **Scheduled automatic backups** — pick a daily, weekly, or monthly schedule and time in Settings (no cron knowledge required); runs for all online devices
-- Download and manage backup files from the UI
-
-### Platform & Automation
-Make the platform scriptable and integrable, under **Settings → Automation**:
-- **Scoped API tokens** — issue `mtm_…` tokens with **read** or **write** scope and optional expiry for scripting and IaC; the token is shown once (only a SHA-256 hash is stored) and maps onto the role model — no token can perform admin actions or manage other tokens. Use with `Authorization: Bearer mtm_…` against the full REST API
-- **Outbound webhooks** — subscribe URLs to any of 12 events (device up/down, log errors, high CPU/memory, cert expiry, device discovered, firmware update, config drift, firmware rollout completed/failed); deliveries are JSON POSTs **HMAC-SHA256 signed** (`X-MTM-Signature`) when a secret is set, with last-status tracking and a Send-test button. Fired through the same alert pipeline (respecting alert rules, cooldowns, and maintenance windows)
-- **Scheduled email reports** — daily/weekly/monthly HTML fleet summaries (devices online, outages + downtime, error/warning counts, updates pending, backups taken, top clients by traffic) to any recipient list, using your Alerting SMTP settings; Send-now for instant delivery
-
-### Configuration History
-- Per-device config snapshots based on the device's full RouterOS `/export` — config-only, so counters and operational state never create noise — captured automatically whenever the configuration changes (deduplicated by content hash, so a snapshot is only stored on a real change)
-- **Config History** tab on each device with a timeline of snapshots and a one-line summary of what changed (e.g. `+8 / −3 lines`)
-- Side-by-side **line diff** between any two snapshots, rendered as readable RouterOS commands
-- **Capture snapshot** button for an on-demand checkpoint, with honest feedback when nothing has changed since the last one
-- **One-click rollback** — each snapshot links a restorable `.rsc` backup, so rolling back reuses the proven restore path
-- The snapshot and its backup are one artifact: snapshot backups are clearly badged **Config Snapshot** on the Backups page, and deleting one (from either place) removes the other so they never drift apart
-- Fires a `config_drift` alert (off by default) when a device's configuration changes
-- Snapshot cadence and retention configurable via `config_snapshot_interval_min` / `config_snapshot_retention`
-
-### Alerts
-Configurable alert rules with cooldown periods:
-- Device online / offline
-- High CPU or memory usage (configurable threshold)
-- SSL certificate expiry warning
-- Firmware update available
-- RouterOS log errors and warnings
-- New device discovered
-- Configuration changed (drift detection)
-
-Alert delivery channels: **Email**, **Slack**, **Discord**, **Telegram**, **ntfy**
-
-**ntfy** (`https://ntfy.sh` or your own instance) is configured with a topic and, optionally, an
-access token or username/password — a publicly-writable topic lets anyone spoof your alerts, so
-protecting it is worth the extra field. Alert severity maps onto ntfy's priority scale so the
-notifications that mean something is *down* (device offline, log errors, CPU/memory pressure) arrive
-at priority 4 and break through a phone's do-not-disturb, recoveries and discoveries arrive quietly
-at 2, and everything else sits at the default 3. Each notification is tagged with its event type, so
-a client can be filtered to wake only for the events you care about. Set the optional **Manager URL**
-and notifications become tappable, opening the device they refer to.
-
-### Maintenance Windows
-- Schedule planned downtime windows per device or group of devices to suppress alerts automatically
-- One-time or recurring windows (cron-based)
-- Active window management with activate/deactivate controls
-- Managed from Settings → Maintenance
-
-### Audit Log
-- Every write operation (create, update, delete, push) performed by an authenticated user is recorded automatically
-- Log includes: user, timestamp, HTTP method, API path, entity type/ID, summary, IP address, and HTTP response status
-- Filterable and paginated view in Settings → Audit Log
-- Useful for multi-operator environments to track who changed what and when
-
-### Configuration Templates
-- Define reusable configuration sets (DNS servers, NTP servers, syslog host) and push them to one or more managed devices in a single operation
-- Per-device result reporting (success / error per device)
-- Managed from Settings → Config Templates
-
-### Global Search
-Instant search across devices, clients, and events from the top navigation bar.
-
-### User Management & Access Control
-- Role-based access: **Admin**, **Operator** (read/write), **Viewer** (read-only)
-- Admin-only user creation and role assignment
-- JWT authentication with secure session handling
-- **Two-factor authentication (TOTP)** — per-user 2FA setup via QR code (compatible with Google Authenticator, Authy, etc.); login requires a 6-digit code after password when enabled; disable with password confirmation
-- **Single sign-on (OIDC/SSO)** — authenticate against any standards-compliant OpenID Connect provider (Microsoft Entra ID, Okta, Google Workspace, Keycloak, Authentik, …). Auto-provisions or links users on first sign-in, maps IdP groups to platform roles, and coexists with local login as a break-glass fallback. Configured entirely in the UI — see [Single sign-on (OIDC)](#single-sign-on-oidc) below
-- **Credential preset access control** — presets can be restricted to admins only (`allow_operator_use`); operators only see presets they are permitted to use when adding or updating devices
-
-### TLS / HTTPS
-- Automatic self-signed certificate generation on first run
-- Upload a custom certificate and private key via the Settings UI
-- nginx handles TLS termination and HTTP→HTTPS redirect
+| **Staged firmware rollouts** | Canary waves, pre-upgrade backup, reboot verification, and halt-on-failure so a bad build never reaches the fleet |
+| **Built-in NetFlow collector** | Traffic analytics with per-client attribution and application breakdown — no external collector to run |
+| **CAPsMAN aware** | Centrally provisioned APs are recognised, grouped under their controller, and protected from local writes the controller would discard |
+| **Rogue AP detection** | A foreign BSSID broadcasting one of your SSIDs is flagged as an evil twin |
+| **Anomaly insights** | Client counts and CPU compared against each device's own same-hour 14-day baseline, not a fixed threshold |
+| **Enterprise auth** | OIDC/SSO, TOTP two-factor, role-based access, and scoped API tokens |
---
-## Tech Stack
+## Screenshots
-| Layer | Technology |
-|---|---|
-| **Frontend** | React 18, TypeScript, Vite, Tailwind CSS |
-| **State / Data** | TanStack Query v5, React Router v6, Zustand |
-| **Charts** | Recharts |
-| **Topology** | @xyflow/react |
-| **Maps** | Leaflet |
-| **Terminal** | xterm.js |
-| **Backend** | Node.js, Express, TypeScript |
-| **Primary DB** | PostgreSQL 15 |
-| **Time-series DB** | InfluxDB 2.7 |
-| **Cache / Queue** | Redis 7, BullMQ |
-| **Real-time** | Socket.IO |
-| **Device comms** | RouterOS API (port 8728), SSH2 |
-| **Proxy** | nginx (TLS termination, static file serving) |
-| **Container** | Docker Compose |
+
+
+
+ 
+ Device list
+ |
+
+ 
+ Device overview
+ |
+
+
+
+ 
+ Switch ports & throughput
+ |
+
+ 
+ Hardware monitor
+ |
+
+
+
+ 
+ Network topology
+ |
+
+ 
+ Wireless radios
+ |
+
+
+
+ 
+ Client tracking
+ |
+
+ 
+ Client detail
+ |
+
+
+
+ 
+ Event log
+ |
+
+ 
+ Backups
+ |
+
+
+
+ 
+ Sign-in, with optional SSO and two-factor
+ |
+
+
---
-## Requirements
+## Get running in five minutes
-- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) (v2+)
-- MikroTik devices running **RouterOS 6.x or 7.x** with the API service enabled
-- Network access from the host running this application to your MikroTik devices on port **8728** (or your configured API port)
+You need [Docker](https://docs.docker.com/get-docker/) with Compose v2, and MikroTik devices running
+**RouterOS 6.x or 7.x** reachable on their API port (`8728`, or `8729` for API-SSL).
----
+### Option A — pre-built images (recommended)
-## Quick Deploy (Pre-built Images)
-
-No source code or build toolchain required — just Docker and Docker Compose.
-
-### 1. Download the compose file
+No source checkout, no build toolchain.
```bash
+# 1. Grab the compose file and environment template
curl -O https://raw.githubusercontent.com/2GT-Media-Group-LLC/mikrotik-manager/main/docker-compose.ghcr.yml
-```
-
-### 2. Create your environment file
-
-```bash
curl -O https://raw.githubusercontent.com/2GT-Media-Group-LLC/mikrotik-manager/main/.env.example
mv .env.example .env
-```
-Edit `.env` and set at minimum:
+# 2. Set your domain in .env (secrets auto-generate if you leave them unset)
+# CORS_ORIGIN=https://manager.example.com
-```env
-JWT_SECRET=your_long_random_jwt_secret_here
-ENCRYPTION_KEY=your_32_character_encryption_key_
-CORS_ORIGIN=https://your-domain.example.com
-```
-
-### 3. Start the application
-
-```bash
+# 3. Start
docker compose -f docker-compose.ghcr.yml up -d
```
-Docker pulls the pre-built images from GitHub Container Registry and starts the stack. No compilation, no cloning.
+Open **https://localhost** (or your server's hostname) and sign in with `admin` / `admin`.
+**Change that password immediately** under Settings → Users.
-### 4. Open the app
+> **Updating:** `docker compose -f docker-compose.ghcr.yml pull && docker compose -f docker-compose.ghcr.yml up -d`
-Navigate to **https://localhost** (or your server's IP/hostname) and log in with `admin` / `admin`.
-
-> To update to the latest release: `docker compose -f docker-compose.ghcr.yml pull && docker compose -f docker-compose.ghcr.yml up -d`
-
----
-
-## Quick Start (Build from Source)
-
-For contributors or anyone who wants to build the images locally.
-
-### 1. Clone the repository
+### Option B — build from source
```bash
git clone https://github.com/2GT-Media-Group-LLC/mikrotik-manager.git
cd mikrotik-manager
-```
-
-### 2. Configure environment variables
-
-Copy the example environment file and edit it:
-
-```bash
-cp .env.example .env
-```
-
-At minimum, change these values in `.env`:
-
-```env
-# Required — use long, random strings
-JWT_SECRET=your_long_random_jwt_secret_here
-ENCRYPTION_KEY=your_32_character_encryption_key_
-
-# Required for production deployments (set to your domain)
-# CORS_ORIGIN=https://manager.example.com
-
-# Optional — defaults work for a local install
-DB_PASSWORD=mikrotik_secure_pw
-INFLUXDB_TOKEN=mytoken123456789
-```
-
-> **Security note:** Never commit your `.env` file to version control. The `.gitignore` already excludes it.
-
-### 3. Start the application
-
-```bash
+cp .env.example .env # edit CORS_ORIGIN for production
docker compose up -d
```
-The first run will:
-- Build the frontend (React → static files)
-- Build the backend (TypeScript → Node.js)
-- Initialize PostgreSQL with the database schema
-- Initialize InfluxDB
-- Generate a self-signed TLS certificate
+First run builds the frontend and backend, initialises PostgreSQL and InfluxDB, and generates a
+self-signed TLS certificate. Accept the browser warning, or upload a real certificate under
+**Settings → TLS Certificate**.
-### 4. Open the app
+> **Updating:** `git pull && docker compose up -d --build backend nginx` — migrations run automatically on startup.
-Navigate to **https://localhost** (or your server's IP/hostname).
+### If a device won't connect
-Accept the browser's self-signed certificate warning, or upload a real certificate in **Settings → TLS Certificate**.
+API access is usually enabled already. If not:
-### 5. Log in
+```
+/ip service enable api # port 8728
+/ip service enable api-ssl # port 8729, TLS
+```
-Default credentials on first run:
-
-| Username | Password | Role |
-|---|---|---|
-| `admin` | `admin` | Admin |
-
-**Change the default password immediately** in Settings → Users.
+Devices on the SSL port are reached over TLS automatically — RouterOS's default self-signed
+certificates are accepted, so credentials are encrypted in transit with no manual trust step.
---
-## Enabling the RouterOS API
+## Features
-Typically, API access is enabled on MikroTik devices. However if you can't connect via API, ensure the API service is enabled:
+### Safety and change protection
-```
-/ip service enable api
-```
+The layers described [above](#what-makes-it-different), plus the surrounding machinery.
-For API over SSL (port 8729):
-```
-/ip service enable api-ssl
-```
+- **Change Guard** — device-side auto-revert on twelve change types, over the RouterOS API, no SSH required
+- **Lockout prediction** — live-state simulation that refuses a change predicted to sever management, with the hop chain that explains why
+- **Config Health** — scheduled audit for RouterOS's documented Layer 2 misconfigurations, surfaced on the device's Security tab and in the dashboard
+- **Firewall lockout guard** — refuses an unscoped `input`-chain drop/reject that would lock you out, requiring explicit confirmation
+- **CAPsMAN write protection** — a provisioned radio is owned by the controller, so local SSID creates, edits and deletes are refused rather than silently discarded
+- **Capability probe** — `POST /api/devices/:id/change-guard/probe` reports what safety mechanisms a device actually supports, tested rather than assumed, and leaves nothing behind
+- **Guard history** — every protected change recorded per device as `committed`, `reverted`, or `failed`
-The default API port is `8728`. You can configure a different port per device in the MikroTik Manager interface.
+
+What you see when auto-revert fires
+
+
+
+The request returns **HTTP 200** with `guard.auto_reverting: true` and *"Contact with the device was
+lost while applying this change. It is restoring itself and should come back shortly."* — not an error.
+
+This matters: the most dangerous changes sever the very connection carrying them, so the API call times
+out even though the change succeeded on the device. **Reachability, not the exception, decides the
+outcome.** A change that throws while the device is still reachable is a genuine failure, and the guard
+is disarmed immediately so nothing reboots for nothing.
+
+Two consequences worth knowing before relying on it:
+
+- **Binary mode reboots the device** to restore. That is the price of a restore that recovers deleted
+ interfaces exactly. Script mode (`/export` + `/import`) avoids the reboot but is additive, so it
+ cannot recover a deletion.
+- **The restore point is saved before the scheduler is armed**, so a rollback returns a configuration
+ containing neither the scheduler nor the change. Self-cleaning, and a revert loop is impossible.
+
+
+
+### Monitoring and operations
+
+- **Live dashboard** — device counts, wireless clients, active alerts, fleet-wide 30-day availability, device-type distribution, firmware notifications, and a historical client graph (1h → 30d)
+- **Operations view** — a second dashboard mode built for running the network:
+ - **Things to handle** — a server-side insights engine, severity-ordered: offline devices, pending firmware, CPU/memory pressure, missing backups, connectivity flapping, WiFi quality problems, and Config Health findings
+ - **Quick actions that actually run** — discovery, back up all online devices, sync config, or open an in-browser SSH terminal, each with inline progress
+ - **Capacity & health** — per-device CPU and memory meters sorted by pressure
+ - **Security posture rollup** — fleet hardening score and the lowest-scoring devices
+ - **Anomaly insights** — each device's last 30 minutes of client count and CPU compared against its **own same-hour-of-day 14-day baseline**, flagging ≥2.5σ deviations plus error-log bursts
+- **Availability tracking** — per-device uptime %, outage count, and longest outage over 30 days
+- **Alerts** — device up/down, CPU/memory thresholds, certificate expiry, firmware available, log errors and warnings, new device discovered, and config drift, each with cooldowns
+- **Delivery channels** — Email, Slack, Discord, Telegram, and **ntfy** (self-hosted or `ntfy.sh`, with severity mapped onto ntfy priority so outages break through do-not-disturb and recoveries stay quiet)
+- **Maintenance windows** — one-time or recurring (cron) windows per device that suppress alerts automatically
+
+### Switching, VLANs and wireless
+
+- **VLAN management** — create, edit and delete VLANs; per-port membership with tagged/untagged control
+- **Per-port connected clients** — selecting a port shows who is *physically* on it. Uplink and trunk ports are auto-detected (via an LLDP/MNDP neighbour, MACs spanning multiple VLANs, or a high MAC count) and show an explainer rather than every MAC reachable through them, with one-click disclosure of the full table
+- **Copy VLANs between switches** — a three-step wizard with click-to-cycle port assignment, conflict detection, and a review summary before anything is applied
+- **Wireless management** — SSID create/edit/enable/disable/delete, bulk SSID deployment across APs, WPA2/WPA3 security profiles, and support for both the RouterOS 7 `wifi` package and the legacy `wlan` package
+- **RF Health** — channel usage across 2.4/5/6 GHz with co-channel overlap highlighting, AP deployment density plotted against RSSI, TX-retry histograms, and a connectivity funnel (association → authentication → DHCP) derived from device logs
+- **Spectral and AP scans** — scheduled or on demand, per radio
+- **Rogue & neighbour AP detection** — stored scan results cross-referenced against your own SSIDs and radio MACs; a foreign BSSID broadcasting your SSID is flagged as an evil twin
+- **Device fingerprinting** — every client classified into a category (server, computer, phone, TV, camera, printer, console, IoT…) from OUI and hostname, overridable per client and persistent across polls
+- **CAPsMAN** — role detection (standalone / CAP / controller / controller-with-local-radios), provisioned interfaces labelled with their controller, and a controller panel grouping every AP with its radios, live channel, provisioned SSID and client counts. CAPs are matched to managed devices by radio MAC — hardware identity, so it cannot confuse two devices on two segments. Read-only for now; see [CAPsMAN scope](#capsman-scope)
+- **Guest WiFi** — a guided wizard that builds a full captive portal in one pass: guest SSID on every radio, VLAN segregation, IP pool, DHCP, hotspot profile, bandwidth-limited user profile and optional NAT. Plus batch vouchers with printable 3-up sheets, a live guests-online table, and an inline walled garden
+
+### Firewall and network services
+
+A Meraki/UniFi-grade firewall experience on the full RouterOS feature set.
+
+- **Security Center** — fleet-wide hardening scores, per-device posture, and a "Common Findings" rollup aggregating identical issues across every device
+- **Friendly rule builder** — action chips, address and address-list pickers, well-known port presets, connection-state chips, per-rule logging, and a plain-English preview of every rule
+- **Address lists as reusable objects** — define `LAN`, `Trusted`, `Blocklist` once and reference them anywhere
+- **Rule reordering** — order is decisive in RouterOS; move rules with one click
+- **Hit counters** — per-rule packet and byte counts surface which rules match and flag dead ones
+- **NAT wizards** — guided Port Forward, Masquerade and 1:1 NAT flows, plus a Custom mode
+- **Security posture audit** — per-device hardening checklist with a score and one-click remediation
+- **Bandwidth control** — simple queues per IP, subnet or interface, with a one-click "limit this client"
+- **Active connections** — live connection-tracking table with search
+
+Each network service supports multi-device management with conflict detection:
+
+| Service | Capabilities |
+|---|---|
+| **DHCP** | IPv4 & IPv6 servers, address pools, static leases, live lease table |
+| **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 keys, RX/TX stats |
+| **Logging** | Syslog actions and routing rules, single-device or push-to-all with per-entry coverage |
+| **NetFlow** | One-toggle Traffic Flow export per device, pointed at the built-in collector |
+| **Discovery & SNMP** | Fleet-wide LLDP toggle and SNMP v1/v2c/v3, scoped to all devices, routers, or switches |
+
+### Visibility and troubleshooting
+
+- **Network topology** — auto-discovered from LLDP, CDP and MNDP, with LLDP treated as ground truth and lower-priority protocols suppressed for the same neighbour. Bidirectional pairs merge into one edge with both port names. Neighbours are resolved to managed devices by MAC before IP, because a MAC is unique fleet-wide and an address is not
+- **Manual links** — drag between any two devices to record a connection discovery cannot see; stored persistently and drawn as purple dashed edges
+- **Orphan detection** — devices with no known connections are grouped with a prompt to link them
+- **Traffic analytics** — a built-in NetFlow v9/IPFIX collector on UDP 2055, per-client attribution, application breakdown (HTTPS, QUIC, DNS, SSH, email, WireGuard…), top talkers over 1h → 30d, automatic deduplication when a flow crosses two managed routers, and NAT-tolerant ingest for routers exporting from behind NAT or a VPN subnet router
+- **Configuration history** — snapshots of each device's full `/export`, deduplicated by content hash so only real changes are stored, with side-by-side line diffs, a change summary, and **one-click rollback** through the proven restore path
+- **Audit log** — every authenticated write recorded with user, timestamp, method, path, entity, IP and response status
+- **Global search** — devices, clients and events from the top bar
+- **Per-device tools**:
+
+| Tool | Description |
+|---|---|
+| **Ping** | ICMP reachability with RTT and loss |
+| **Traceroute** | Hop-by-hop path trace |
+| **IP Scan** | ARP sweep of a subnet to find live hosts |
+| **Wake-on-LAN** | Magic packet sent from the MikroTik device |
+| **Packet Capture** | RouterOS sniffer for 5–60s, downloaded as a `.pcap` for Wireshark (requires SSH) |
+| **Bandwidth Test** | Throughput between two devices; the target's test server is enabled and disabled automatically |
+
+### Fleet operations
+
+- **Firmware orchestration** — fleet version overview, live update checks, and MikroTik's official release notes in-app. Staged rollouts run in **waves** (wave 1 = canary) through a verified pipeline per device: pre-upgrade backup → install → ride out the reboot → confirm it returned healthy on the new version → next. **Halt on failure** stops the rollout if any device fails, and a device that comes back on the *old* version counts as a failure. Rollouts can be scheduled, and cancelling never interrupts an in-flight flash
+- **Backups** — on demand or on a daily/weekly/monthly schedule for all online devices, downloadable from the UI. Config snapshots and their restorable `.rsc` are one artifact: delete either and both go, so they never drift apart
+- **Configuration templates** — reusable sets (DNS, NTP, syslog) pushed to many devices with per-device result reporting
+- **Bulk device add** — "Try All" on discovered devices runs as a server-side job that survives a closed browser tab, with live progress and cancel
+- **Device organisation** — colour-coded tags, rack location, physical address with map support, and per-device notes
+
+### Access control and platform
+
+- **Roles** — Admin, Operator (read/write), Viewer (read-only)
+- **Two-factor authentication** — per-user TOTP via QR code, required at login once enabled
+- **Single sign-on (OIDC)** — any standards-compliant provider (Entra ID, Okta, Google Workspace, Keycloak, Authentik…), with group-to-role mapping, auto-provisioning, and local login retained as break-glass. Configured entirely in the UI — see [Single sign-on setup](#single-sign-on-oidc)
+- **Scoped API tokens** — `mtm_…` tokens with read or write scope and optional expiry, shown once and stored only as a SHA-256 hash. No token can perform admin actions or manage other tokens
+- **Outbound webhooks** — subscribe to any of twelve events, delivered as HMAC-SHA256-signed JSON POSTs through the same pipeline that respects alert rules, cooldowns and maintenance windows
+- **Scheduled email reports** — daily, weekly or monthly HTML fleet summaries to any recipient list
+- **Credential presets** — shared device credentials, optionally restricted to admins only
+- **Encryption at rest** — device passwords encrypted with AES-256-GCM under a [self-healing key](#secret-management-self-healing)
+- **TLS** — self-signed certificate generated on first run, replaceable via the Settings UI; nginx terminates TLS and redirects HTTP
---
-## Configuration Reference
+## Reference
-All configuration is done via environment variables in `.env`:
+### Environment variables
+
+Set in `.env`. Both secrets auto-generate on first boot if you leave them unset.
| Variable | Default | Description |
|---|---|---|
-| `JWT_SECRET` | *(auto-generated & persisted if unset)* | Secret for signing JWT tokens. Leave unset to have a strong one generated on first boot; set it to pin your own value. |
-| `ENCRYPTION_KEY` | *(auto-generated & persisted if unset)* | Key for encrypting device passwords at rest. Leave unset to auto-generate; set it to pin your own value. |
-| `CORS_ORIGIN` | *(localhost defaults set by Docker Compose)* | Comma-separated list of browser origins allowed to call the API (e.g. `https://manager.example.com`). **Required in production** — set this to your domain. |
+| `JWT_SECRET` | *auto-generated & persisted* | Signs session tokens. Set it to pin your own value. |
+| `ENCRYPTION_KEY` | *auto-generated & persisted* | Encrypts device passwords at rest. Set it to pin your own value. |
+| `CORS_ORIGIN` | *localhost defaults* | Comma-separated browser origins allowed to call the API. **Required in production.** |
| `DB_PASSWORD` | `mikrotik_secure_pw` | PostgreSQL password |
| `INFLUXDB_TOKEN` | `mytoken123456789` | InfluxDB admin token |
-| `INFLUXDB_ORG` | `mikrotik-manager` | InfluxDB organization name |
+| `INFLUXDB_ORG` | `mikrotik-manager` | InfluxDB organization |
| `INFLUXDB_BUCKET` | `metrics` | InfluxDB bucket for time-series data |
| `INFLUXDB_ADMIN_PASSWORD` | `admin_password_123` | InfluxDB admin UI password |
| `HTTP_PORT` | `80` | Host port for HTTP (redirects to HTTPS) |
| `HTTPS_PORT` | `443` | Host port for HTTPS |
-### Change Guard & Config Health settings
+> Never commit `.env`. It is already in `.gitignore`.
-These live in `app_settings` (Settings UI), not `.env`, so they can be changed without a restart:
+### Change Guard and Config Health settings
+
+These live in the Settings UI rather than `.env`, so they apply without a restart.
| Setting | Default | Description |
|---|---|---|
-| `change_guard_enabled` | `true` | Master switch. When off, guarded changes still apply but with no safety net and no lockout refusal. |
+| `change_guard_enabled` | `true` | Master switch. When off, guarded changes still apply — with no safety net and no lockout refusal. |
| `change_guard_mode` | `binary` | `binary` = `/system backup` restore; recovers deletions exactly, **reboots the device**. `script` = `/export` + `/import`; no reboot, but additive, so it cannot undo a deletion. |
-| `change_guard_timeout_sec` | `120` | How long the device waits before rescuing itself. Must exceed the time the manager needs to verify reachability (~40s), with margin for a slow device. |
+| `change_guard_timeout_sec` | `120` | How long the device waits before rescuing itself. Must exceed the manager's verification window (~40s) with margin for a slow device. |
| `config_health_enabled` | `true` | Runs the standing configuration audit. |
-| `config_health_interval_min` | `60` | Audit cadence per device. Each pass is a read-only snapshot over the API. |
+| `config_health_interval_min` | `60` | Audit cadence per device — one read-only snapshot over the API. |
-Overriding a predicted lockout is a deliberate act: the API requires `confirm_lockout: true` in the
-request body, and the UI requires typing the device name. The change then runs under Change Guard
-regardless, so an override is a decision to rely on auto-revert rather than to bypass protection.
+Overriding a predicted lockout is deliberate by design: the API requires `confirm_lockout: true` and the
+UI requires typing the device name. The change then runs under Change Guard regardless, so an override
+is a decision to *rely on* auto-revert — not to bypass protection.
+
+### CAPsMAN scope
+
+Provisioning, configuration edits and interface enable/disable are **deliberately not exposed**. A
+CAPsMAN configuration applies to every access point bound to it simultaneously, which is the fleet-wide
+version of the failure Change Guard exists to prevent. Those operations are held back until they carry
+the same prediction-and-revert protection that per-device changes do.
+
+Legacy `/caps-man` (the pre-RouterOS 7 controller) is a separate API tree and is not covered yet.
### Secret management (self-healing)
-`JWT_SECRET` (session signing) and `ENCRYPTION_KEY` (AES-256-GCM credential encryption, `backend/src/utils/crypto.ts`) are managed automatically:
+`JWT_SECRET` and `ENCRYPTION_KEY` are managed automatically:
-- **If set to a strong value in the environment**, that value is used — you stay in control.
-- **If unset or left at an old default**, the backend generates a strong secret **once**, persists it to the `app_data` volume (`SECRETS_DIR`, default `/app/data`), and reuses it on every boot. Secrets live outside the database so a DB dump alone can't reveal the key protecting the credentials in it.
+- **Set to a strong value in the environment** — that value is used, and you stay in control.
+- **Unset or left at an old default** — the backend generates a strong secret once, persists it to the
+ `app_data` volume (`SECRETS_DIR`, default `/app/data`), and reuses it every boot. Secrets live outside
+ the database, so a database dump alone cannot reveal the key protecting the credentials inside it.
-Upgrades are non-breaking: on startup, existing ciphertext is decrypted via a **legacy-key fallback** (including previous defaults) and transparently **re-encrypted under the current key** by a background sweep. When rotating the JWT secret off a public default, sessions signed with that default are no longer accepted — users simply log in again once.
+Upgrades are non-breaking. Existing ciphertext is decrypted via a **legacy-key fallback** (including
+previous defaults) and transparently re-encrypted under the current key by a background sweep. Rotating
+the JWT secret off a public default invalidates sessions signed with it — users simply log in again.
-**Key rotation:** set a new `ENCRYPTION_KEY` (or delete the persisted secret to force regeneration) and restart. Old rows keep decrypting via the legacy fallback and are re-encrypted forward automatically. **If the persisted secret and any prior key are both lost**, ciphertext under that key cannot be recovered — re-enter device credentials or restore a backup.
+**Key rotation:** set a new `ENCRYPTION_KEY` (or delete the persisted secret to force regeneration) and
+restart. Old rows keep decrypting via the fallback and are re-encrypted forward automatically. **If the
+persisted secret and every prior key are lost**, ciphertext under that key cannot be recovered — re-enter
+device credentials or restore a backup.
### Single sign-on (OIDC)
-Sign in against any standards-compliant OpenID Connect provider (Microsoft Entra ID, Okta, Google Workspace, Keycloak, Authentik, and others). It uses the Authorization Code flow with PKCE; the server validates the ID token (signature via JWKS, plus `iss`/`aud`/`exp`/`nonce`) and then issues the platform's normal session — so SSO users get the same experience as local users. **No environment variables or redeploy are required** — everything is configured in the UI and stored (with the client secret encrypted at rest) in the database.
+Authorization Code flow with PKCE. The server validates the ID token (JWKS signature plus `iss`, `aud`,
+`exp`, `nonce`) and then issues the platform's normal session, so SSO users get the same experience as
+local ones. **No environment variables or redeploy** — everything is configured in the UI, with the
+client secret encrypted at rest.
-**1. Register the app with your identity provider.** Create an OIDC/OAuth "web application" and note its **Issuer URL** (the base that serves `/.well-known/openid-configuration`), **Client ID**, and **Client Secret** (public clients with no secret are also supported via PKCE). For the **redirect / callback URI**, use the value shown in the settings panel:
+**1. Register the app with your provider.** Create an OIDC web application and note its **Issuer URL**
+(the base serving `/.well-known/openid-configuration`), **Client ID** and **Client Secret** — public
+clients with no secret work too, via PKCE. Use this redirect URI:
```
https:///api/auth/oidc/callback
```
-**2. Configure it in the app.** As an admin, go to **Settings → SSO / OIDC** and fill in:
+**2. Configure it under Settings → SSO / OIDC.**
| Field | Purpose |
|---|---|
-| **Issuer URL** | Your provider's base URL; **Test discovery** verifies it's reachable. |
-| **Client ID / Client Secret** | From step 1. The secret is write-only and encrypted at rest; leave blank to keep the existing one. |
-| **Scopes** | Default `openid profile email`. Add a groups scope if your IdP needs one to emit group membership. |
-| **Username / Email / Groups claim** | Which ID-token claims to read. Defaults: `preferred_username`, `email`, `groups`. |
-| **Group → role mapping** | Map IdP groups to **admin / operator / viewer** (e.g. `net-admins → admin`). Highest-privilege match wins. |
-| **Default role** | Role for users whose groups don't match any mapping (default **viewer**). |
-| **Auto-provision** | Create a local user automatically on first sign-in. |
-| **Link by verified email** | Attach an SSO identity to an existing local account when the IdP's `email_verified` matches. |
-| **Allowed email domains** | Optional allowlist restricting who may sign in. |
-| **Button label** | Text on the login button (e.g. "Sign in with Okta"). |
+| **Issuer URL** | Your provider's base URL; **Test discovery** verifies reachability. |
+| **Client ID / Secret** | From step 1. The secret is write-only; leave blank to keep the existing one. |
+| **Scopes** | Default `openid profile email`. Add a groups scope if your IdP needs one. |
+| **Username / Email / Groups claim** | Defaults `preferred_username`, `email`, `groups`. |
+| **Group → role mapping** | Map IdP groups to admin / operator / viewer. Highest-privilege match wins. |
+| **Default role** | For users matching no mapping (default **viewer**). |
+| **Auto-provision** | Create a local user on first sign-in. |
+| **Link by verified email** | Attach an SSO identity to an existing account when `email_verified` matches. |
+| **Allowed email domains** | Optional allowlist. |
+| **Button label** | Text on the login button, e.g. "Sign in with Okta". |
-Enable it and **Save**. A **Sign in with SSO** button appears on the login page.
+**Behaviour and safety:**
-**Behavior and safety:**
-- **Provisioning / linking** — first sign-in creates a user (auto-provision) or, if `email_verified` matches an existing local account, links to it (no duplicate). SSO users have no local password.
-- **Roles** are always resolved server-side from your group mapping; existing users are never silently demoted when a login carries no matching group.
-- **Break-glass** — local username/password login stays available, so the seeded local admin can always get in even if the IdP is misconfigured or unreachable.
-- **Behind a proxy / custom domain** — the redirect URI is derived from the request host; set **Public base URL** in the panel to pin it if your external URL differs from what the backend sees.
-- SSO users are labeled with an **SSO** badge in **Users & Roles**.
+- **Provisioning** — first sign-in creates a user, or links to an existing local account when the
+ verified email matches. SSO users have no local password.
+- **Roles** are always resolved server-side from your mapping, and existing users are never silently
+ demoted when a login carries no matching group.
+- **Break-glass** — local login stays available, so the seeded admin can always get in even if the IdP
+ is misconfigured or unreachable.
+- **Behind a proxy** — the redirect URI derives from the request host; set **Public base URL** to pin it
+ when your external URL differs from what the backend sees.
----
+### Tech stack
-## Updating
+| Layer | Technology |
+|---|---|
+| **Frontend** | React 18, TypeScript, Vite, Tailwind CSS |
+| **State / data** | TanStack Query v5, React Router v6, Zustand |
+| **Charts / topology / maps** | Recharts, @xyflow/react, Leaflet |
+| **Terminal** | xterm.js |
+| **Backend** | Node.js, Express, TypeScript |
+| **Primary DB** | PostgreSQL 15 |
+| **Time-series DB** | InfluxDB 2.7 |
+| **Cache / queue** | Redis 7, BullMQ |
+| **Real-time** | Socket.IO |
+| **Device comms** | RouterOS API (8728 / 8729), SSH2 |
+| **Proxy** | nginx — TLS termination and static serving |
+| **Container** | Docker Compose |
-Pull the latest changes and rebuild:
-
-```bash
-git pull
-docker compose up -d --build backend nginx
-```
-
-Database migrations run automatically on backend startup.
-
----
-
-## Project Structure
+### Project structure
```
mikrotik-manager/
├── frontend/ # React + TypeScript (Vite)
│ └── src/
-│ ├── pages/ # Page components (one per route)
-│ ├── components/ # Shared UI components
+│ ├── pages/ # One component per route
+│ ├── components/ # Shared UI
│ ├── services/ # API client (Axios)
│ ├── hooks/ # Custom React hooks
-│ └── types/ # TypeScript type definitions
+│ └── types/ # TypeScript definitions
│
├── backend/ # Node.js + Express + TypeScript
│ └── src/
-│ ├── routes/ # REST API route handlers
-│ ├── services/ # Business logic (polling, alerts, backups)
-│ │ └── mikrotik/ # RouterOS API client and device collector
-│ ├── db/ # Database migrations
-│ ├── config/ # DB, InfluxDB, Redis connections
-│ ├── middleware/ # Auth, audit logging, error handling
-│ └── utils/ # Helpers (crypto, OUI lookup, etc.)
+│ ├── routes/ # REST API handlers
+│ ├── services/ # Polling, alerts, backups, orchestration
+│ │ ├── mikrotik/ # RouterOS API client and device collector
+│ │ ├── changeGuard/# Auto-revert, lockout prediction, Config Health
+│ │ └── topology/ # Neighbour graph construction
+│ ├── db/ # Schema and migrations
+│ ├── config/ # Database, InfluxDB, Redis connections
+│ ├── middleware/ # Auth, audit logging, error handling
+│ └── utils/ # Crypto, OUI lookup, VLAN parsing
│
├── nginx/ # Reverse proxy config and Dockerfile
-├── docker-compose.yml
+├── docker-compose.yml # Build from source
+├── docker-compose.ghcr.yml # Pre-built images
└── .env.example
```
@@ -676,38 +533,41 @@ mikrotik-manager/
## Contributing
-Contributions are welcome! Please open an issue before submitting a pull request so we can discuss the approach.
+Contributions are welcome. Please open an issue before submitting a pull request so we can discuss the
+approach — several recent features started as exactly that conversation.
1. Fork the repository
-2. Create a feature branch: `git checkout -b feature/your-feature`
-3. Commit your changes: `git commit -m "Add your feature"`
-4. Push to the branch: `git push origin feature/your-feature`
-5. Open a pull request
+2. Create a branch: `git checkout -b feature/your-feature`
+3. Commit your changes
+4. Push and open a pull request
---
## License
-This project is licensed under the **GNU Affero General Public License v3.0 (AGPLv3)** — see the [LICENSE](LICENSE) file for the full text.
+Licensed under the **GNU Affero General Public License v3.0** — see [LICENSE](LICENSE) for the full text.
-### What this means
+- You are free to use, modify and distribute this software.
+- If you run a modified version as a network service, you **must** make your modified source available
+ to users of that service under the same license.
+- Distributed copies and derivatives must also carry AGPLv3.
-- You are free to use, modify, and distribute this software.
-- If you run a modified version of this software as a network service (e.g., as a hosted web app), you **must** make your modified source code available to users of that service under the same AGPLv3 license.
-- Any distributed copies or derivatives must also carry the AGPLv3 license.
-
-This license was chosen to ensure that improvements made to this project — including those deployed as a service — remain open and available to the community.
+This license keeps improvements — including those deployed as a service — open and available.
---
-## AI Assistance
+## AI assistance
-This project was designed and built with the help of [Claude](https://claude.ai) by Anthropic. AI assistance was used throughout development — including architecture decisions, backend services, frontend components, the CI/CD pipeline, security configuration, and unit tests.
+This project was designed and built with the help of [Claude](https://claude.ai) by Anthropic. AI
+assistance was used throughout: architecture, backend services, frontend components, the CI/CD pipeline,
+security configuration and unit tests.
-We believe in being transparent about how software is made. The code has also been reviewed and tested using AI and is maintained by the project authors.
+We believe in being transparent about how software is made. The code is reviewed and tested with AI
+assistance and maintained by the project authors.
---
## Disclaimer
-This project is not affiliated with or endorsed by MikroTik. MikroTik and RouterOS are trademarks of SIA MikroTīkls. Use this software at your own risk. Always test configuration changes in a non-production environment first.
+Not affiliated with or endorsed by MikroTik. MikroTik and RouterOS are trademarks of SIA MikroTīkls.
+Use at your own risk, and always test configuration changes outside production first.
diff --git a/backend/package.json b/backend/package.json
index 86336a7..07d23be 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "mikrotik-manager-backend",
- "version": "0.23.2-beta",
+ "version": "0.23.3-beta",
"description": "Mikrotik Network Management Platform - Backend API",
"main": "dist/index.js",
"scripts": {
diff --git a/frontend/package.json b/frontend/package.json
index 1a196d8..e4089cc 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "mikrotik-manager-frontend",
- "version": "0.23.2-beta",
+ "version": "0.23.3-beta",
"private": true,
"type": "module",
"scripts": {
diff --git a/frontend/src/version.ts b/frontend/src/version.ts
index 848add2..856aa47 100644
--- a/frontend/src/version.ts
+++ b/frontend/src/version.ts
@@ -1 +1 @@
-export const APP_VERSION = 'v0.23.2 Beta';
+export const APP_VERSION = 'v0.23.3 Beta';