Resolves#87 — virtual APs removed directly on the device (WinBox/WebFig) kept
showing in the radios/interface lists, and neither refresh button cleared them.
Two distinct bugs:
1. Stale rows. The collectors upsert every interface found on the device but
never removed rows for interfaces that disappeared, so deleted radios lingered
forever. Added a prune to collectWirelessInterfaces, collectInterfaces (same
bug for deleted VLANs/bridges/bonds) and collectSecurityProfiles, deleting
stored rows whose names are no longer present on the device.
Safety: the prune only runs when the device list was fetched successfully.
Fetches that previously swallowed errors into an empty array now return null
and skip the prune, so a transient API failure can never be mistaken for
"everything was deleted". /interface/print already throws on failure.
2. Full sync skipped wireless entirely. collectAll() — what POST /devices/:id/sync
runs — never called collectWirelessInterfaces/collectSecurityProfiles, so a
manual resync could not refresh radios or clear stale ones; only the 5-minute
slow poll touched them. collectAll now collects wireless data for wireless_ap
devices, making the fix reachable on demand.
Verified on a live hAP-class AP: injected a ghost virtual-AP row, ran a sync, and
the ghost was removed while all 7 real interfaces (and offline devices' rows)
were retained. 114 backend tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves#86 — MikroTik Manager was attempting SSH connections to devices that
have no SSH credentials configured, flooding RouterOS logs with failed logins.
Cause: config snapshots (a separate feature from scheduled backups, enabled by
default, hourly) capture `/export` over SSH via BackupService.exportConfig, which
silently falls back to the API credentials for the SSH login. On devices where
SSH was never set up, that auth fails every interval (and on each restart), and
there was no UI switch to turn it off.
- PollerService.processConfigSnapJob now skips devices without SSH credentials,
so no SSH connection is attempted where SSH isn't configured. Manual "Capture
snapshot" / backup actions are unchanged (user-initiated, with error feedback).
- Add a "Configuration Snapshots" section to Settings → General: enable toggle,
interval, and retention (the feature was on-by-default with no UI control),
with a note that it runs over SSH and only on devices that have SSH creds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address the actionable findings from an adversarial review:
- 2FA enrollment now requires the current password (/totp/setup), so a
hijacked session or XSS can't enable TOTP and lock the real user out.
SSO-only accounts (no local password) are told to set one first. Frontend
collects the password before starting setup.
- OIDC config rejects default_role='admin': auto-provisioning unmatched users
straight to admin is almost never intended — admin must come from an explicit
group → role mapping.
- Rate limiter no longer fails fully open on a Redis outage. Redis calls are
bounded by an 800ms timeout (the client uses maxRetriesPerRequest:null, so
commands would otherwise hang, not throw), and on failure a per-process
in-memory sliding-window fallback keeps /login and other limited endpoints
throttled. Verified with Redis stopped: attempts throttle (429) instead of
hanging or passing unlimited.
Findings intentionally not actioned: the "API-token auth is fire-and-forget"
report was a false positive (Express awaits next(), which is called inside the
promise), and the webhook DB-pool and JWT-revocation items are either
overstated or a separate architectural decision.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address the open CodeQL code-scanning alerts:
- guestWifi voucher codes now use rejection sampling so the character
distribution is uniform (fixes js/biased-cryptographic-random).
- Scheduled-report recipient validation replaced the backtracking email regex
with a linear split-based check (fixes js/polynomial-redos).
- Add .github/codeql/codeql-config.yml excluding js/missing-rate-limiting:
rate limiting is applied globally via app.use('/api', rateLimitRedis) plus
per-route limiters, which CodeQL can't attribute to individual handlers, so
that rule fired ~38 false positives.
Remaining alerts are dismissed as accepted/false-positive (RouterOS MD5 API
login and self-signed cert acceptance are protocol-mandated; log sites already
strip CRLF).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add OpenID Connect single sign-on so the platform can authenticate against
any standards-compliant provider (Entra ID, Okta, Google Workspace, Keycloak,
Authentik, …) alongside local accounts. Resolves the feature request in #73.
Flow (backend-driven Authorization Code + PKCE):
- GET /api/auth/oidc/login builds the authorize URL and stores the PKCE/nonce
transaction in Redis (single-use, 10m TTL); the callback validates the ID
token via openid-client (JWKS signature, iss/aud/exp/nonce), resolves the
identity to a local user, and mints the platform's normal session JWT —
handed to the SPA via URL fragment. No sessions/cookies introduced.
Identity mapping (users table gains email/oidc_subject/oidc_issuer/
auth_provider; password_hash now nullable; partial unique indexes):
- First sign-in auto-provisions a user, or links to an existing local account
when the IdP email is verified and matches.
- Roles derive from a configurable IdP group -> role map with a default
fallback; existing users are never auto-demoted on a groupless login.
Configuration (admin-only, no env/redeploy needed):
- Stored as an app_settings row with the client secret encrypted at rest
(utils/crypto); masked in responses and excluded from GET /api/settings.
- New UI: Settings -> SSO / OIDC panel (issuer/client, claim + group->role
mapping, provisioning toggles, discovery test, copyable redirect URI), a
login-page SSO button, and an SSO badge in Users & Roles.
Security: local break-glass login preserved; PKCE + single-use state; email
linking requires email_verified; same-site-only post-login redirect.
Adds openid-client@^5 (CommonJS). 17 new unit tests (claim/role mapping,
username derivation, config secret round-trip/masking); 114 backend tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The v0.16.11 update-notice text used a raw apostrophe in JSX ("You're"),
which trips react/no-unescaped-entities and failed the frontend lint job.
Escape it as '. No visual change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update-availability fixes:
- The backend read its version from npm_package_version, which is unset
when the container runs `node dist/index.js` directly, so current was
always "0.0.0" and every release looked newer — the "update available"
banner never cleared. Read the bundled package.json instead
(routes/system.ts), falling back to npm_package_version.
- Move the update notification out of the top-of-app banner into the
Operations dashboard (a dedicated notice under the network-status hero),
shown only when genuinely behind and cleared once up to date.
Security banner:
- Replace the now-dead jwt_secret_default / encryption_key_default checks
with the current secrets_not_persisted warning (auto-generated secrets
that couldn't be persisted), matching the self-healing secrets model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the /terminal authorization fix: a full-codebase security
review surfaced several more issues, fixed here. All changes are designed
to roll out with no interruption to existing deployments — no forced
migration, no manual re-key, no boot failure.
Self-healing secret management (utils/secrets.ts):
- JWT_SECRET / ENCRYPTION_KEY auto-generate to strong values on first boot
and persist to the app_data volume (0600, outside the DB) when not set
to a strong value in the environment. Replaces the prior behavior of
running on the public repo defaults.
- decrypt() tries the current key plus every legacy/default key, so
existing device credentials keep decrypting after a key change; a
startup sweep re-encrypts stale rows forward (db/reencryptCredentials.ts).
- verifyToken() accepts the current signing secret plus prior *strong*
secrets (never the public defaults), so rotation doesn't invalidate live
sessions; sessions signed with a default secret require one re-login.
- Dockerfile pre-creates /app/data as the app user so the non-root
process can persist to a fresh named volume; app_data volume added to
both compose files; compose secret defaults changed to empty (auto-gen).
Authorization / input:
- Default Socket.IO namespace now requires a valid JWT (was unauthenticated
and broadcasting fleet-activity events); frontend sends the token and
drops the shared socket on logout.
- Webhook delivery blocks SSRF to loopback/private/link-local/CGNAT ranges
and pins the request to the validated IP (WebhookService.ts).
- Rate limit added to /auth/totp/verify; global mutation backstop on /api.
- Password policy (>=10 chars, letter+digit) enforced on set/change/create.
- system.ts used verifyToken as middleware (always 500); now requireAuth.
Hardening:
- CSP + HSTS/nosniff/frame-deny headers in nginx, scoped to allow the
OpenStreetMap tile/geocoding hosts the maps use.
Docs/tests:
- README secret section rewritten for the self-healing model.
- New unit tests for secret resolution, persistence, rotation, and
legacy-key decryption (97 tests pass).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Socket.IO /terminal namespace validated the JWT but discarded the
role, so a read-only viewer (403'd on every REST write) could connect
and open an interactive SSH shell on any managed device using the
device's stored router-admin credentials — a full bypass of the role
model (reported CVSS 9.6).
- Gate the namespace handshake by role: only admin/operator may connect;
viewers are rejected with "Console access denied for this role".
- Re-check the role at the start handler (defense in depth) and attach
the verified payload to socket.data.
- Audit-log terminal session opens (and rate-limit hits) to audit_log.
- Rate-limit shell starts per user (5 / 60s, in-memory sliding window).
- Frontend: gate the Terminal button (DeviceDetailPage) and dashboard
"Open terminal" quick action behind canWrite so viewers don't see it.
Validated on the local docker stack: viewer rejected from /terminal,
admin still connects; viewer remains 403 on POST /api/devices.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Firmware actions on the device Config tab (check/install RouterOS,
check/install RouterBOOT) and Firmware-page actions now invalidate the
shared react-query caches on success — devices list, device header,
firmware overview, dashboard "Things to handle", and Security Center
findings. Previously they only updated local component state, so other
screens showed stale "update available" data for up to 30s until their
own background refetch fired, even though the backend had already
persisted the new state.
Browser-verified: after a firmware check on the Config tab, /api/devices
refetches within the same second (sidebar observer) and the Devices page
fetches fresh data on mount; both screens agree immediately.
- New GET /api/firmware/changelog/:version proxies MikroTik's official
per-version release notes (download.mikrotik.com/routeros/<ver>/CHANGELOG)
with strict version validation and process-lifetime caching
- ChangelogModal: "What's new in X.Y.Z" on the device Config tab's
update box and on the Firmware page's latest-version cells opens the
changelog in-app, with an external mikrotik.com link and graceful
fallback when the CDN is unreachable
- Security posture: a pending RouterOS update is now a medium-severity
finding and a pending RouterBOOT upgrade a low-severity one; titles
are version-free so Common Findings aggregates the fleet into one row
- chore(deps): npm audit fix bumps morgan (GHSA-4vj7-5mj6-jm8m); both
workspaces audit clean
Validation: backend tsc/lint/92 tests, frontend tsc/lint/build/11 tests,
endpoint tested (valid/invalid/unknown versions), browser-verified on
Firmware page, device Config tab, Security Center, and device Security
tab; security review of the diff found no findings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UI/UX consolidation pass to reduce nav sprawl (~24 destinations -> ~17):
- Merge Router Settings + Switch Settings (copy-paste duplicates) into one
Network Services > Discovery & SNMP page with an All/Routers/Switches
scope selector; LLDP + SNMP status tables merge both fleets with a
type pill, apply fans out per scope
- Retire Routers/Switches overview pages; the Devices type filter now
lives in the URL (?type=AP|SW|RTR) and legacy routes redirect to the
pre-filtered view. Wireless overview kept (RF Health / Rogue APs are
unique content)
- Fold Wireless Clients into Clients: All/Wired/Wireless segmented
filter (URL-synced); wireless view swaps the Type column for SSID +
signal quality columns
- Regroup sidebar into Monitor / Network / Services / Operations
sections; Devices and Wireless are collapsible groups (auto-open on
active section, manual collapse always wins); Wireless group renders
only when wireless APs exist
- Rename Syslog -> Logging with a cross-link to live Events; group the
Settings tabs into Platform / My Account / Fleet sections
- Legacy routes (/routers, /switches, /routers/settings,
/switches/settings, /wireless/clients) redirect so bookmarks keep
working
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Guest WiFi (Hotspot): one-click captive-portal wizard that creates the guest
SSID (virtual AP per radio), VLAN/bridge segregation + L3 VLAN interface, pool,
DHCP, portal, rate-limited guest profile, and masquerade; vouchers with limits +
printable sheets; live guest sessions; walled garden.
Network intelligence: per-client device fingerprinting (vendor/hostname →
category) with a persisted per-client override on the detail page; rogue/neighbor
AP detection from stored AP scans (evil-twin flagging); Mist-style anomaly
insights (client-count/CPU vs same-hour 14-day baseline + error bursts) in the
Operations engine.
Firmware orchestration: staged wave-based RouterOS rollouts (canary first) with
pre-upgrade backup, reboot-ride-out, version verification, halt-on-failure, and
scheduling; live per-device progress + cancel.
Platform & automation: scoped API tokens (mtm_, read/write, hashed at rest,
Bearer auth in requireAuth); outbound webhooks (12 events, HMAC-signed) fired
from the alert pipeline + firmware orchestrator; scheduled email reports.
api-ssl: RouterOSClient negotiates TLS on port 8729 and accepts RouterOS's
default self-signed certs (rejectUnauthorized:false), so SSL-secured API devices
connect without manual cert trust. Fixes#57.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Global search:
- Clicking/Enter on a client result now opens that client's detail page
(/clients/<mac>) instead of dumping to the clients list.
Operations dashboard (new backend route /api/operations):
- GET /insights: server-side attention engine (offline, updates, CPU/memory
pressure, missing backups, recent outages, weak WiFi clients, co-channel
overlap, high TX retries, error events) + per-device capacity + merged
activity feed (config changes, audit actions, alerts; login noise filtered).
- POST /backup-all and /sync-all: fleet-wide actions over online devices.
- Quick Actions now execute: Run discovery, Backup all, Sync config, and
Open terminal (device picker -> live SSH TerminalModal); inline status.
- New cards: Capacity & health meters, Security posture rollup (reuses
per-device posture, cached), Recent activity feed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wireless RF Health (fleet Wireless page + per-AP Radios tab):
- Channel usage map across 2.4/5/6 GHz with co-channel overlap; operating
channel resolved live from /interface/wifi/monitor on the wifi package
(auto-channel radios report no frequency in /print).
- AP deployment density: client RSSI distribution on the -90..-30 dBm scale
with a coverage-gap warning.
- AP radio TX retries histogram (0%->35%+) with band selector, derived from
per-client CCQ (new wireless_radio_quality InfluxDB series).
- WiFi connectivity success funnel (Association/Auth/DHCP) derived from logs.
Per-port connected clients (Ports tab):
- New card lists clients physically connected to a selected port (host/MAC/
IP/VLAN/connection time, linked to client pages).
- Distinguishes physical clients from the raw MAC table: uplink/trunk ports
are auto-detected (topology neighbor, multi-VLAN MAC spread, or high MAC
count) and show an uplink explainer + "view MAC table" disclosure instead
of every reachable MAC.
- Added clients.first_seen ("connected since"); set once, preserved across polls.
Clients:
- Replace Previous/Next buttons with a "Show N entries" dropdown
(25/50/100/200, persisted to localStorage) and numbered page selection
with prev/next chevrons and ellipses for long ranges.
Syslog (Network Services):
- Drop the Single Device / All Devices mode toggle; always show the
fleet-aggregated view with coverage badges.
- Add a per-section device dropdown + "Add to device" button beside
"Add to All Devices" for targeting a single device (direct save).
- Edit / enable-disable / remove remain coverage-scoped, so single-device
management is preserved without the separate mode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Traffic by Application: donut + legend side-by-side, larger donut with
center total label, percentage in tooltip; card now sizes to its content
- New Inbound Packets area chart below the throughput chart (packets field
now summed per direction in /traffic/timeseries)
- New Flow Sources & Data Health card: collector listening state, packets
received, flows attributed, attribution rate, per-exporter flow/packet
counts and last-seen; low-attribution hint for NAT-in-path exporters
- /traffic/apps now returns per-app packets alongside bytes
- Restructured bottom row: Top Talkers (left) | Application + Data Health
stacked (right) for alignment
- Tightened Top Talkers row spacing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UI/UX refinements on top of v0.14.0-beta, all validated on the test server:
- Logo: new theme-aware compass iconmark (light/dark SVGs swapped via the
dark class), sized to 45px; favicon updated to match. Wordmark casing
corrected to "MikroTik Manager" (font size unchanged).
- Navigation icons enlarged 15 -> 17px.
- Dashboard: "Connected clients" line chart converted to a bar chart with a
category axis + light downsampling so bars stay readably thick across ranges.
- Clients: sorting is now server-side across the whole dataset (was sorting
only the current page, so Type/others appeared to do nothing on a uniform
page); table uses a fixed column layout + keepPreviousData so columns no
longer jump or blank out when sorting/paging. Backend clients route gains
whitelisted sort/dir params (safe numeric IP sort, deterministic tiebreaker).
- Connections tab: surfaces the live /ip/firewall/connection/tracking state
(on/auto/off pill) and clearer empty-state copy explaining auto-mode.
Lint/typecheck/build/jest green in both workspaces; npm audit --audit-level=high
clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A dedicated top-level Security section plus a Meraki/UniFi-grade firewall built
on the full RouterOS feature set.
Security Center (new /security page)
- Fleet hardening scores, per-device posture list, and a "Common Findings"
rollup aggregating identical issues across the fleet
- "Manage" deep-links into the device's Security tab (?tab= support added)
Firewall & NAT (per device + in Security Center)
- Friendly rule builder: Allow/Drop/Reject chips, Any/Address/Address-List
pickers, port presets, connection-state chips, logging, live plain-English
preview
- Address lists as reusable objects, referenced from rules
- Rule reordering (/ip/firewall/{filter,nat}/move); per-rule hit counters
(print stats) with dead-rule highlighting + reset
- NAT wizards: Port Forward / Masquerade / 1:1 NAT + Custom
- Safe-apply lockout guard: backend 409 on an unscoped input drop/reject that
would sever management; UI confirms before forcing (detectLockoutRisk, tested)
Access & security
- Security posture audit: graduated, de-duplicated scoring that never proposes
disabling the management service the platform connects through (no self-
lockout); switch-aware input-firewall severity; service hardening table
- Bandwidth: simple-queue CRUD + one-click "Limit this client" on client detail
- Active connections viewer
These L3 features are available on all managed RouterOS devices (routers,
switches, APs), not gated by device type.
Backend: DeviceCollector address-list/move/stats/connections/queues/service
methods; device routes for all of the above; firewallSafety + firewallSummary
utils with unit tests (19 new).
Security: bump nodemailer 8 -> 9.0.1 to clear GHSA-p6gq-j5cr-w38f (our
createTransport/sendMail usage is unaffected by the advisory's raw-option path).
Verified live against RouterOS devices; lint/typecheck/build/jest green in both
workspaces; npm audit --audit-level=high clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI Security Audit (npm audit --audit-level=high) was failing. Resolved all
high-severity advisories via non-breaking transitive bumps (npm audit fix,
no --force, no direct-dependency or source changes):
- frontend: form-data, ws (via socket.io-client), @babel/core,
react-router-dom, js-yaml — now 0 vulnerabilities
- backend: form-data, ws (via socket.io) — 0 high-severity remaining
Only package-lock.json files changed plus the version bump. Verified both
workspaces: npm ci, lint, type-check, build, and backend jest (73 tests)
all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two per-client usage charts exposed different time windows: "Wireless
Usage" offered 2h/24h/7d while "App Traffic (NetFlow)" offered 1h/24h/7d/30d.
Worse, selecting 1h (or any range with no flows in window) made the whole
App Traffic card unmount — including its range buttons, so it couldn't be
recovered without reloading.
- AppTrafficCard now uses 2h/24h/7d (default 24h), matching Wireless Usage
- Card visibility latches on first non-empty result, so switching to an
empty range shows an inline "No traffic data yet for this range" state
instead of unmounting the card (latch derived via guarded setState in
render — no effect)
- Backend /api/traffic/* now accepts the '2h' range (rangeToFlux +
windowForRange: 2h -> 2m buckets); previously '2h' silently fell back to 24h
Verified in the live stack with Playwright: ranges render as 2h/24h/7d (no
1h/30d), and clicking 2h on a client with data only outside the 2h window
keeps the card mounted with an empty-state and recovers on switching back.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Dashboard "Top talkers" card and the Traffic page previously used
different data sources: the card ranked clients by device-reported
since-connection byte counters (wireless-centric, reset on reconnect, no
time window), while the Traffic page uses NetFlow-measured bytes.
The card now prefers the NetFlow top-clients data (last 24h — identical
numbers to the Traffic page) and falls back to the legacy counters only
when the collector has no data, with the source labeled in the card
header ("by bytes · 24h" vs "since connection"). Rows now click through
to the client detail page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Configuration History:
- Per-device snapshots from the full RouterOS /export (.rsc) — config-only, so
counters/operational state no longer cause phantom changes; deduped by content hash
- New Config History tab: snapshot timeline, readable line diff, on-demand Capture
(with honest 'no changes' feedback), one-click rollback via the linked .rsc backup
- Snapshot and its backup are one artifact: badged 'Config Snapshot' on the Backups
page, FK ON DELETE CASCADE keeps them in lockstep; delete is gated by a warning modal
- New config_drift alert event (off by default); periodic snapshot poll with
configurable cadence/retention (config_snapshot_interval_min/_retention)
- New endpoints under /api/config-history (list/get/diff/capture/rollback/delete)
Scheduled Backups:
- Replaced the raw cron textbox in Settings with Frequency (Daily/Weekly/Monthly)
+ time + day pickers; cron is generated/parsed behind the scenes
- Fixed cronMatchesNow to evaluate day-of-month/month/day-of-week (it previously
only checked minute/hour, so weekly/monthly schedules ran every day)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CI Security Audit (npm audit --audit-level=high) started failing on all
new PRs after a critical advisory was published against vitest <4.1.0
(GHSA-5xrq-8626-4rwp — Vitest UI server arbitrary file read/exec). The
remediation chain also requires vite to move off the vulnerable esbuild range.
These are devDependencies (test/build tooling) and do not ship in the
production image, so there was no runtime exposure to deployed users — but the
audit gate blocks the pipeline, so bump them:
- vite ^5.0.11 -> ^8.0.16
- vitest ^3.0.5 -> ^4.1.8
- @vitejs/plugin-react ^4.2.1 -> ^5.2.0 (vite 8 peer compatibility)
Vite 8 ships the rolldown bundler, which requires rollupOptions.output
.manualChunks to be a function rather than an object — converted accordingly,
with explicit precedence so @xyflow/react (whose name contains the substring
"react") lands in the `flow` chunk instead of being pulled into `vendor`.
Verified locally: npm ci, npm run lint, tsc --noEmit, npm run build, and
npm test (vitest 4.1.8, 3/3 passing) all pass; npm audit reports 0
vulnerabilities.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Root cause: backend filtered switches by status='online', so a mid-poll-cycle
device (briefly 'unknown') caused the endpoint to return [] — and the frontend
rendered nothing when the array was empty.
- Backend: query ALL switch devices (not just online ones); unreachable devices
already fall through to the Promise.allSettled error path and render with an
error badge, so no data is lost
- Frontend: replace silent `length > 0 &&` guard with a three-way ternary that
shows a "No managed switches found" message when the array is empty, so the
user always sees feedback instead of a blank section
- Frontend: add staleTime: 60_000 to the SNMP query to prevent the hair-trigger
background refetch that raced with the poller on every navigation
Backend: GET /api/system/version-check fetches frontend/package.json from
the main branch on GitHub, caches result in app_settings for 24 hours,
and compares semver against the running version. Fails silently (no banner)
for air-gapped or firewalled deployments.
Frontend: dismissible blue update banner in SecurityBanners (rendered
between TopBar and main content) — only visible when a newer version is
detected. Shows target version and docker compose update command.
- Packet capture: RouterOS sniffer + SFTP download to PCAP for Wireshark analysis
- Bandwidth test: RouterOS /tool/bandwidth-test with TX/RX Mbps result display
- Both exposed in the Tools tab on device detail pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- aggregateRules/aggregateActions: deduplicate coverage per device using a per-device seenKeys set,
preventing a device with duplicate topic|action rules from inflating the coverage count above total
- executePush (add path): skip devices that already have an identical rule/action before pushing to
all devices, preventing duplicate RouterOS logging entries from being created in the first place
- TopBar: remove hardcoded decorative 'live' indicator that had no backend connection logic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 'Copy VLANs from Another Switch' — a 3-step modal available on the
VLANs tab of any switch device. Step 1 selects a source switch and which
VLAN IDs to copy (VLAN 1 deselected by default with a management risk
warning). Step 2 lets the user assign interfaces on the target device
per VLAN: clicking a port chip cycles it through None → Tagged → Untagged
→ None, with a live summary and bridge picker per VLAN. Conflict detection
identifies VLANs already present on the target and offers per-VLAN
Skip/Overwrite with the port grid disabling itself when Skip is active.
Step 3 shows the full plan before a single apply call executes all
operations over one device connection and reports per-VLAN results.
Also fix CPU load display on the Devices page for hardware switches that
report 0% CPU via ASIC offloading — the previous filter (v > 0) silently
dropped all valid zero readings; now filters on field presence instead.
- DevicesPage: add statusFilter + typeFilter to filtered useMemo deps (was a real bug — filter chips had no effect until unrelated re-render)
- DevicesPage: refactor cpuHistories useMemo to pure Object.fromEntries form
- DashboardPage: suppress react-hooks/purity for intentional Date.now() chart domain
- Sidebar: move GroupHeader from inline arrow fn to module-scope named function (fixes react-hooks/static-components)
- axios: bump ^1.6.5 → ^1.16.0 to clear high-severity CVEs (prototype pollution, CRLF injection, NO_PROXY bypass)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New Syslog page under Network Services exposes RouterOS logging actions
and routing rules with full CRUD. Logging actions define destinations
(remote syslog server, memory, disk, echo); logging rules define which
topics route to which action.
Single Device and All Devices modes are functionally identical — All
Devices applies changes across every online device in parallel and
shows per-entry coverage badges indicating which devices have each
action or rule. Toggle enable/disable works in both modes.
Remote actions display address:port with facility/severity in a single
Destination column; memory/disk/echo rows show no irrelevant remote
fields. Syslog KPI card added to the Network Services overview.
Add a right-justified refresh interval dropdown above the client table
with options for Not Updating, 30 s, 1 min, and 3 min. The chosen
interval controls both React Query polling and WebSocket-triggered
invalidations — selecting Not Updating fully freezes the list until
page reload. Preference persists via localStorage.
RouterOS vs RouterBOOT distinction:
- Rename "Firmware Updates" → "RouterOS Updates" everywhere (Config tab,
dashboard banner, devices list tooltip) to accurately reflect what is
being updated
- Add RouterBOOT bootloader upgrade support as a distinct operation:
new DeviceCollector methods checkRouterboardUpgrade() and
installRouterboardUpgrade() using /system/routerboard/print and
/system/routerboard/upgrade
- New API endpoints: POST /api/devices/:id/check-routerboard and
POST /api/devices/:id/install-routerboard
- Daily poller now also checks RouterBOOT upgrade status while already
connected during firmware check cycle, with first-discovery alerts
Config tab update panel:
- Combine RouterOS and RouterBOOT into a single "Software & Firmware
Updates" card with two subsections separated by a divider
- Pre-populate both sections from stored DB state on load — no longer
requires a manual "Check" click to see a known pending update
- Enforce update order: when both are pending, show a blue info banner
explaining RouterOS must be installed first, and disable the
RouterBOOT "Upgrade & Reboot" button until the RouterOS update clears
- Fix GET /api/devices/:id to include firmware_update_available,
latest_ros_version, routerboard_upgrade_available, upgrade_firmware_version
(these were missing from the detail endpoint)
Dashboard & devices list:
- Combined amber banner now shows two distinct rows: RouterOS (N) with
device links and RouterBOOT (N) with device links — one dismiss for both
- Devices list: amber ArrowUpCircle = RouterOS update, blue Cpu icon =
RouterBOOT upgrade, each with distinct tooltips
- Fix GET /api/devices (list) to include routerboard_upgrade_available and
upgrade_firmware_version (these were missing, causing RouterBOOT
notifications to never appear on the dashboard)
DB migration:
- Add routerboard_upgrade_available BOOLEAN DEFAULT FALSE
- Add upgrade_firmware_version VARCHAR(20)
Discovery system fixes:
- Fix duplicate entries: replace single-key (mac || ip || identity) dedup
with three cross-referenced indexes so a neighbor seen via LLDP by one
device and via MNDP by another always merges into one row
- Fix offline devices persisting: handleDeviceFailure now deletes
topology_links for the offline device (same pattern as clients)
- Add periodic stale-link sweep every 15 min that removes topology_links
rows older than 20 min as a safety net for devices that go offline
without cleanly triggering handleDeviceFailure
Documents CORS_ORIGIN env var, credential preset operator access control,
and server-side bulk device add job in README. Bumps version across all
four locations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* devices: hide discovered neighbors that are already managed
Discovered Devices showed every unresolved MikroTik neighbor, even when
the neighbor was obviously the same device a managed node is reporting
through a different link / MAC / alias. Since we already know every
managed device interface MAC, primary IP, and system identity, we can
flag those rows.
- /api/devices/discovered now annotates each row with duplicate_of_device_id
and duplicate_of_device_name when the neighbor matches a managed device
by interface MAC, primary IP, or system identity.
- Add a Hide duplicates of managed devices toggle (default on) above the
table, with a count of how many matches were found. When enabled, the
list hides matched rows; when disabled, matches stay visible but are
dimmed and tagged Already managed so the origin is clear.
- Suppress the Add to Manager action for rows that are already a
managed device, since adding them would create a second record for the
same hardware.
Co-authored-by: Denis <denis-ev@users.noreply.github.com>
* devices: safer duplicate handling, try-all safeguards, and timezone/firmware fixes
Co-authored-by: Denis <denis-ev@users.noreply.github.com>
* Non-blocking follow-ups: parsePort util, tests, bulk-add queue, preset rate limit
Co-authored-by: Denis <denis-ev@users.noreply.github.com>
* fix(security): tighten CORS, Redis rate limits, and preset access controls
Require explicit CORS origins in production, align Socket.IO CORS with HTTP,
default Docker Compose to local HTTPS origins, and remove the in-memory
mutation limiter in favor of Redis. Also add operator-use flags for credential
presets in the UI and harden bulk-add job access and client error messages.
Co-authored-by: Denis <denis-ev@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>