The backend already refused a predicted lockout with 409 + a verdict, but the
frontend surfaced it as a generic "failed" message, so the reasoning never reached
the user.
- LockoutVerdictDialog renders the verdict properly: each violated invariant with
its plain-English consequence, and the resolved management path ("how the manager
reaches this device") so the user can verify the claim rather than take it on
trust. Critical verdicts require typing the device name to override; warnings
need only an acknowledgement, per the agreed friction levels.
- lockoutVerdictOf() extracts the 409 payload, mirroring the existing lockoutOf()
helper used by the firewall guard.
- VLAN add/update/delete now capture the verdict and re-send with confirm_lockout
when the user accepts, instead of showing an error banner.
Verified end to end on the production switch: changing the ingress port's PVID off
the management VLAN returns severity=critical with the ingress-port-pvid violation
and the hop chain (192.168.0.40/24 -> bridge -> sfp28-1), and the shipped bundle
contains the new dialog. Full CI preflight green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add 'Copy VLANs from Another Switch' — a 3-step modal available on the
VLANs tab of any switch device. Step 1 selects a source switch and which
VLAN IDs to copy (VLAN 1 deselected by default with a management risk
warning). Step 2 lets the user assign interfaces on the target device
per VLAN: clicking a port chip cycles it through None → Tagged → Untagged
→ None, with a live summary and bridge picker per VLAN. Conflict detection
identifies VLANs already present on the target and offers per-VLAN
Skip/Overwrite with the port grid disabling itself when Skip is active.
Step 3 shows the full plan before a single apply call executes all
operations over one device connection and reports per-VLAN results.
Also fix CPU load display on the Devices page for hardware switches that
report 0% CPU via ASIC offloading — the previous filter (v > 0) silently
dropped all valid zero readings; now filters on field presence instead.