VM NIC surfaces present Networks, not vSwitches — design
Synced read-only from
/home/synnet/mirrors/synvirt-product/docs/superpowers/specs/2026-07-10-vm-nic-network-picker-design.md. Edit at the source, not here.
VM NIC surfaces present Networks, not vSwitches — design
Section titled “VM NIC surfaces present Networks, not vSwitches — design”Date: 2026-07-10 · Status: approved (UI + backend scope)
Problem
Section titled “Problem”The VM-facing NIC editors added with the Network-tab feature (5c86ff56)
present the wrong abstraction: AddNicModal and ChangeNetworkModal
offer a vSwitch selector plus a raw VLAN slider (0-4094), and the
Network-tab rows label each adapter with the bridge name. Product
doctrine is the opposite — a VM maps to a Network (a named slice of
a vSwitch: NetworkInfo { name, vlan, default }), never to a bare
vSwitch. The doctrine is already encoded elsewhere:
useNetworks.ts: “A VM is mapped to a Network, not to a bare vSwitch.”- The create-VM wizard (
NetworkInterfacesConfigurator) and the hardware editor’sNicCardboth present declared Networks by name. - The vocabulary table (CLAUDE.md §4.2): vSwitch/bridge → Network, VLAN ID → Network tag; raw terms only in Advanced/tooltips.
- The daemon’s PUT
/vms/{name}/nics/{mac}already rejects a(virtswitch, virtlan_tag)pair that no declared Network covers (E_VIRTLAN_NOT_DECLARED).
A related API-level hole: POST /vms/{name}/nics (attach) performs no
such validation — any tag on any switch attaches successfully, so the
API can create adapters on undeclared networks that the PUT would refuse.
Design
Section titled “Design”Frontend — Network picker (both modals)
Section titled “Frontend — Network picker (both modals)”One selector listing the flat declared Networks from useNetworks
(all vSwitches): Network name as primary text, vSwitch · VLAN n
(or untagged) as secondary. The chosen row projects to the existing
{virtswitch, virtlan_tag} body — no API contract change.
AddNicModal: picker replaces the vSwitch control + untagged checkbox + VLAN slider. The host’sdefaultNetwork is preselected. Model + MAC controls stay as-is.ChangeNetworkModal: same picker, seeded with the NIC’s current Network (matched by bridge+tag). If the current combo matches no declared Network (legacy adapter), a fallback row labelledbridge · VLAN nrepresents the current state so it is always visible; every other option is a declared Network.- No raw-VLAN escape hatch (YAGNI): the daemon refuses undeclared combos; declaring a Network in the Networking view is the workflow.
- Empty state: “No networks declared” + pointer to the Networking view.
Frontend — Network-tab rows
Section titled “Frontend — Network-tab rows”Primary chip = resolved Network name (same resolution NicCard
already uses: match bridge+tag against useNetworks rows; fallback
bridge · VLAN n). vSwitch/VLAN demoted to secondary text. Copy in
the tab (empty state, modal subtitles) stops saying “vSwitch”.
Backend — attach validates like edit
Section titled “Backend — attach validates like edit”vm_edit::nics::attach_nic gains the same gate as
vm_nic_edit::edit_nic: resolve the NetworkController snapshot,
404 E_VIRTSWITCH_NOT_FOUND when the switch does not exist, 400
E_VIRTLAN_NOT_DECLARED when no declared Network on it carries the
tag. Unit tests mirror the PUT’s cases.
Rejected alternatives
Section titled “Rejected alternatives”- Cascading vSwitch → Network picker: reintroduces the vSwitch as the primary decision — the exact error being fixed.
- New flat
/api/v1/networksendpoint: unnecessary;useNetworksalready flattens the nested/api/v1/vswitchesshape client-side.
Testing
Section titled “Testing”- Cargo: attach validation cases (missing switch, undeclared tag,
declared tag OK) alongside the existing 18
vm_nic_edittests. - Vitest: modal specs updated to drive the Network picker; NetworkTab row-label spec. Judge by diff-vs-baseline (baseline is not green).
- E2E
e2e/vm-network-tab.spec.ts: drive the picker by Network name against a deployed host; fleet deploy lockstep afterwards.