管理页新增：允许/禁止新设备接入 + 修改设备配对码

== 功能一：允许新设备接入开关 ==

关掉之后只有已经登记过的电脑能继续上线，没见过的新设备一律拒绝。
状态存在 relay_state.json 里，重启不丢。

判断新设备用的是 deviceId，不是配对码 ——
这一点很关键：管理员给老设备改过码之后，
那台设备必须仍然能进来；按码判断的话它会被自己的开关挡在外面。
测试里专门有一条 A3b 守这个行为。

被拒时回 blocked=True，让客户端能区分被管理员挡了
和配对码冲突，而不是去匹配中文错误文案（文案一改就坏）。

== 功能二：修改任意设备的配对码 ==

管理页每台设备一个「改码」按钮，填 6 位数字即生效。
要点：
  * 新旧码冲突检查
  * 旧码映射必须删掉，否则变成孤儿条目，
    管理页配对码占用的数字会虚高
  * 改码踢掉该设备当前会话（会话绑在码上）
  * 立刻持久化

== 顺手修掉两个真 bug ==

1. 改码功能一上线就 500：Session 用了 __slots__，
   根本没有 session_id 属性（会话是以 token 为 key 存在 dict 里的），
   写 s.session_id 直接 AttributeError。
   改成遍历 sessions.items() 拿 key。

2. snapshot() 里有同样的错（清理超时设备那段）：
   平时 stale 列表是空的所以从没炸过，一旦真有设备超过 6 小时
   没上报就会 500。这次一起修了。

== 管理页 UI ==
  * 设备小节加了「允许新设备接入」勾选框
  * 每行加「改码」按钮（走 prompt，不引入弹窗层）
  * 改码按钮用 data-recode 而不是 data-device ——
    现有单台删除按 button.danger[data-device] 绑，
    带上 data-device 会被当成删除按钮，点改码变删设备。
    测试 C6 专门断言这一点。

== 测试 ==
新增 _tests/test_admin_device_control.py（23 项），
已注册进 _tests/verify_all.ps1 第 3e 阶段。
