Commit Graph
8 Commits
Author SHA1 Message Date
2GT-RichandClaude Opus 5 1ab2e6547e feat(ssh): per-device SSH keys, deployed and proved (v0.23.24-beta)
Closes #110. Also the prerequisite for #118 — arbitrary console commands need
SSH on every device, and stored passwords do not scale to sixty of them.

Each device gets its own ed25519 keypair, generated on the manager, pushed over
the existing password session, imported, and then *proved* by opening a fresh
connection and authenticating with it. Nothing trusts a key until that last step
succeeds. Backups and config export use a verified key where one exists.

Verified end to end on hardware: deploy, rotate twice leaving exactly one
authorised key each time, verify, and a 4.6 KB config backup pulled over
key-only SSH with no password fallback.

**The design note on the issue was wrong about the central safety property**, and
testing on a switch is what proved it. I claimed the stored password remained as
a fallback. It does not: RouterOS binds a key to a user and then refuses password
authentication for that user entirely. Removing the key restored password login
immediately, which settles the causation.

That mistake compounded with a second one. Deployment wrote the new private key
to the database before proving it, so a rotation that failed halfway left the
database holding a key the device had never authorised while the device demanded
one we no longer had — SSH lost on the test switch, recoverable only through the
binary API. Three changes follow:

  * Rotation authenticates with the *existing* key, since the password is no
    longer an option once a key is installed.
  * Nothing is written until a fresh connection has authenticated with the new
    key. A deployment that fails verification removes the key it just installed
    rather than leaving the device demanding a credential nobody holds.
  * The binary API, unaffected by any of this, is the removal path — and is
    therefore the real recovery route, not the password.

Two further faults came out of live testing rather than reasoning. Every key for
a device shared one comment, so removing "the old key" during a rotation removed
the new one too; comments are now unique per key. And RouterOS stores that
comment in `info`, not `key-owner` — matching the wrong field silently matched
nothing and left every rotation's old key behind, which is why the switch
accumulated keys that later blocked it.

Revoke now clears *every* key this manager installed for a device, not just the
one on record. Orphans from a half-failed rotation otherwise keep password SSH
disabled with no way back through the product, which is exactly the state the
test switch got stuck in. When deployment fails with an authentication error and
orphans are present, the message says so instead of leaving someone guessing.

The UI states the trade before it is made: installing a key disables password SSH
for that user, the API and WinBox are unaffected, and revoking restores it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:39:38 -07:00
2GT-RichandClaude Opus 5 1332f7384d fix(firmware): make the per-device update button actually install (v0.23.16-beta)
The "Install update" button on a device's Config tab reported success and did
nothing. Staged rollouts were unaffected, which is why this survived: the two
paths do the same job through different code, and only one of them was right.

RouterOS installs the version discovered by `check-for-updates` **in the same
API session**. Issued alone, `/system/package/update/install` replies !done and
takes no action — no download, no reboot, no error. The route issued it alone.
The rollout orchestrator calls the check first, so it always worked.

Confirmed by A/B on one switch, two minutes apart. Through the button: HTTP 200,
"Update installation initiated. Device will reboot", and ninety seconds later
the device still reported 7.24 with "New version is available" and had not
rebooted. Through a rollout: reboot within fourteen seconds, upgraded to 7.24.1.

Two further faults in the same handler, both of which made the first one harder
to see:

  * `firmware_update_available` was cleared the moment the command was sent, so
    every screen showed the device as current while it sat unchanged. The next
    poll flipped it back, which reads as an update that "keeps coming back".
  * Success was reported on submit rather than on outcome, so a device that was
    already up to date and a device that had genuinely started an upgrade
    produced identical messages.

The route now checks first, distinguishes "installing 7.24.1" from "already
running 7.24.1 — nothing to install", and leaves the flag alone: an upgrade
takes minutes and a reboot, neither of which an HTTP request can wait for.

Ownership of that flag moves to the version read itself. collectSystemInfo now
clears it when the observed version matches the known latest, because the
dedicated firmware check runs only once a day — without this, not clearing
optimistically would leave a device flagged for up to 24 hours after it was
upgraded. It also fixes the same staleness when a device is upgraded outside the
platform entirely.

