Commit Graph
10 Commits
Author SHA1 Message Date
2GT-RichandClaude Opus 5 719563dd22 feat(backups): filter, preview, compare and bulk delete (v0.24.2-beta)
Closes #134.

Backups are `/export compact` output — plain RouterOS script, not the binary
`.backup` blob — which is what makes the reporter's "it's just a text, after
all" correct and most of this cheap.

  - Filters: device, type, date range and free text over device name,
    filename and notes. Dates are inclusive at both ends, because a picker
    showing "to: 3 Sep" that stops at that day's first instant is a trap.
  - Preview in place, on row click. No round trip through the downloads
    folder to see what a backup contains.
  - Compare any two, reusing the lineDiff that Config History already uses
    rather than adding a second diff implementation.
  - Bulk delete, with selection and a confirmation that names what goes.

Two deliberate choices.

Bulk delete had to respect an existing invariant: a backup and its config
snapshot are one artifact, joined by ON DELETE CASCADE so they cannot drift
apart. Removing fifty in one click can therefore destroy restore points
nobody looked at. The confirmation is built from a server-side preview that
reports the backups per device *and* how many config snapshots will go with
them, and it is capped at 200 per call. The action is also site-scoped, so it
cannot reach into another site's history.

The Filename column is gone from the table but the filename is not gone: it
is the natural handle for a file — device and timestamp, the thing you would
quote to a colleague — so it moved to the preview and diff headers where you
are actually looking at that file, rather than being dropped.

Diff direction is extracted and tested. Selection order in a table is
whatever the user happened to click and carries no meaning, so the pair is
ordered oldest-first; rendered the other way round every addition reads as a
deletion, and nothing about the output looks wrong while it does.

Incidentally fixed: 'pre-upgrade' backups were labelled "Manual" in the table
because the badge only knew three types. Type wording now has one home.

The list query's placeholder counter is replaced by a bind() helper deriving
from params.length. The counter's final increment was dead, and whoever added
the next filter would have inherited a stale index.

Verified against 110 real backups: every filter and combination returns the
expected counts and partitions cleanly, preview and diff read real .rsc with
file_path withheld, and bulk delete removed three throwaway backups plus
their files and left the other 110 untouched. Guards checked: empty, missing,
non-numeric and injected ids, unknown ids, and the 200 cap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 21:05:55 -07:00
2GT-RichandClaude Opus 5 7b994ad935 feat(sites): one install, many networks (v0.24.0-beta)
Closes #130. Multi-site, not multi-tenancy: a site is a named collection of
devices whose clients, events, topology and dashboards are viewed separately.
Sites separate what you see, not who you are — the admin/manage/read-only
roles are unchanged and every user sees every site. Per-site permissions
would have multiplied the work for a need nobody stated.

Two measurements decided the design.

Every device-derived table hangs off devices(id) — 25 of 50 tables, 35
foreign keys — so a single site_id column on devices scopes clients, events,
topology, backups and the rest transitively. No other table needed one.

The frontend has 162 useQuery call sites across 57 files. Threading a site
through each would be a large change that leaves a permanent trap: every
future query another chance to forget. The site is injected in the existing
axios interceptor instead, beside the auth token, so a new query is scoped by
default. Only 23 collection routes needed filtering; the 131 per-device
routes are already scoped by their device.

Existing installs are untouched. Migration creates one "Default Site", adopts
every device into it, and a single-site install never meets the concept. An
absent X-Site-Id header means unscoped, so existing API tokens and scripts
behave exactly as before.

Polling stays fleet-wide. Sites are a view over data already gathered, not a
change to how it is gathered — a device does not stop being monitored because
you are looking elsewhere. Rogue-AP detection scopes the scans but keeps the
"these are ours" BSSID set fleet-wide, or your own AP in another site reads
as an evil twin. Bulk actions do obey the selector: an operation that
silently spanned two customers is the blast radius the wave guards exist to
contain.

Four rounds of defects came out of testing the UI rather than the API, and
the first is the one worth recording. Switching sites called
queryClient.clear(), which empties the cache and reviews as correct. It
isn't: clear() destroys each query but a *mounted* observer keeps its own
reference and last result, so every rendered page went on showing the
previous site's data. The QueryClient is now keyed on the site, which
remounts the tree against an empty cache. Three tests pin the old behaviour
down so the shortcut cannot return.

The others: an unscoped fallback path behind the Connected Clients chart;
Config Health, TX retries and the activity feed reading fleet-wide; and
site_id added to the device list query but none of the other four that build
a device payload, so the Site field could not read its own value and appeared
to reject every change while the writes were landing. That last one is now
structural — DEVICE_BASE_COLUMNS is shared by all five, with a test that
fails when a device payload is hand-rolled, and which found two further
instances while being written.

Frontend tests were never run by CI despite vitest being configured and six
suites existing; they now run in both the workflow and ci-preflight.

Verified against a live four-device fleet: the migration left every figure
identical, an empty site returns zero from every read endpoint, moving a
device carries its history with it, and deleting a site with devices is
refused.

Minor bump rather than patch: sites change the shape of the product rather
than adding to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 12:02:00 -07:00
2GT-RichandClaude Opus 5 e47819e232 fix(topology): read the spanning-tree root instead of guessing it (v0.23.35-beta)
Closes #131.

