Commit Graph
6 Commits
Author SHA1 Message Date
2GT-RichandClaude Opus 5 2625fea8de feat(lte): cellular telemetry, band details and roaming history (v0.23.12-beta)
Adds LTE support, requested in discussion #85 by a contributor running a
MikroTik ATL 18 on a fixed antenna. Cellular was previously a blank page: the
only occurrence of "lte" in the tree excluded LTE interfaces from DHCP binding.

Built against two real field captures rather than documentation, which
disagreed with the hardware in several places — the modem reports `data-class`
not `access-technology`, `iccid` not `uicc`, `status: running` not `connected`,
embeds units inside values (`-97dBm`, `17dB`), and omits `registration-status`,
`pin-status` and `lac`/`tac` entirely. What an LTE interface reports depends on
the modem chipset, not on RouterOS, so every field is optional by construction.

Fixes a latent bug in the API client while it was in reach. A sentence carrying
the same attribute twice kept only the last value, and a Cat-18 modem reports
one `ca-band` per aggregated carrier. On the contributor's 4-carrier capture
that yielded B3 + B20 — 30 MHz — on a device actually running 70 MHz, because
the narrowest carrier happens to arrive last. Repeats are now preserved
alongside the flat value, so existing callers are byte-for-byte unaffected.

Signal quality is graded rather than merely displayed. RSRP alone misleads: the
capture shows −97 dBm, nominally "fair", on a link running 256QAM at CQI 15 with
two spatial streams. SINR leads the grade and RSRP moderates it.

Surfaces the uplink anchor, which no MikroTik UI shows. Left on automatic a
modem anchors to whichever band it hears loudest, often a narrow one, and
aggregates wider bands for downlink only — but uplink rides the primary carrier
alone, so upload stays capped. This is the contributor's actual reason for
locking bands, and it reframes band selection as anchor control rather than
coverage.

Band changes, tower handovers and re-registrations are reconstructed by
comparing consecutive polls, since scanning interrupts service and finds nothing
at a site served by a single base station. Observed bands accumulate as the
evidence a future band lock will be checked against.

Band selection itself is deliberately absent: it needs Change Guard plus a
free-space check, as the target hardware has 1.1 MB free of 16 MB flash.

Also fixes an unrelated display bug in the RF Health channel map. The 2.4 GHz
axis is labelled by channel, but ticks at 2400 and 2480 fall outside the channel
plan and fell back to printing raw frequencies — and the 2400 label sat at
exactly 0% with a centring transform, hanging half its width outside the card.
2.4 GHz is the only band with a tick at an edge. Unlabelled ticks are now
dropped and edge labels anchor inward.

  - backend/src/utils/lte.ts — parsing, quality, change detection (59 tests)
  - RouterOSClient — repeated attributes preserved (6 tests)
  - lte_interfaces, lte_cell_history, lte_observed_bands
  - GET /devices/:id/lte, /lte/history, /lte/metrics
  - LteTab, gated on has_lte from the interface list
  - scripts/seed-lte-fixture.sql — a cellular device for local development

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:06:28 -07:00
2GT-RichandClaude Opus 5 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>
2026-08-20 21:24:18 -07:00
2GT-RichandClaude Opus 5 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>
2026-08-20 16:48:29 -07:00
2GT-RichandClaude Opus 5 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>
2026-08-20 11:11:43 -07:00
2GT-RichandClaude Opus 5 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>
2026-08-20 08:25:35 -07:00
2GT-Rich 5d96503832 Add Jest unit test infrastructure and initial test suite
Sets up Jest + ts-jest for the backend and adds 47 tests across 5
suites covering crypto utilities, OUI vendor lookup, auth middleware,
AlertService dispatch/cooldown logic, and DeviceCollector uptime parsing.
2026-04-19 10:21:14 -07:00