4cc8c84ea68deee0855846a4d55625aac22b48a3
140
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4cc8c84ea6 |
fix(wireless): model channel usage as spectrum, not channel numbers (v0.23.5-beta)
Closes #98, #97. The Channel Usage panel treated a radio as occupying a channel *number* and coloured a channel amber when two radios shared it. Both halves are wrong, and the result is not merely incomplete — it gives actively bad planning advice. Channels are not slots. In 2.4 GHz the centres are 5 MHz apart while a channel is 20 MHz wide, so an AP on channel 1 occupies about 2402-2422 MHz and pollutes channels 1 through 5. Showing 2 through 5 as free is why 1/6/11 exists as the only three-way non-overlapping plan, and a model built on channel indices cannot express that. Sharing a channel is also not the problem. Two radios on exactly the same channel hear each other and take turns; CSMA/CA does its job and the cost is airtime. Two radios partially overlapping cannot decode each other, transmit over one another, and the cost is corruption. Partial overlap has to rank above co-channel, and it was not represented at all. Occupancy is now modelled in megahertz and overlap classified by how two ranges relate. On the local fleet, a 40 MHz radio on channel 1 is correctly shown bleeding 5 MHz into two channel-6 radios — which are themselves correctly identified as co-channel with each other rather than as the worst problem in the band. The model lives in the backend (utils/rfSpectrum.ts) with 26 tests rather than in the browser, because it is correctness-critical and the frontend has no test runner. Tests pin the arithmetic that is easy to get wrong: 1/6/11 is clear, 1/3/5 is three partial overlaps, channels 4 and 8 abut exactly rather than overlapping, a 20 MHz radio nested inside an 80 MHz one is partial rather than co-channel, and a lone radio is clear rather than co-channel with itself. The panel now draws real spectrum to scale, stacking overlapping radios so each stays visible, with the clash list on hover naming the other radio and how many megahertz they share. Width is taken as the widest entry of a RouterOS capability list such as 20/40/80mhz, since that is the spectrum the radio may occupy and therefore what a planner must treat as unavailable. Per-location scoping (#97): analysis runs per CAPsMAN controller, with a selector, because two APs on channel 6 in different buildings are not a conflict. Radios with no controller are analysed together as standalone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e10f7ba568 |
docs: split reference material into docs/, keeping the README a shop window (v0.23.4-beta)
Documentation only — no functional change.
The README was carrying two jobs for two different readers. Its Reference section was
the single largest block at 154 lines, and nobody deciding whether to try the project
reads an InfluxDB environment variable table — while the people who need it were
scrolling past the pitch to reach it.
Split along that seam: anything you would read before installing stays in the README,
anything you would read after installing moves to docs/.
README.md 430 lines sell, prove, install, feature overview
docs/change-guard.md 193 safety system, settings, guard states
docs/architecture.md 116 data stores, polling model, module layout
docs/capsman.md 84 role model, radio matching, current limits
docs/alerting.md 81 rules, channels, webhooks, maintenance windows
docs/api.md 77 scoped tokens, preflight, scripting
docs/configuration.md 76 environment, secrets, rotation, TLS
docs/sso-oidc.md 71 identity provider setup and behaviour
docs/README.md 20 index
In-repo rather than a documentation site, deliberately. A change to Change Guard's
semantics can edit its documentation in the same commit as the code — which, given how
often that behaviour has been refined recently, is the property that actually matters.
A static site generator can point at docs/ later without moving a file.
The Change Guard pitch stays on the front page: the problem framing, the verified
recovery timeline and the quoted prediction warning are what make someone care, and a
reference page is the wrong home for them.
Three sections are new rather than relocated, written because the split exposed the gaps:
- architecture.md did not exist. It now records why there are three data stores, the
polling cadences, and which modules are pure functions over a snapshot and why.
- Scoped API tokens shipped without usage documentation. api.md shows preflighting a
change from a script and reading the guard block afterwards.
- change-guard.md records why ip-on-bridge-port is informational rather than critical
on RouterOS 7 — reasoning that previously existed only in a commit message.
Every internal anchor and cross-file link verified to resolve, including anchors into
other documents. No content dropped, checked by keyword comparison against the previous
single file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2201759aff |
docs: restructure README around what the project actually does differently (v0.23.3-beta)
Documentation only — no functional change. The previous README opened with eighty lines of screenshots before saying what the software was, then listed twenty-five feature sections in no particular order. Change Guard — the one capability nothing else in this space has — sat fifth, under a heading that gave no hint of it. Reordered around the questions a new visitor actually asks: what is it → why is it different → show me → how do I run it → full features → reference "What makes it different" now leads with the problem rather than the feature: that RouterOS applies every command immediately, with no transaction and no cross-object validation, and that MikroTik's own documented advice for VLAN filtering is to have a serial console ready. Change Guard, lockout prediction and Config Health follow as answers to it, with the verified auto-revert timeline and a real prediction warning quoted rather than described. The twenty-five flat sections are grouped into seven, safety first. Reference material — environment variables, Change Guard settings, secret management, OIDC setup, tech stack, project layout — is consolidated at the end instead of being interleaved with features, and the deepest auto-revert mechanics move into a collapsible block: present for anyone relying on it, out of the way of a skim. Added a table of contents; the document was seven hundred lines with no navigation. Headings that anchors point at use "and" rather than an ampersand, because GitHub slugifies an entity into a double hyphen and six links would have broken silently. No content dropped: every feature, table and screenshot from the previous version is still present, verified by keyword and image-path comparison. 713 lines to 573, entirely from removing repetition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9128573801 |
chore: stop tracking .vscode editor settings (v0.23.2-beta)
Personal editor preferences — font size and colour theme — were swept into the previous commit by git add -A. Committing them would override every other contributor's editor configuration. Removed from tracking and .vscode/ added to .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0258ee47dc |
fix(wireless): attribute CAP radios to the access point, not the controller (v0.23.1-beta)
Third round of #94 field testing. The placement bug survived v0.23.0 because the previous fix addressed the wrong mechanism. Cause A CAPsMAN controller mirrors every CAP interface locally, so a CAP's radio MAC legitimately appears on two devices: the CAP that owns the hardware, and the controller that mirrors it. The MAC index was single-valued and first-wins over an unordered UNION, so each radio landed on whichever row the database happened to return first. On the reporter's fleet six radios attached to a router with no radios at all, while their siblings attached correctly — the same access point appearing in two places, and liable to move between polls. v0.23.0 excluded the controller from *prefix* matches only. These are exact matches, so the exclusion never applied. Consecutive MACs on one physical AP resolving to different devices is what gave it away. Fix The index is multi-valued: a MAC maps to every device carrying it. A radio the controller reports as local:false lives on a CAP by definition, so the controller is rejected as a candidate at exact strength as well as prefix — an exact match against it only ever means the mirror. Attribution now runs through matchRadiosToDevices, which was previously tested but unused while the collector kept its own copy of the logic. With no radios left attributed to it, a controller that has none of its own stops appearing in the access point list, which was the reporter's follow-up point. SSID on provisioned interfaces A CAP holds no configuration, so its interface rows carry a generic name and a null SSID. The controller knows it, on its mirror of the same interface. Radios now record the provisioned SSID from there, and the table is driven by the controller's radio list rather than the CAPs' own rows — the controller is the only side that knows the SSID, channel and client count together. Band and frequency columns removed Both were empty while the channel column beside them read 5680/ax/eCee/D. The channel string already carries the band and the extension layout; parsing it properly is its own task, and half-parsing it produced empty columns next to populated ones. Requested by the reporter. 75 tests across the CAPsMAN and topology modules, including fixtures built from the reporter's MAC set: both radios of one AP resolve to that AP, order-independently, with a genuinely local radio still landing on the controller and an unknown CAP left unattributed rather than absorbed. Reproduced the mirror scenario against the local fleet before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
82efcaa371 |
fix(wireless,topology): CAPsMAN write guards, MAC neighbour resolution, unified panel (v0.23.0-beta)
Closes #95. Addresses the second round of #94 feedback. Includes a regression introduced in v0.22.0. Wireless write paths were not CAPsMAN-aware (regression) v0.22.0 widened getAP() and the bulk-SSID selector to include cap/controller roles so a controller — normally tagged a router — would stop being invisible to the wireless section. Those same helpers gate the write endpoints, so the change also allowed writes to devices whose radios a controller owns. Reads needed widening; writes needed a guard, and did not get one. Five paths now refuse or skip provisioned radios: bulk SSID deployment, single interface create, interface edit, interface delete, and guest network setup. Delete was the worst of them — RouterOS will not remove a provisioned interface, but the cached row was deleted anyway, so it vanished from the UI until the next poll brought it back. The check is shared: partitionRadios() splits physical radios into locally controllable and controller-owned, assertNotCapsmanManaged() refuses an edit to a provisioned interface and names the controller. Neighbours identified only by MAC (#95) Topology resolved a neighbour to a managed device by IP address. A trunk port has no IP, so LLDP across one reports a MAC and an identity but no address — and those links stayed unresolved, drawing an external node beside the very device it already had on the map. Two directly connected devices appeared four times. Resolution by MAC now runs first, before address matching, because a MAC is hardware identity and unique fleet-wide where an address is not. This is the same join the CAPsMAN radio matching uses. CAPsMAN panel, from the reporter's testing - Radios and provisioned interfaces were two tables describing the same things from different angles; they are now one, per interface, with the access point, SSID, band, frequency, channel, clients and status. - Provisioning rules sit inside the configuration they apply. Rules referencing a configuration we did not collect are listed separately rather than dropped, since that usually means a stale reference. - The Wireless header reported zero clients while the Clients page reported 32: it summed the CAP-local count, which is empty under central forwarding. It now takes the greater of that and the controller's per-radio counts, so a controller that is also a CAP is not double-counted. - A remote radio could be attributed to the controller by the five-octet prefix fallback when the two shared a MAC block. Prefix matches now refuse the controller for a non-local radio; exact matches are still honoured. Radios tab followed device_type device_type is user-supplied tagging, and a router with radios had no way to display them. The tab now follows the detected wifi_role, which is a fact about the hardware. 48 tests on the CAPsMAN module, 22 on topology, including a fixture built from the device output in #95. Verified against the fleet: topology output unchanged, so MAC resolution merged nothing it should not have; guards correctly permissive on a standalone AP. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e15556b601 |
fix(wireless): CAPsMAN panel corrections from field testing (v0.22.1-beta)
All five points raised on #94 after the reporter tested v0.22.0 against a live CAPsMAN deployment. Two were regressions in that release. Operating channel instead of a capability list (mine) current_channel was stored from /interface/wifi/radio's current-channels, which is every channel the radio supports rather than the one it is using. Measured on a wAP ax: 535 characters on the 2.4 GHz radio and 3037 on the 5 GHz one — the "two kilobytes of text" reported. /interface/wifi/monitor gives the operating channel (5500/ax/Ceeeeeee/D), and carries state, peer counts and TX power too, all now shown on the radio chip. Client counts that were zero everywhere (mine) The panel read registered_clients from the CAP's own wireless_interfaces rows, which are empty when traffic is processed centrally by the controller. Monitor's registered-peers is not the fix either, and testing caught that before it shipped: clients register on the virtual AP carrying the SSID, not on the physical radio, so monitor on a physical radio reports zero on a busy access point. On the test AP, wifi1 and wifi2 both read 0 while wifi3-wifi6 held all ten clients. Counts now come from the registration table, with each entry attributed to its radio by following master-interface up to the interface owning a radio-mac. That field requires /interface/wifi/print detail — it is absent from the plain listing. Verified against real hardware: ten registrations, attributed 8 and 2 across the two radios, none dropped. "no network" on remote interfaces The Network column resolved from the AP's own bridge ports. A CAPsMAN-provisioned interface is not a local bridge port, so nothing matched and the UI asserted the interface had no network at all. It now falls back to the datapath — inline fields or a named/.id reference — and labels the source. Where the datapath names no bridge it says "via controller" rather than claiming no network. Status labels "Serving Clients" claimed something the underlying field does not say: running means the radio is up, not that clients are connected. Now "Running" and "Enabled, not running". Table title "SSIDs / Wireless Interfaces" conflated two different things; the table lists interfaces, virtual and physical. Now "Wireless Interfaces". Guest WiFi on CAPsMAN-provisioned radios Raised by the reporter as an aside, and it was a real hazard already shipping. setupGuestNetwork created virtual APs locally on whatever physical radios it found, with no awareness that a controller might own them — the SSID would be ignored or overwritten on the next provision while the UI reported success. Provisioned radios are now skipped with an explanation, and if every radio is provisioned the setup is refused, pointing at the controller instead. 37 unit tests on the CAPsMAN module. No regression on a fleet without a controller: local bridge-port resolution unchanged, roles still correct. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
14bab24c7e |
feat(wireless): CAPsMAN awareness and read-only controller view (v0.22.0-beta)
Addresses #94, phases A and B. Write operations are deliberately out of scope. An access point provisioned by CAPsMAN holds none of its own wireless configuration, so /interface/wifi/print returns rows with the configuration.* and security.* fields absent. normalizeWifiInterface mapped nothing, stored nulls, and the UI rendered an AP with no SSID, security or band — indistinguishable from one that is simply broken. Phase A — stop showing blanks Devices are classified standalone / cap / controller / controller_cap from /interface/wifi/capsman and /interface/wifi/cap. Both toggles can be on: MikroTik supports a controller that also runs its own radios under itself, and the issue calls that case out, so it gets its own role. Provisioned interfaces are marked and labelled in the Radios tab with the controller's MAC and an explanation that the blank fields live on the controller. SSID, mode and channel are recovered from RouterOS's status line where present, so the AP is described rather than blank. Role is the signal, not missing data: an AP whose SSID is empty because the controller owns it looks identical to an unconfigured one, and only the toggle distinguishes them. A CAP can still hold locally-configured interfaces alongside provisioned ones, so per-interface state is checked rather than assuming the whole device. Phase B — read-only controller view Controllers now collect the radios they manage, named configurations, and provisioning rules, surfaced as a CAPsMAN panel on the Wireless page grouped by the access point each radio physically lives on. CAPs are matched to managed devices by radio MAC from the controller's own /interface/wifi/radio list, not by resolving addresses through neighbours as the issue proposed. A MAC is hardware identity and globally unique, so unlike an address it cannot mean two devices on two segments — the failure fixed in v0.20.1. A radio MAC often differs from the interface MAC in the final octet, so lookup falls back to a five-octet prefix while keeping the OUI exact. A CAP that is not a managed device is surfaced rather than hidden; that is usually the reason an AP appears to be missing from the fleet. The wireless section required device_type = 'wireless_ap' in five places, which hid a dedicated controller — normally classified as a router — from every wireless view. Collection was gated the same way. Both now include CAPsMAN roles, and the collector probes an unknown device once, records the role, and skips cheaply forever after. Not included Provisioning, config edits and interface enable/disable. A CAPsMAN configuration applies to every AP bound to it simultaneously, which is the fleet-wide version of the failure Change Guard exists to prevent. It needs the same prediction and revert treatment rather than being bolted on. Legacy /caps-man (pre-RouterOS 7) is a separate API tree and is not covered. Verified: API shapes probed against RouterOS 7.23 rather than taken from docs. 24 unit tests on the pure parsing and matching. No CAPsMAN hardware here, so the read path was exercised against a seeded controller with a local radio, a matched two-radio CAP and an unmanaged CAP; grouping, provisioning and SSID resolution all came out correct. Confirmed no regression on a fleet with no controller: roles detected correctly, no interface falsely flagged, wireless views unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
19617e8357 |
feat(alerts): ntfy.sh notification channel (v0.21.0-beta)
Closes #93. Routing push notifications through email ingestion was a workaround. ntfy is now a first-class channel alongside Slack, Discord, Telegram and email, using the same alert_channels row and the same dispatch switch. Built to cover the whole plausible configuration space rather than waiting on the reporter's answers, so no config migration is needed once they arrive: - server_url is operator-supplied and defaults to https://ntfy.sh, so a self-hosted instance works — the likely case for infrastructure alerting - both of ntfy's auth schemes: an access token (preferred; scopable and revocable) or username/password, with none required for an open topic - optional click_url deep-links each notification to the device it refers to Published as JSON to the server root rather than POST /{topic} with header metadata: the header form is ASCII-only and these titles carry per-event emoji. Severity maps onto ntfy's priority scale so the channel stays worth being woken by — 4 (breaks through do-not-disturb) for device_offline, log_error and CPU/memory pressure; 2 for recoveries and discoveries; 3 for everything else. The event type is attached as a tag so a client can be filtered to wake only for chosen events without needing a channel per event. Secrets follow the existing pattern: token and password are added to SENSITIVE_KEYS, so they are masked on read and the mask placeholder is restored to the stored value on write rather than overwriting it. The type column is a CHECK constraint rather than a lookup table, so widening it means dropping and recreating the constraint; done idempotently for existing installs. Note on trust: server_url lets an admin point the channel at an internal address. That is the same exposure the existing Slack and Discord webhook URLs already have, and only an admin can configure any of them. Restricting it would break the self-hosted case this feature is mostly for. Verified against the real ntfy.sh — a test alert arrived with the correct title, emoji, body, priority, tag and click URL; masking verified to hide the token on read while preserving it across a save that did not retype it. 13 unit tests cover priority mapping, both auth schemes, URL normalisation, and rejection of a missing topic or non-http server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b03ea87f11 |
fix(topology,ui): spurious cross-segment links, CPU rounding, serial column (v0.20.1-beta)
Closes #90, #91, #92. #90 — Spurious topology links Topology treated IP addresses and system identities as unique across the whole fleet. They are unique only within a broadcast domain, so an installation with physically separate segments that each reuse 10.2.0.0/24 saw devices in one segment joined to devices in another. The reporter pointed at shared-segment detection, and that is one of four sites with the same flaw — but not the first. Step 2 resolves a neighbour IP to a managed device from a global first-wins map, so a reused address silently resolves to whichever device was loaded first; the union-find in step 6 then merges on the resulting device id, a correct-looking key derived from bad data. Fixing only step 6 would have left the reported link in place. Addresses and identities are now scoped to the device that reported them wherever they name a neighbour, so they still collapse one device's duplicate sightings but can never join two devices. Resolution and LLDP-coverage suppression additionally require the address to be unambiguous fleet-wide. MAC addresses and resolved device ids are unaffected — they identify hardware, and genuinely shared segments still merge on them. Ambiguity detection deliberately ignores MAC divergence. A switch assigns a MAC per port, so a device with two uplinks is reported under several MACs; an earlier version of this fix treated that as a conflict and refused to resolve ordinary multi-port hardware. Caught on a live fleet, where three sightings of one CRS510 disagreed on MAC but agreed on identity. The signals used instead are an address claimed by two managed devices, or sightings that disagree about the neighbour's identity. The graph builder is extracted from the route into a pure module with 16 tests, including a fixture of the reporter's five-device, three-segment network. It had no test coverage before, which is why a flaw this systemic went unnoticed. Verified against a live fleet: graph output is identical before and after, and the ambiguity list is empty when addressing is clean. #91 — CPU load not rounded The devices table rendered the raw value from a 5-minute Influx mean, so a device idling between 1% and 2% displayed as 1.3333333333333333%. Rounded for display; the sparkline keeps the unrounded series so its curve stays smooth. #92 — Serial number in the device table Serials were already collected, stored, returned by the API and matched by global search, so this is presentation only: a sortable SERIAL column, and serial added to the table's filter predicate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
93ff1fbc53 |
feat(safety): Config Health audit, verified auto-revert, VLAN cache fixes (v0.20.0-beta)
Completes the lockout-prevention work: a standing audit for configurations
RouterOS accepts but that do not work, an empirical answer on /system history
undo, and corrections to the VLAN cache the editors are built on.
Config Health (new)
Eleven rules over a live snapshot, encoding MikroTik's documented Layer2
misconfigurations plus the latent lockout cases: IP or VLAN interface on a
bridge slave port, a bridged VLAN interface, a bond slave that is also a
bridge port, MTU above L2MTU, a multi-VLAN entry with untagged ports,
bridges competing for hardware offload, a port in no VLAN on a filtering
bridge, a PVID that frame-type=admit-only-vlan-tagged makes inert, a VLAN
interface whose VLAN the bridge is not tagged in, duplicate addresses, and
management that survives only on a dynamic entry. Each finding names the
consequence, the fix, how long it has been present, and the vendor doc.
Runs hourly, surfaces on the Security tab and the dashboard.
Calibrated against hardware: RouterOS 7 relocates an address off a bridge
slave port onto the bridge (actual-interface), so the stock defconf layout
works. Reporting that as critical would have fired on a default install; it
is informational unless RouterOS did not relocate it.
Change Guard: severed-connection handling
Proven on a CRS by deleting its own management address — the switch locked
itself out and self-restored unattended in ~3 minutes, twice.
That test exposed a real defect. withSafeApply treated any exception from
the apply step as "the change failed", disarming the guard and rethrowing.
But the most dangerous changes sever the connection carrying them, so the
call times out even though the change applied. The guard was stripping the
safety net off a device that had just gone silent, and reporting HTTP 500
"Read timeout" instead of telling the user the device was restoring itself.
Only luck saved it: the disarm needed the same dead connection.
Reachability now decides, not the exception. Unreachable leaves the revert
armed and returns auto_reverting; still reachable means a genuine failure,
disarmed so nothing reboots for nothing. committed_at is no longer stamped
on failed or reverted guards; resolved_at records completion instead.
/system history undo probe
probeUndoCapability tests the device rather than assuming. On 7.23.x over
the binary API: /system/history/print works, /system/history/undo does not
exist, /undo numbers= rejects the parameter, bare /undo works. Undo is
therefore untargeted, and cannot replace auto-revert since a dead-man switch
must fire after contact is lost. Binary restore stays the default.
VLAN cache correctness
- vlans keyed on (device_id, bridge, vlan_id); the old key collapsed the
same VID on two bridges
- vlan-ids ranges expanded instead of parseInt truncating "10-20" to 10
- both VLAN tables pruned, but only when the device returned an
authoritative list, so a failed read leaves the cache stale not empty
- bridge_vlan_entries.vlan_ids written and .tagged derived rather than
hardcoded false, which had left the port editor unable to see a trunk
- removeBridgeVlan matched by expanded VIDs; the ?vlan-ids= server filter
compares raw strings, so deleting one VID of a range silently no-opped
while reporting success. Multi-VLAN entries are refused in the route,
before the guard arms, since a mid-apply failure would cost a reboot
- interfaces.vlan_filtering promoted to a column
Membership is deliberately the STATIC tagged/untagged, never current-*.
These tables back the editors and updateBridgeVlan is remove-then-add, so
whatever is shown is written back; the effective view baked RouterOS's
dynamic PVID entries into saved configuration on every save. Analysis that
needs the effective view reads the device live. Extracted to pure functions
with tests, because that path was previously untestable inline.
Verified on four devices; 195 backend tests; all CI gates green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
d67698bac9 |
feat(safety): wire the lockout verdict into every guarded UI surface (v0.19.2-beta)
The verdict dialog only reached the VLANs tab, so a predicted lockout on the other guarded paths still surfaced as a generic "failed" message with the reasoning lost. Every surface that can push a guarded change now explains the refusal: - SwitchPortDiagram — port VLAN (PVID / tagged / untagged, the shape of the real incident), bond create and destroy, and the bridge vlan-filtering toggle. - RoutingTab — static route add and delete. - SystemConfigTab — IP address add and remove. - SecurityTab — /ip/service enable/disable. This route also has an older hard refusal that returns 409 with a reason but no verdict; that text is now shown instead of a generic error, where previously it was swallowed entirely. Supporting changes: confirm_lockout threaded through the affected API methods (DELETE routes send it as a request body), and deviceName threaded down to SwitchPortDiagram, RoutingTab and SecurityTab so the typed confirmation can ask for the actual device name. Verified the shipped bundle contains the dialog and the typed-confirm prompt. Full CI preflight green; 152 backend tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
118dd9ee8f |
feat(safety): explain predicted lockouts in the UI with typed confirmation (v0.19.1-beta)
The backend already refused a predicted lockout with 409 + a verdict, but the
frontend surfaced it as a generic "failed" message, so the reasoning never reached
the user.
- LockoutVerdictDialog renders the verdict properly: each violated invariant with
its plain-English consequence, and the resolved management path ("how the manager
reaches this device") so the user can verify the claim rather than take it on
trust. Critical verdicts require typing the device name to override; warnings
need only an acknowledgement, per the agreed friction levels.
- lockoutVerdictOf() extracts the 409 payload, mirroring the existing lockoutOf()
helper used by the firewall guard.
- VLAN add/update/delete now capture the verdict and re-send with confirm_lockout
when the user accepts, instead of showing an error banner.
Verified end to end on the production switch: changing the ingress port's PVID off
the management VLAN returns severity=critical with the ingress-port-pvid violation
and the hop chain (192.168.0.40/24 -> bridge -> sfp28-1), and the shipped bundle
contains the new dialog. Full CI preflight green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
518063e64f |
feat(safety): predict management lockouts before applying a change (v0.19.0-beta)
Phase 2a of the change-safety work. Change Guard could already rescue a device after a bad change; this predicts the break beforehand and refuses it. Rather than blocklisting "risky operations" — which is how deleting an *unused* bond and a routine VLAN edit both slipped through — the engine derives the invariants that must hold for the manager to keep reaching the device, simulates the planned change against LIVE device state, and reports any invariant that flips from satisfied to violated. Novel change shapes are covered for free, and each invariant explains itself, so the warning names the real mechanism. - pathModel.ts: captures a live snapshot over the API (no SSH) and resolves the management path — which address, interface, bridge, VLAN and physical ingress port the session depends on, each with the reason it is load-bearing. - invariants.ts: nine checks drawn from MikroTik's documented behaviour, notably that the bridge IS the CPU port and must be a member of the management VLAN. - analyzeChange.ts: simulates the change and diffs invariants, returning safe / warning / critical plus the path used to reach the verdict. - withGuardedChange now runs the analysis first and returns 409 with the verdict unless the caller passes confirm_lockout; analysis failures never block a change. - New GET /:id/management-path and POST /:id/preflight (analyse without applying). Three bugs were found by testing against the live switch rather than fixtures: - Invariants read vlan-filtering from the frozen path instead of the snapshot under evaluation, so "enable vlan-filtering" — the single most important case — was invisible. - VLAN membership is spread across BOTH a static row and a dynamic "added by pvid" row. Simulating only the first match hid the danger; membership now tracks static and effective separately, and a bridge left in the VLAN only by the dynamic row is reported as a warning rather than silently safe. - Requiring the ingress port to be UNTAGGED in the management VLAN flagged a healthy tagged trunk. Because a pre-existing violation is (correctly) not reported as new, that false positive silently suppressed real detections. Verdicts now also list pre-existing violations so this can't hide again. Verified on the production 100G switch: changing the ingress port's PVID off the management VLAN is refused with 409 and the switch is left untouched (pvid still 1); removing the bridge from VLAN 1 is correctly reported as a warning because a dynamic PVID row would still cover it; unrelated VLAN edits stay safe. 29 changeGuard tests, 152 backend tests, full CI preflight green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
78e1c114e5 |
feat(safety): extend Change Guard to VLAN, IP and route changes (v0.18.2-beta)
Change Guard shipped covering only 4 routes (service toggle, bond create/delete, bridge vlan-filtering). The most recent real incident — a VLAN change that removed a switch from management — went through PUT /:id/ports/:name/vlan, which was NOT guarded. Neither were the bridge-VLAN CRUD routes, IP address changes, or route changes, all of which can sever the manager's own path. Guarded routes go from 4 to 12, adding: - port.vlan PUT /:id/ports/:name/vlan (PVID + tagged/untagged membership) - vlan.add POST /:id/vlans - vlan.update PUT /:id/vlans/:vlanDbId - vlan.delete DELETE /:id/vlans/:vlanDbId - ip.add POST /:id/ip-addresses - ip.remove DELETE /:id/ip-addresses/:addrId - route.add POST /:id/routing - route.remove DELETE /:id/routing/:routeId Each now takes a device-side restore point and arms a self-restore before applying, disarming only once the device is proven still reachable. Two correctness details while converting: - vlan.delete removes the local DB row in afterConfirmed, so a change that auto-reverts no longer leaves the UI claiming a VLAN was deleted when the device restored it. - The VLAN CRUD routes previously returned a bare array, which would have been spread into the guard response object. They now return a message; the frontend ignores these bodies and re-queries, so no client change is needed. Verified live against the production 100G switch: a guarded VLAN add and delete both returned protected/confirmed, were recorded in device_change_guards, and the switch was left in its original state. 133 backend tests pass; full CI preflight green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d44a0ea8a7 |
fix(deps): patch socket.io-parser advisory; add local CI preflight gate
Security Audit failed on a newly-published advisory, GHSA-2m8v-j782-fhvr (socket.io-parser "Zero-attachment Memory Exhaustion", high). It affected both projects transitively via socket.io / socket.io-client. Patched to 4.2.7 in both lockfiles; no direct dependency versions changed. Also adds the gate that should have caught the previous two CI failures before they reached GitHub: - scripts/ci-preflight.sh runs every gate from .github/workflows/ci.yml (frontend and backend lint/type-check/build, plus both prod-dependency audits with the same audit-ci flags and allowlist) and additionally the backend Jest suite, which CI does not currently run. It runs all gates rather than failing fast, so one invocation surfaces every problem. ~35s. - scripts/claude-push-gate.sh wires that into a Claude Code PreToolUse hook so a `git push` is refused while any gate fails, with the failure detail returned inline. Non-push commands and --dry-run pass straight through. Verified: with the fix reverted, the gate correctly denied the push and reported "backend: lint FAIL"; with it restored, all 9 gates pass and both audit-ci invocations exit 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
23a45703a5 |
fix(ci): attach cause when rethrowing bridge-restore failure (v0.18.1-beta)
The backend Lint job failed on the new createBond error path: ESLint's
preserve-caught-error rule (in effect since the eslint 10.7 bump) requires a
rethrown error to carry the original. Pass `{ cause: err }`, which also makes the
underlying RouterOS failure visible in the error chain instead of only inside the
message string.
No behavior change beyond the richer error. Verified with the exact CI commands:
`npm run lint` and `npm run build` both exit 0, 133 backend tests pass, and the
redeployed build still guards correctly (guarded change commits, disabling the
management service is refused with 409).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
3a18cb1d91 |
feat(safety): Change Guard — device-side auto-revert for risky config changes (v0.18.0-beta)
A user change can sever the manager's own path to a device with no warning and no
way back. Most recently, deleting an *unused* bond on a switch took the whole
switch offline. This adds a safety net, and fixes the bug that caused that
specific incident.
Root cause of the bond incident (DeviceCollector.deleteBond):
- Slaves were re-added to the bridge ONLY when the bond itself was a bridge
member. For an unbridged bond the restore loop never ran, so the member ports
were released from the bond and left in no bridge at all — the ports stopped
forwarding and the switch dropped off the network.
- The restore in both deleteBond and createBond was wrapped in `.catch(() => {})`,
so a failed restore was swallowed and the route still returned 200 OK.
- Restores also dropped PVID, silently returning tagged ports to VLAN 1.
Now: port settings (PVID, frame-types, ingress-filtering, edge, horizon) are
captured and carried over, and restore failures are reported instead of hidden.
Change Guard (services/changeGuard/ChangeGuard.ts):
Before a risky change the device saves a restore point and arms a scheduler that
restores it after N seconds. The change is applied, then reachability is proven
on a NEW connection; success disarms the scheduler, failure leaves the device to
restore itself and come back. Because the restore point is created before the
scheduler, a binary restore rolls back to a config containing neither the
scheduler nor the change — self-cleaning, with no revert loop.
- Works entirely over the RouterOS API. This matters: every recovery artifact
today requires SSH, so devices without SSH credentials had no rollback target
at all (cf. #86).
- Modes: `binary` (exact restore incl. deleted interfaces, reboots) or `script`
(/import, no reboot but additive). Configurable via change_guard_* settings.
- Per-device Redis lock, `device_change_guards` audit table, and startup
reconciliation for guards left pending by a manager restart.
- Degrades gracefully: if protection can't be armed the change still runs and the
response says it was unprotected.
Wired into bond create/delete, bridge vlan-filtering, and /ip/service. Disabling
the service the manager is connected through is now refused outright (409) — the
mgmt-service signal already existed but was confined to the read-only posture
endpoint.
Also: replaces the bare confirm() on "Destroy Bond" with a ChangeGuardDialog that
explains the risk and the protection, and reports the outcome (including "the
device is restoring itself"). Fixes getAppSettings omitting
config_snapshot_enabled/interval, which made the v0.17.4 snapshot toggle a no-op.
Verified on real hardware (hAP-class switch): capability probe confirmed
/system/backup/save and scheduler add/remove work over the API; a guarded change
armed, applied, verified and committed leaving zero schedulers and zero files on
the device; disabling the api service was refused with 409. 133 backend tests
pass, including 7 guard state-machine tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
5895e905b7 |
fix(netflow): bound template cache, throttle UDP ingress, canonical IPv6 (v0.17.7-beta)
Follow-up to the decoder DoS fix (
|
||
|
|
7e11115a11 |
fix(netflow): reject zero-length template fields to stop decoder DoS (v0.17.6-beta)
A NetFlow v9/IPFIX template declaring a field of length 0 produced a template with minLength 0. parseDataSet's loop condition (off + minLength <= setEnd) then stayed true while the field pass advanced `off` by 0 — an infinite loop. Impact: the collector socket is UDP and unauthenticated, so any host able to reach the NetFlow port could hang the entire backend with a single 36-byte packet — Node is single-threaded, so the event loop never yields again and the API, poller, sockets and health checks all stop. Only reachable while NetFlow is enabled (off by default). Two layers of defense in decoder.ts: - parseTemplates rejects any field with length 0 (meaningless in v9/IPFIX) and refuses to cache a template whose computed minLength is 0. - parseDataSet asserts forward progress: if a full field pass consumes no bytes, break instead of spinning. Guards any future zero-width path. Adds 5 regression tests covering the hang case, a zero-length field among valid ones, a variable-length field whose length byte is 0, absurd/truncated field lengths, and that valid v9/IPFIX traffic still decodes afterwards. Verified: the exact packet that previously hung the process now returns immediately with no template cached; 20 attack packets fired at the live collector left the backend healthy and serving, and real exporter flows kept decoding (102 -> 152 flows across the test). 119 backend tests pass. Found via an adversarial review; the report identified the infinite loop but attributed it to the variable-length branch, which provably terminates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b6d161fc9d |
fix(wireless): prune interfaces deleted on the device; collect wireless on full sync (v0.17.5-beta)
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> |
||
|
|
4ef9fd0e37 |
chore(ci): fix Security Audit — audit prod deps via audit-ci, patch dev advisories
The Security Audit job failed because new advisories dropped against existing deps and `npm audit --audit-level=high` audits the whole tree (incl. dev/build tooling that isn't shipped). - Switch both audit steps to `audit-ci --high --skip-dev`, so the gate reflects what's actually deployed (backend ships --omit=dev; frontend ships a static build) while still failing on real high/critical production advisories. - Allowlist GHSA-qwww-vcr4-c8h2 (React Router "RSC Mode CSRF") for the frontend: this is a client-side SPA using BrowserRouter — RSC mode is not used — and there is no forward-compatible fix for react-router-dom 7.x (only a downgrade). - `npm audit fix` on both lockfiles to patch non-breaking transitive dev advisories (js-yaml, postcss, brace-expansion, minimatch). No direct/prod dependency versions changed; app runtime is unchanged. Verified: backend 114 tests pass, frontend builds, and audit-ci passes for both projects locally. |
||
|
|
f0176efd95 |
fix(config-history): only auto-snapshot devices with SSH configured; add settings UI (v0.17.4-beta)
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> |
||
|
|
28701cfc67 |
fix(security): harden 2FA setup, OIDC default role, and rate-limit fallback (v0.17.3-beta)
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> |
||
|
|
b3ba2b98dc |
chore(security): resolve CodeQL findings — unbiased voucher RNG, linear email check, rate-limit query filter (v0.17.2-beta)
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>
|
||
|
|
3fe8341b71 |
chore(deps): batch Dependabot bumps + ES2022 tsconfig (v0.17.1-beta)
Apply the open Dependabot updates as one validated batch (build + test + lint + local Docker smoke test all green) rather than 11 separate merges. Backend: - @types/node 20.19.43 -> 26.1.1 (aligns types with the Node 26 runtime) - typescript-eslint 8.58.2 -> 8.64.0 - eslint 10.5.0 -> 10.7.0 - eslint-plugin-security 4.0.0 -> 4.0.1 - nodemailer 9.0.1 -> 9.0.3 - tsconfig target/lib ES2020 -> ES2022 so Array.prototype.at() (devices.ts) type-checks under the new @types/node — this is what unblocked #72. Frontend: - axios 1.18.0 -> 1.18.1 - react-router-dom 7.18.0 -> 7.18.1 - lucide-react 1.21.0 -> 1.23.0 - @xyflow/react 12.10.2 -> 12.11.1 - @vitejs/plugin-react 6.0.2 -> 6.0.3 CI: - docker/setup-qemu-action v3 -> v4 Closes #58 Closes #60 Closes #62 Closes #65 Closes #66 Closes #67 Closes #68 Closes #69 Closes #70 Closes #71 Closes #72 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
c0f230bdf2 |
feat(auth): OIDC / SSO single sign-on (v0.17.0-beta)
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> |
||
|
|
ea1ad956ff |
fix(ui): escape apostrophe in Operations update notice to satisfy lint (v0.16.12-beta)
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>
|
||
|
|
8e564fc813 |
fix(system): report real version so update banner clears; move update notice to Operations (v0.16.11-beta)
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> |
||
|
|
9e962939ea |
fix(security): self-healing secrets + auth/SSRF/header hardening (v0.16.10-beta)
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> |
||
|
|
27560fa1d6 |
fix(security): enforce role auth on /terminal SSH namespace (v0.16.8-beta)
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> |
||
|
|
79e97e14cc |
fix(ui): firmware state syncs across screens immediately (v0.16.7-beta)
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. |
||
|
|
27f155f778 |
feat(firmware): in-app MikroTik release notes + posture findings for pending updates (v0.16.6-beta)
- 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> |
||
|
|
8307bcd291 | Updating images to match current GUI. | ||
|
|
6f0fbdb74f |
chore(deps): dependency upgrades — react-router 7, eslint 10, TS 6, plugin-react 6; drop unused zod (v0.16.5-beta)
Applies the open dependabot PRs in one validated pass: - react-router-dom 6.30 -> 7.18 (drop-in for our API surface; verified in-browser: login, sidebar routing, ?type= deep-links, legacy redirects) - react + react-dom -> 19.2.7, axios -> 1.18.0, lucide-react -> 1.21.0 - actions/checkout v6 -> v7 across all workflows - backend: eslint 10 + @eslint/js 10 (fix 5 new no-useless-assignment findings), typescript 6.0.3 (explicit types: ["node","jest"] in tsconfig — TS 6 no longer auto-includes @types), remove zod (unused) - frontend: @vitejs/plugin-react 6 Not taken: ioredis 5.11.1 (#55) — bullmq pins ioredis to exactly 5.10.1 (including latest bullmq 5.79), so the bump nests a second ioredis and breaks BullMQ connection types. Validation: backend tsc/lint/build + 92 tests green; frontend tsc/lint/ build + 11 tests green; docker stack rebuilt and browser-verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dd1b6b7d13 |
feat(ui): navigation reorganization — consolidate duplicate pages, grouped sidebar (v0.16.4-beta)
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> |
||
|
|
662d7852fa |
ci(publish): build & push multi-arch images (amd64 + arm64) to GHCR
Add docker/setup-qemu-action and platforms: linux/amd64,linux/arm64 to both the backend and nginx build-push steps so GHCR images run natively on arm64 (ARMv8) hosts instead of amd64-only. Refs #56. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9dfb8a5333 |
feat: Guest WiFi, network intelligence, firmware orchestration, platform automation, api-ssl (v0.16.3-beta)
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> |
||
|
|
efb430d934 |
docs(security): update supported versions to 0.16.x in security policy
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6cfca387d8 |
feat(ops): operations insights engine + working quick actions; fix global search (v0.16.2-beta)
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> |
||
|
|
4e6c9f481e |
feat(wireless+ports): RF Health panels + physical per-port clients (v0.16.1-beta)
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.
|
||
|
|
1be161e67a |
feat(ui): modern clients pagination + unified syslog page (v0.16.0-beta)
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> |
||
|
|
6e56048f2f |
chore(deps): bump axios, react-query, lucide-react, pg (v0.15.1-beta)
Validated dependency updates (Dependabot #41, #42, #43, #48): - axios 1.16.0 → 1.16.1 - @tanstack/react-query 5.95.2 → 5.101.0 - lucide-react 1.16.0 → 1.17.0 - pg 8.20.0 → 8.21.0 Holding ioredis at 5.10.1: 5.11.1 (#45) breaks the backend build via a type mismatch with BullMQ's bundled ioredis copy; leave until BullMQ bumps its pinned ioredis. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b1dd62bd1f |
feat(traffic): protocol breakdown layout + inbound packets chart + flow source health (v0.15.0-beta)
- 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> |
||
|
|
d79eb13119 |
feat(ui): theme-aware logo, dashboard bar graph, server-side client sorting (v0.14.1-beta)
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> |
||
|
|
ee9c9ff9f7 |
feat(security): Security Center — firewall, NAT, bandwidth, posture, connections (v0.14.0-beta)
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>
|
||
|
|
623e54120d |
chore(deps): clear high-severity npm audit findings (v0.13.3-beta)
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> |
||
|
|
e777276e41 |
fix(clients): unify client traffic chart timeframes to 2h/24h/7d and stop App Traffic card vanishing (v0.13.2-beta)
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> |
||
|
|
368b4e83b6 |
feat(dashboard): unify Top Talkers with NetFlow traffic data (v0.13.1-beta)
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>
|
||
|
|
b841eb6e7b |
feat: NetFlow/IPFIX traffic analytics — per-client usage, app breakdown, built-in collector (v0.13.0-beta)
Adds Meraki-style traffic analytics powered by MikroTik Traffic Flow: Backend - Built-in NetFlow v9 + IPFIX collector (UDP 2055) with hand-rolled decoder, template caching, enterprise-field and IPv6 support - Flow attribution: client IP → MAC via the clients table, upload/download direction, NAT-duplicate records dropped - App classification by protocol/port (~25 categories: HTTPS, QUIC, DNS, SSH, Email, WireGuard, ...) - Deduplication: per-client best-exporter-per-window so flows crossing two managed routers are never double-counted - NAT-tolerant ingest: flows from sources matching no managed device (NAT / Tailscale subnet routers in path) are accepted as per-source unidentified exporters instead of silently dropped (netflow_accept_unknown setting) - Storage: InfluxDB client_traffic time-series (bounded cardinality via top-N fold) + Postgres client_traffic_daily rollups; daily retention purge - One-toggle device config push via RouterOS API (/ip traffic-flow + target) - New /api/traffic/* analytics endpoints + /api/network-services/netflow - docker-compose: NETFLOW_PORT (default 2055/udp) published on the backend Frontend - New Traffic page: fleet up/down chart, top talkers with client click-through, app-category donut, 1h–30d ranges - New NetFlow config page: collector settings, live ingest stats, per-device export toggles with health badges, NAT-source banner - Client detail: App Traffic card (auto-hides without data) - Clients page: sortable "Data (today)" column - Settings: NetFlow retention controls Includes 17 unit tests (decoder fixtures, dedup, attribution, classifier); verified end-to-end against live RouterOS devices. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |