c4db0dc1e2bc6d17d1e501bccd1b54e767632006
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c4db0dc1e2 | fix: harden auth site access and SSH validation | ||
|
|
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> |
||
|
|
324ceba676 |
fix: place log events in real time, reset the data cap on send, export runs (v0.23.29-beta)
Three reports from this morning. **Event timestamps were not the times the events happened.** RouterOS writes log times in the device's own timezone and never says so, and it uses more than one format. The parser handled `sep/03 08:37:38` and `08:37:38` but labelled both UTC, shifting every event by the device's offset — reported as an event log seven hours out from the device page. It did not handle `2026-09-03 08:37:38` at all, which is what current RouterOS actually returns over the API, so those fell through to the collection time instead: plausible on a live poll, badly wrong for a backlog, where a thousand historical lines all arrive stamped "now". Devices now report their clock, and timestamps are converted from device-local to UTC through it, with the numeric gmt-offset as a fallback and daylight saving handled by the runtime rather than a fixed number. Where the zone is unknown the parser returns null and the caller falls back to collection time, deliberately — a confidently wrong timestamp is worse than an openly approximate one. Verified on hardware: 18 of 18 events in a steady-state window now store exactly seven hours ahead of what the device printed, which is the offset the device itself reports. Existing rows are left alone; they were written by three different code paths and cannot be corrected uniformly. **The data-cap counter kept climbing after the SMS was sent.** Reported from the field, and correct: the message is what zeroes the carrier's allowance — it is the alternative to waiting for midnight — so ours has to follow. Left as it was, two things went wrong: the bar counted against an allowance that no longer applied, and usage stayed above the threshold, so the rule would re-fire on every cooldown for the rest of the day. Both send paths now restart the count. **Command runs export to CSV**, asked for on #118 for archiving and audit. Output is quoted so multi-line device responses survive, and timestamps are ISO rather than the driver's default string form, which no spreadsheet parses. The download goes through the authenticated client rather than a plain link, because a link cannot carry a Bearer header and would simply 401. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0e364b1558 |
feat(commands): run one command across a fleet, in waves (v0.23.27-beta)
Closes #118. Unblocked by #110, which is what makes SSH on sixty devices practical. Arbitrary RouterOS console commands over SSH, because `:put`, `:foreach` and `/interface print` are console syntax that the binary API cannot express, and because a curated command library cannot cover "add a channel to LACP" or "rewrite a WireGuard endpoint" — the things people actually asked for. **Waves are the feature, not the safety rail around it.** A shell loop already runs a command on many hosts, costs nothing, and prints errors as they happen. What it cannot do is stop. Devices run together within a wave, waves run strictly in sequence, and halt-on-failure ends the run before a mistake becomes sixty mistakes. Without that, this would be a worse bash. Proved on hardware rather than argued: a bad command on two devices, one per wave, failed on the first and left the second `skipped`. RouterOS reports most errors in output text rather than an exit code, so output is read for failure markers — otherwise a rejected command and a successful one are indistinguishable and the run would sail on. Conservatively: only unambiguous markers count, since misreading ordinary output would halt a run that was working. Change Guard wraps each device by default, so one that stops answering after the command restores itself. It is one click to turn off, because a sharp tool that refuses to cut is not a tool — the requester's argument, and a fair one. The combination that gets an explicit acknowledgement is a command that can sever management *with the guard disabled*; that is the only case where nothing catches a mistake. A preview names devices, wave assignment, and the reasons a command looks capable of cutting management access. It warns, never blocks. Devices with no SSH credential are listed before the run rather than producing a wave of identical authentication failures. Per-device output is captured and kept, because "it failed" without the device's own words is not actionable, and because some commands exist purely for output. Verified end to end against real hardware: a read-only command across two devices, halt-on-failure stopping a bad command after one device, and a genuine write applied under Change Guard, confirmed, then reverted — with both devices returned to their original configuration afterwards. Also adds services/sshExec.ts, shared with the key work: it selects a verified key or a password, and throws rather than swallowing. That last part is deliberate — a swallowed error is exactly how the data-cap feature spent a week doing nothing while looking healthy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |