"""Pure web-UI projection of ``VirtualSwitchState`` (render + apply).
The exact inverse of the Task-3 parsers and Task-4 form encoders: ``render_page``
turns device state into the documented HTML shape the parsers consume, and
``apply_form`` mutates the state from a POSTed form body. No network here — the
``VirtualHttpFace`` (Task 11) wraps these in an ``http.server`` handler.
The rendered HTML is deliberately minimal but carries every field the parsers
read, including a constant CSRF ``hash`` token on each writable page. Routing
(deciding whether a requested path is one a given model's ``http_spec``
actually advertises, and returning a 404-equivalent for anything else) is
Task 11's job at the I/O boundary; this module renders/applies whatever page
its caller already resolved against ``spec``.
"""
from __future__ import annotations
import re
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..protocols.http.endpoints import HttpModelSpec
from .state import PoeSim, VirtualSwitchState
_HASH = "virtualhash"
_DETECT_TEXT = {3: "Delivering", 1: "Searching", 2: "Disabled", 4: "Fault"}
[docs]
def render_login(rand: str) -> str:
return (
f"<html><body><form>"
f'<input type="hidden" id="rand" name="rand" value="{rand}">'
f'<input type="hidden" name="hash" value="{_HASH}">'
f"</form></body></html>"
)
def _hash_input() -> str:
return f'<input type="hidden" name="hash" value="{_HASH}">'
def _has_csrf_hash(spec: HttpModelSpec) -> bool:
"""Whether this model's real web UI carries an ``<input name="hash">``.
ONLY the Plus ``.cgi`` dialects do. MEASURED 2026-08-02: a live capture of
the FASTPATH ``vlanStatus.html`` on gsm7252ps (10.1.5.22, committed as
``tests/fixtures/http/gsm7252ps_vlan_status_live.html``) carries no ``hash``
input anywhere -- only ``applet_port``, ``applet_unit``, ``dbgopt`` and the
XUI cell hiddens.
This mock used to emit the token on EVERY page regardless of dialect, and
that is how ``HttpWriter.create_vlan`` -- which scrapes the token and is
written for the Plus form -- passed here while failing on real FASTPATH
hardware with "no CSRF 'hash' token on page before write". The mock and the
writer agreed with each other while both disagreed with the device, which
is exactly the failure principle 5 exists to prevent. Emitting it only
where the hardware does makes the mock refuse the same write the switch
refuses.
"""
from ..protocols.http.endpoints import dialect_has_csrf_hash
return dialect_has_csrf_hash(spec.html_dialect)
[docs]
def render_page(
state: VirtualSwitchState, spec: HttpModelSpec, path: str, form: dict[str, str]
) -> str:
html = _render_page_body(state, spec, path, form)
if not _has_csrf_hash(spec):
html = html.replace(_hash_input(), "")
return html
def _render_page_body(
state: VirtualSwitchState, spec: HttpModelSpec, path: str, form: dict[str, str]
) -> str:
if path == spec.dashboard_path:
return _render_dashboard(state)
if path == spec.stats_path:
return _render_stats(state)
if path == spec.poe_status_path:
return _render_poe_status(state)
if path == spec.pvid_path:
return _render_pvid(state)
if path == spec.vlan_config_path:
return _render_vlan_cfg(state)
if path == spec.vlan_membership_path:
vid = int(form.get("VLAN_ID", "1"))
return _render_membership(state, vid)
if path == spec.poe_config_path:
return f"<html><body>{_hash_input()}</body></html>"
return f"<html><body>OK{_hash_input()}</body></html>"
def _render_dashboard(state: VirtualSwitchState) -> str:
rows = "".join(
f'<tr class="portID"><td><input type="checkbox"></td>'
f"<td>{p}</td>"
f"<td>{('Up ' + str(sim.speed) + 'M') if sim.link else 'Down'}</td>"
f"<td>{'Enabled' if sim.admin else 'Disabled'}</td>"
f"<td>{sim.name}</td></tr>"
for p, sim in sorted(state.ports.items())
)
return f"<html><body>{_hash_input()}<table>{rows}</table></body></html>"
def _render_stats(state: VirtualSwitchState) -> str:
rows = "".join(
f'<tr class="portID"><td>{p}</td>'
f"<td>{sim.rx_octets or 0}</td><td>{sim.tx_octets or 0}</td>"
f"<td>{sim.rx_errors or 0}</td></tr>"
for p, sim in sorted(state.ports.items())
)
return f"<html><body><table>{rows}</table></body></html>"
def _render_poe_status(state: VirtualSwitchState) -> str:
def _detect_text(psim: PoeSim) -> str:
if not psim.admin:
return "Disabled"
return _DETECT_TEXT.get(psim.detect, "Disabled")
rows = "".join(
f'<tr class="portID"><td>{p}</td>'
f"<td>{_detect_text(psim)}</td>"
f"<td>{psim.power_mw}</td></tr>"
for p, psim in sorted(state.poe.items())
)
return f"<html><body>{_hash_input()}<table>{rows}</table></body></html>"
def _render_pvid(state: VirtualSwitchState) -> str:
rows = "".join(
f'<tr class="portID"><td><input type="checkbox" name="port{p - 1}"></td>'
f'<td sel="text">{p}<input type="hidden" value="1"></td>'
f'<td sel="input">{state.pvids.get(p, 1)}</td></tr>'
for p in sorted(state.ports)
)
return f"<html><body>{_hash_input()}<table>{rows}</table></body></html>"
def _render_vlan_cfg(state: VirtualSwitchState) -> str:
boxes = "".join(
f'<input type="checkbox" name="vlanck{i}" value="{vid}">'
for i, vid in enumerate(sorted(state.vlans))
)
return f"<html><body>{_hash_input()}{boxes}</body></html>"
def _render_membership(state: VirtualSwitchState, vid: int) -> str:
from ..registry import get_model
vsim = state.vlans.get(vid)
port_count = get_model(state.model_key).port_count
chars = []
for p in range(1, port_count + 1):
if vsim is None or p not in vsim.member:
chars.append("3")
elif p in vsim.untagged:
chars.append("1")
else:
chars.append("2")
hidden = "".join(chars)
options = "".join(
f'<option {"selected " if v == vid else ""}value="{v}">VLAN {v}</option>'
for v in sorted(state.vlans)
)
return (
f"<html><body><form>{_hash_input()}{options}"
f'<input name="hiddenMem" id="hiddenMem" value="{hidden}" type="hidden">'
f"</form></body></html>"
)
def _apply_poe(state: VirtualSwitchState, form: dict[str, str]) -> None:
if form.get("ACTION") == "Apply" and "portID" in form:
port = int(form["portID"]) + 1
if port in state.poe:
on = form.get("ADMIN_MODE") == "1"
state.poe[port].admin = on
state.poe[port].detect = 3 if on else 1
elif form.get("ACTION") == "Reset":
for key in form:
m = re.fullmatch(r"port(\d+)", key)
if m:
port = int(m.group(1)) + 1
if port in state.poe:
state.poe[port].detect = 3 if state.poe[port].admin else 1
def _apply_pvid(state: VirtualSwitchState, form: dict[str, str]) -> None:
vlan = int(form.get("pvid", "0"))
if vlan <= 0:
return
for key in form:
m = re.fullmatch(r"port(\d+)", key)
if m:
state.pvids[int(m.group(1)) + 1] = vlan
def _apply_membership(state: VirtualSwitchState, form: dict[str, str]) -> None:
vid = int(form["VLAN_ID"])
vsim = state.vlans.get(vid)
if vsim is None:
return
hidden = form["hiddenMem"]
member: set[int] = set()
untagged: set[int] = set()
for i, ch in enumerate(hidden):
port = i + 1
if ch == "1":
member.add(port)
untagged.add(port)
elif ch == "2":
member.add(port)
vsim.member = member
vsim.untagged = untagged
def _apply_vlan_cfg(state: VirtualSwitchState, form: dict[str, str]) -> None:
from .state import VlanSim
action = form.get("ACTION")
if action == "Add" and "ADD_VLANID" in form:
vid = int(form["ADD_VLANID"])
state.vlans.setdefault(vid, VlanSim(name=""))
elif action == "Delete":
for key, val in form.items():
if re.fullmatch(r"vlanck\d+", key):
state.vlans.pop(int(val), None)