install-routerboard had the identical shape and gets the identical treatment.
It matters more there: `/system/routerboard/upgrade` on a current bootloader is
a no-op, but the code rebooted the device afterwards regardless — an outage in
exchange for nothing.

Verified against the live fleet: the fixed button upgraded 2GT-NW-BigSwitch
7.24 -> 7.24.1; an already-current device answered "nothing to install" instead
of claiming a reboot; the flag stayed set during the upgrade and cleared once
the new version was observed; and a device genuinely behind stayed flagged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:40:24 -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-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 68d88d0306 feat: RouterBOOT upgrade support, update UI overhaul, discovery fixes (v0.10.6 Beta)
RouterOS vs RouterBOOT distinction:
- Rename "Firmware Updates" → "RouterOS Updates" everywhere (Config tab,
  dashboard banner, devices list tooltip) to accurately reflect what is
  being updated
- Add RouterBOOT bootloader upgrade support as a distinct operation:
  new DeviceCollector methods checkRouterboardUpgrade() and
  installRouterboardUpgrade() using /system/routerboard/print and
  /system/routerboard/upgrade
- New API endpoints: POST /api/devices/:id/check-routerboard and
  POST /api/devices/:id/install-routerboard
- Daily poller now also checks RouterBOOT upgrade status while already
  connected during firmware check cycle, with first-discovery alerts

Config tab update panel:
- Combine RouterOS and RouterBOOT into a single "Software & Firmware
  Updates" card with two subsections separated by a divider
- Pre-populate both sections from stored DB state on load — no longer
  requires a manual "Check" click to see a known pending update
- Enforce update order: when both are pending, show a blue info banner
  explaining RouterOS must be installed first, and disable the
  RouterBOOT "Upgrade & Reboot" button until the RouterOS update clears
- Fix GET /api/devices/:id to include firmware_update_available,
  latest_ros_version, routerboard_upgrade_available, upgrade_firmware_version
  (these were missing from the detail endpoint)

Dashboard & devices list:
- Combined amber banner now shows two distinct rows: RouterOS (N) with
  device links and RouterBOOT (N) with device links — one dismiss for both
- Devices list: amber ArrowUpCircle = RouterOS update, blue Cpu icon =
  RouterBOOT upgrade, each with distinct tooltips
- Fix GET /api/devices (list) to include routerboard_upgrade_available and
  upgrade_firmware_version (these were missing, causing RouterBOOT
  notifications to never appear on the dashboard)

DB migration:
- Add routerboard_upgrade_available BOOLEAN DEFAULT FALSE
- Add upgrade_firmware_version VARCHAR(20)

Discovery system fixes:
- Fix duplicate entries: replace single-key (mac || ip || identity) dedup
  with three cross-referenced indexes so a neighbor seen via LLDP by one
  device and via MNDP by another always merges into one row
- Fix offline devices persisting: handleDeviceFailure now deletes
  topology_links for the offline device (same pattern as clients)
- Add periodic stale-link sweep every 15 min that removes topology_links
  rows older than 20 min as a safety net for devices that go offline
  without cleanly triggering handleDeviceFailure
2026-04-23 13:46:20 -07:00
2GT-RichandClaude Sonnet 4.6 c203011494 Fix all ESLint errors so CI passes on every branch
- Move nested components outside parent functions (GraphsTab, ClientDetailPage, NetworkServicesDHCPPage)
- Add eslint-disable comments for intentional setState-in-effect patterns (form sync in DNS, NTP, Wireless pages)
- Fix unescaped quote entities in JSX (SystemConfigTab, GlobalSearch, TopologyPage)
- Fix empty catch block in main.tsx
- Convert ternary-as-statement to if/else in RadiosTab
- Move Date.now() calls into useMemo in DashboardPage (react-hooks/purity)
- Fix DOM mutation in event handler in TerminalModal (react-hooks/immutability)
- Wrap ref assignment in useLayoutEffect in useSocket (react-hooks/refs)
- Update TypeScript devDependency version pin to match installed version

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:20:03 -07:00
Rich Teslow 6b0fc9fdf1 Initial commit baby! 2026-04-13 18:15:41 -07:00