719563dd2262c24112ff61fde21f2bddf239fcb2
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
273a0da737 |
fix(deploy): restore NetFlow to the published compose file, and generate it (v0.23.34-beta)
Closes #121. The Quick Deploy file was missing NETFLOW_BIND_PORT and the 2055/udp mapping, so the collector bound inside the container and nothing reached it. NetFlow did nothing, silently, with an empty traffic page and no error — for everyone following the path the README tells new users to take. The report was accurate and understated. docker-compose.ghcr.yml had drifted for seven weeks, and three closed issues had never reached it: * NETFLOW_BIND_PORT and the UDP port — as reported * all five POLLER_* tunables — #114, whose fix was "pass the tunables through" * the Redis memory ceiling and eviction policy — #116 * TZ on the postgres service, which nobody had noticed at all The middle one is the uncomfortable part. Four days ago #114 was closed by forwarding poller settings that were documented but inert, with a comment saying setting them "did precisely nothing". On Quick Deploy that remained true, because one compose file was fixed and the other was not — including the advice given to that reporter about which knob to turn. So patching the four gaps would have been treating the symptom. The two files describe the same system and differ only in where images come from; maintaining both by hand meant remembering every change twice, and the one forgotten was always the file no maintainer runs. docker-compose.ghcr.yml is now generated from docker-compose.yml by scripts/generate-ghcr-compose.mjs, which performs the two build-to-image swaps and nothing else. Substitution is textual rather than parse-and-reserialise so comments survive — the reasoning about Redis eviction is worth more than the two lines it explains. The file stays committed and browsable, because the README tells people to curl it directly. A --check mode runs as the first preflight gate and as its own CI job, both of which fail when the committed file does not match what the generator produces. Verified by deleting the NetFlow line and watching the guard reject it, with the reason and the fix in the failure message. Reported by @mamb1329, on their first issue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
5071e1f512 |
chore(dev): seedable CAPsMAN fixture so managed-wireless paths can be tested (v0.23.8-beta)
Two bugs shipped in the last three releases because CAPsMAN code was written and
"verified" against a fleet that contains no CAPsMAN. Neither was reachable by unit
tests, because both lived in SQL: a join that could never match, and a WHERE clause
that filtered out every managed radio before the analysis ran. Both were found by
the reporter, in production, after release.
The fix is not another test of the pure functions — those were already right. It is
to stop the local environment from being unrepresentative.
scripts/seed-capsman-fixture.sql builds a deployment shaped like a real one:
- the controller is a ROUTER with no radios of its own, so anything keyed on
device_type = 'wireless_ap' misses it
- CAP interfaces carry NULL frequency, SSID and band, because the controller owns
the configuration — the exact condition that removed them from Channel Usage
- the controller mirrors every CAP interface locally, so a CAP's radio MAC
legitimately appears on two devices — the condition behind the misattribution
- channels exist only as "<freq>/<phy>/<control-positions>", never a plain number
- the radios are arranged to produce one partial overlap and one co-channel pair,
so interference classification has something real to get wrong
Running it against v0.23.7 confirms the fix shipped blind two commits ago actually
works: six managed radios now group under the controller, the controller appears in
the location selector, the standalone group holds only the genuinely standalone AP,
5680/ax/eCee/D resolves to 80 MHz, client counts come from the controller where the
CAP rows read zero, and the controller's mirror interfaces do not double-count.
Documented in docs/architecture.md, including the point that when a report arrives
with device output attached, that output is the most valuable thing in it and
belongs in the fixture.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
d44a0ea8a7 |
fix(deps): patch socket.io-parser advisory; add local CI preflight gate
Security Audit failed on a newly-published advisory, GHSA-2m8v-j782-fhvr (socket.io-parser "Zero-attachment Memory Exhaustion", high). It affected both projects transitively via socket.io / socket.io-client. Patched to 4.2.7 in both lockfiles; no direct dependency versions changed. Also adds the gate that should have caught the previous two CI failures before they reached GitHub: - scripts/ci-preflight.sh runs every gate from .github/workflows/ci.yml (frontend and backend lint/type-check/build, plus both prod-dependency audits with the same audit-ci flags and allowlist) and additionally the backend Jest suite, which CI does not currently run. It runs all gates rather than failing fast, so one invocation surfaces every problem. ~35s. - scripts/claude-push-gate.sh wires that into a Claude Code PreToolUse hook so a `git push` is refused while any gate fails, with the failure detail returned inline. Non-push commands and --dry-run pass straight through. Verified: with the fix reverted, the gate correctly denied the push and reported "backend: lint FAIL"; with it restored, all 9 gates pass and both audit-ci invocations exit 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |