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>