Reported as the wrong device being marked root: an RB4011 at priority 4096 says
"root bridge: yes", two APs at 8192 say they are not and name the RB4011's MAC,
and the map crowned an AP anyway — consistently, always the same one.

The detection had never worked at all. It looked for a device with no port whose
STP role was 'root', but RouterOS reports the role as 'root-port'. Nothing ever
matched, so the set of devices-with-a-root-port was permanently empty and the
lookup returned the first device in the list. It was not choosing badly; it was
not choosing.

The same mismatch broke link state. Blocking was derived from a role of
'alternate' or 'backup' against values that arrive as 'alternate-port' and
'backup-port', so blocked links were drawn as forwarding — which is the "traffic
connection path" half of the report, since a blocked link shown as live yields a
path that does not exist.

Roles are now normalised on write so both ends of the system share one
vocabulary, and the root is no longer inferred. RouterOS answers it directly:
/interface/bridge/monitor reports whether a bridge is the root and names the root
it sees, which is exactly what the reporter's own screenshots show the device
knowing. That is collected into device_bridges and resolved in
utils/stpRoot.ts with 13 tests.

Two properties of real networks shaped the resolution, both observed on the
development fleet rather than reasoned about:

  * **A root belongs to a spanning tree, not to a fleet.** Two of four devices
    here legitimately report root at once, being separate bridge domains, so
    roots are resolved per connected component.
  * **The root may not be ours.** One switch points at priority 8192 on a MAC
    belonging to no device in the inventory. Naming a local device in that case
    is precisely the original bug, so the crown is withheld and the foreign
    bridge id reported instead.

Verified against that fleet: the managed root resolves to the device that claims
it, a self-rooted bridge keeps its own crown, and the switch with an external
root now reports "not ours" where the old logic confidently crowned it — the
reported symptom, reproduced and fixed.

Also adds collectStp to the full sync. It previously ran only on the five-minute
slow poll, so pressing Sync after rewiring did not refresh the map.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 08:58:29 -07:00
2GT-RichandClaude Opus 5 4ff8eec200 fix(topology): stop breadth translating into width (v0.23.22-beta)
Closes #115.

The map gave every leaf its own column, so a subtree's width was simply its leaf
count. That is reasonable for a deep branching network and useless for a real
one: a core switch with fifty-eight access switches under it produced a single
row fifty-eight slots wide — over ten thousand pixels, which is exactly how it
was reported.

Real networks are shallow and wide, so breadth had to stop translating directly
into horizontal distance. Large groups of sibling leaves are now laid out as a
block rather than a row, trading width for height and turning linear growth into
roughly square-root. The reported shape goes from 58 slots to 10, six rows deep.

Children that have subtrees of their own keep the classic tidy-tree treatment,
because there the hierarchy is the information and flattening it would destroy
meaning. Only leaf groups — where the sibling order carries nothing — are packed.

Block proportions are deliberately wider than tall: screens are landscape, and a
column of forty reads worse than a band four rows deep.

The layout moved to utils/topologyLayout.ts in slot and row units, with pixels
left to the renderer, and 12 tests. That split is what makes the fix checkable at
all — the old arithmetic was only observable by looking at a rendered graph of a
network none of us has. The tests pin the reported case and, as importantly, the
shapes that must not regress: a three-node row stays a row, a branching tree
keeps its levels, a deep chain stays one slot wide, and no two nodes land on the
same coordinate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 10:45:18 -07:00
2GT-RichandClaude Opus 5 ec873ab8bd fix(wireless): keep the 2.4 GHz channel map inside its card (v0.23.13-beta)
The 2.4 GHz row of the Channel Usage panel painted over the edge of the card.

The cause is the band table, not the layout. A 40 MHz channel 1 occupies
2392-2432 MHz, but the drawn 2.4 GHz band started at 2400, so that carrier
resolved to `left: -8%` and an absolutely positioned bar at a negative offset
renders outside its container. 2.4 GHz is the only band where this can happen:
channel 1 is the lowest channel in the plan and the only one whose 40 MHz form
reaches below the round number the axis started on.

The band now starts at 2390, which is also more honest — the radio really does
reach below 2400, and clamping alone would have drawn it as though it did not.

Geometry moves to utils/spectrumGeometry.ts with tests, and keeps a clamp even
though the range now covers channel 1: band ranges are static assumptions, and
region-specific channels and widths they do not anticipate will keep appearing.
No bar can leave the card again regardless of what the backend reports.

An earlier attempt at this fixed a real but different bug in the same row: ticks
at 2400 and 2480 fall outside the channel plan, so an axis labelled by channel
fell back to printing raw frequencies, and the 2400 label sat at exactly 0% with
a centring transform and hung half its width outside the card. That fix stands;
it simply was not the whole of what the user was seeing. The verification script
used at the time filtered radios to those starting inside their band, which
silently discarded the very row that was overflowing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:35:22 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-07-02 21:58:02 -07:00
2GT-Rich 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.
2026-06-25 15:37:08 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-06-18 09:24:22 -07:00
2GT-RichandClaude Opus 4.8 367034c2d6 feat: config history (diff/rollback) + friendly backup scheduling (v0.12.3-beta)
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>
2026-06-06 16:01:58 -07:00
DenisandCursor Agent a12f52ebc7 feat: non-blocking bulk-add, CORS hardening, credential preset access control (#31)
* 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>
2026-04-21 19:54:35 -07:00