Commit Graph
130 Commits
Author SHA1 Message Date
2GT-RichandClaude Opus 5 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>
2026-08-17 11:47:14 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-08-16 21:30:17 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-08-16 21:04:29 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-08-16 20:45:30 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-08-15 23:27:03 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-08-10 09:27:40 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-08-10 08:34:58 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-08-03 11:52:07 -07:00
2GT-RichandClaude Opus 4.8 5895e905b7 fix(netflow): bound template cache, throttle UDP ingress, canonical IPv6 (v0.17.7-beta)
Follow-up to the decoder DoS fix (7e11115), closing the remaining hardening
items from the same adversarial-review triage.

Bounded template cache (was unbounded):
- Templates now carry a lastUsed timestamp, refreshed whenever one decodes data.
- New exported pruneTemplateCache() evicts templates idle past a TTL then
  enforces a hard cap, least-recently-used first. The collector runs it on its
  existing 60s map-refresh timer (30 min TTL, 10k entries) — no new timer.

Bounded per-source bookkeeping:
- unknownExporterIds is capped; sources beyond the cap share one overflow
  pseudo-exporter instead of allocating a new id each (client attribution
  doesn't depend on exporter identity, so only per-exporter stats coarsen).
- Pseudo ids now come from a monotonic counter rather than map size, so they
  can't collide after eviction.

UDP ingress guards (the socket is unauthenticated by protocol design):
- Drop datagrams too small to carry a header before doing any parsing work.
- New PacketRateLimiter: fixed-window per-source cap (2000 pkt/s), with the
  counter map cleared each window and a cap on distinct sources per window so
  spoofed source IPs can't grow it.
- getStats() now reports packetsDropped and templatesCached.

Canonical IPv6 rendering:
- formatIPv6 applies RFC 5952 zero-compression (2001:db8::1, not the fully
  expanded form) and unwraps IPv4-mapped addresses (::ffff:192.0.2.1 ->
  192.0.2.1) so those flows match client/exporter IPs instead of appearing as
  separate IPv6 endpoints.

Adds 7 tests (rate limiter windows//caps/isolation, cache TTL + LRU eviction,
lastUsed refresh, IPv6 canonical + mapped + no-run forms); 126 backend tests pass.

Verified live: flooding the collector with 3000 packets from one source dropped
614 once over the cap while the backend stayed healthy and served the API,
malicious templates were still refused (cache held only the 2 real ones), and
real exporter flows kept decoding right through it (260 -> 336).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 21:48:50 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-07-31 21:31:25 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-07-30 19:36:35 -07:00
2GT-Rich 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.
2026-07-29 15:47:52 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-07-29 15:38:08 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-07-29 15:21:04 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-07-18 09:56:18 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-07-18 09:33:26 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-07-16 18:54:36 -07:00
2GT-RichandClaude Opus 4.8 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 &apos;. No visual change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 08:09:05 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-07-14 17:18:25 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-07-14 12:42:46 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-07-14 08:34:09 -07:00
2GT-Rich 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.
2026-07-11 22:05:12 -07:00
2GT-RichandClaude Fable 5 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>
2026-07-11 15:45:53 -07:00
2GT-Rich 8307bcd291 Updating images to match current GUI. 2026-07-06 08:24:40 -07:00
2GT-RichandClaude Fable 5 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>
2026-07-03 20:35:21 -07:00
2GT-RichandClaude Fable 5 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>
2026-07-03 08:07:08 -07:00
2GT-RichandClaude Fable 5 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>
2026-07-02 22:39:27 -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-RichandClaude Opus 4.8 efb430d934 docs(security): update supported versions to 0.16.x in security policy
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:52:48 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-06-25 23:12:16 -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 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>
2026-06-19 08:53:19 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-06-18 16:44:30 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-06-18 16:32:17 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-06-18 13:37:11 -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 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>
2026-06-16 19:51:45 -07:00
2GT-RichandClaude Opus 4.8 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>
2026-06-16 19:38:16 -07:00
2GT-RichandClaude Fable 5 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>
2026-06-10 17:31:59 -07:00
2GT-RichandClaude Fable 5 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>
2026-06-10 10:58:41 -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
2GT-RichandClaude Opus 4.8 4ff5ed805c chore(deps): clear frontend security audit (vite 8, vitest 4, plugin-react 5) (#40)
The CI Security Audit (npm audit --audit-level=high) started failing on all
new PRs after a critical advisory was published against vitest <4.1.0
(GHSA-5xrq-8626-4rwp — Vitest UI server arbitrary file read/exec). The
remediation chain also requires vite to move off the vulnerable esbuild range.

These are devDependencies (test/build tooling) and do not ship in the
production image, so there was no runtime exposure to deployed users — but the
audit gate blocks the pipeline, so bump them:

- vite        ^5.0.11 -> ^8.0.16
- vitest      ^3.0.5  -> ^4.1.8
- @vitejs/plugin-react ^4.2.1 -> ^5.2.0 (vite 8 peer compatibility)

Vite 8 ships the rolldown bundler, which requires rollupOptions.output
.manualChunks to be a function rather than an object — converted accordingly,
with explicit precedence so @xyflow/react (whose name contains the substring
"react") lands in the `flow` chunk instead of being pulled into `vendor`.

Verified locally: npm ci, npm run lint, tsc --noEmit, npm run build, and
npm test (vitest 4.1.8, 3/3 passing) all pass; npm audit reports 0
vulnerabilities.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:18:56 -07:00
2GT-RichandClaude Opus 4.8 6dd855476e fix(topology): stop infinite render loop crashing the Topology page (#37) (#39)
The Topology page crashed on load with React error #185 ("Maximum update
depth exceeded"), thrown from inside React Flow's setNodes.

Root cause: handleDeleteManual depended on the whole TanStack Query
useMutation result object, which is recreated on every render. That made
handleDeleteManual unstable, which made the `graph` useMemo recompute every
render, which made the node/edge sync effect fire every render — calling
setNodes/setEdges in a loop until React aborted.

Fix: depend on the referentially-stable `.mutate` function instead of the
whole mutation object, so handleDeleteManual stays stable and `graph` only
rebuilds when the underlying data or connectMode actually change.

Verified: tsc --noEmit clean, eslint clean, vite build succeeds, and the
Topology page renders without #185 in a local Docker build.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:08:57 -07:00
2GT-RichandClaude Sonnet 4.6 914ded24b0 chore(deps): upgrade to React 19, react-dom 19, lucide-react v1, helmet v8 (v0.11.11 Beta)
- React + @types/react bumped to v19 (merged Dependabot PRs #22, #23, #25)
- react-dom + @types/react-dom aligned to v19 (PR #26 had conflicts; applied manually)
- lucide-react upgraded from 0.309 → 1.16 to satisfy React 19 peer deps (no API changes for our icon usage pattern)
- helmet upgraded 7 → 8; contentSecurityPolicy: false option unchanged
- jest + @types/jest bumped (dev-only, no runtime impact)
- TypeScript clean, Vite build passes, zero new errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 16:17:42 -07:00
2GT-Rich bc555fee2f Merge pull request #25 from 2GT-Media-Group-LLC/dependabot/npm_and_yarn/backend/multi-d4eba38f0e
chore(deps-dev): bump jest and @types/jest in /backend
2026-05-25 16:05:21 -07:00
2GT-Rich 2cc14bdcf3 Merge pull request #23 from 2GT-Media-Group-LLC/dependabot/npm_and_yarn/frontend/multi-0193e73c84
chore(deps): bump react and @types/react in /frontend
2026-05-25 16:05:18 -07:00
2GT-Rich 5d1d730e0f Merge pull request #22 from 2GT-Media-Group-LLC/dependabot/npm_and_yarn/backend/helmet-8.1.0
Bump helmet from 7.2.0 to 8.1.0 in /backend
2026-05-25 16:05:15 -07:00
2GT-Rich 457ad0c948 Merge pull request #35 from 2GT-Media-Group-LLC/dependabot/docker/backend/node-26-alpine
chore(deps): bump node from 20-alpine to 26-alpine in /backend
2026-05-25 16:03:27 -07:00
2GT-Rich d62dabb157 Merge pull request #34 from 2GT-Media-Group-LLC/dependabot/docker/nginx/node-26-alpine
chore(deps): bump node from 20-alpine to 26-alpine in /nginx
2026-05-25 16:03:24 -07:00
2GT-Rich 73087a6e40 Merge pull request #21 from 2GT-Media-Group-LLC/dependabot/github_actions/docker/build-push-action-7
Bump docker/build-push-action from 6 to 7
2026-05-25 16:03:21 -07:00