witbitz-code 1.3.9__tar.gz → 1.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. {witbitz_code-1.3.9/src/witbitz_code.egg-info → witbitz_code-1.5.0}/PKG-INFO +13 -2
  2. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/README.md +12 -1
  3. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/pyproject.toml +2 -2
  4. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/__init__.py +1 -1
  5. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/cli.py +25 -2
  6. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/connector.py +2 -0
  7. witbitz_code-1.5.0/src/witbitz_code/ui.py +545 -0
  8. witbitz_code-1.5.0/src/witbitz_code/ui_static/app.css +51 -0
  9. witbitz_code-1.5.0/src/witbitz_code/ui_static/app.js +147 -0
  10. witbitz_code-1.5.0/src/witbitz_code/ui_static/index.html +62 -0
  11. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/uninstall.py +5 -0
  12. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/wizard.py +19 -1
  13. witbitz_code-1.5.0/src/witbitz_code/wslstart.py +135 -0
  14. {witbitz_code-1.3.9 → witbitz_code-1.5.0/src/witbitz_code.egg-info}/PKG-INFO +13 -2
  15. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code.egg-info/SOURCES.txt +8 -1
  16. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_setup.py +22 -1
  17. witbitz_code-1.5.0/tests/test_ui.py +194 -0
  18. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_uninstall.py +11 -1
  19. witbitz_code-1.5.0/tests/test_wslstart.py +42 -0
  20. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/LICENSE +0 -0
  21. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/setup.cfg +0 -0
  22. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/__main__.py +0 -0
  23. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/_js.py +0 -0
  24. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/account.py +0 -0
  25. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/asks.py +0 -0
  26. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/attachments.py +0 -0
  27. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/auto.py +0 -0
  28. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/auto_runner.py +0 -0
  29. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/codekeys.py +0 -0
  30. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/codetools.py +0 -0
  31. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/compress.py +0 -0
  32. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/computers.py +0 -0
  33. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/confidential.py +0 -0
  34. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/confidential_models.json +0 -0
  35. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/devicelink.py +0 -0
  36. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/dirs.py +0 -0
  37. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/folders.py +0 -0
  38. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/gateway.py +0 -0
  39. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/keys.py +0 -0
  40. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/link.py +0 -0
  41. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/mkdir.py +0 -0
  42. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/models.py +0 -0
  43. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/net.py +0 -0
  44. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/offered_models.json +0 -0
  45. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  46. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  47. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  48. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  49. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/outputs.py +0 -0
  50. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/pair.py +0 -0
  51. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/pairings.py +0 -0
  52. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/payers.py +0 -0
  53. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/plugins.py +0 -0
  54. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/policy.py +0 -0
  55. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/ports.py +0 -0
  56. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/qr.py +0 -0
  57. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/receipt.py +0 -0
  58. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/relay.py +0 -0
  59. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/seen.py +0 -0
  60. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/tinfoil_read.py +0 -0
  61. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/tools_catalog.json +0 -0
  62. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/tools_probe.py +0 -0
  63. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/toolsetup.py +0 -0
  64. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/winagent.py +0 -0
  65. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code/winconsole.py +0 -0
  66. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  67. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  68. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code.egg-info/requires.txt +0 -0
  69. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/src/witbitz_code.egg-info/top_level.txt +0 -0
  70. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_asks.py +0 -0
  71. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_attachments.py +0 -0
  72. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_auto.py +0 -0
  73. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_cli.py +0 -0
  74. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_codekeys.py +0 -0
  75. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_codetools.py +0 -0
  76. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_confidential.py +0 -0
  77. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_connector_e2e.py +0 -0
  78. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_devicelink.py +0 -0
  79. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_dirs.py +0 -0
  80. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_files_and_compress.py +0 -0
  81. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_folders.py +0 -0
  82. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_outputs.py +0 -0
  83. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_pair_flows.py +0 -0
  84. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_plugins.py +0 -0
  85. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_policy.py +0 -0
  86. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_ports.py +0 -0
  87. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_ports_win.py +0 -0
  88. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_registry.py +0 -0
  89. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_relay_codec.py +0 -0
  90. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_tools_probe.py +0 -0
  91. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_toolsetup.py +0 -0
  92. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_winagent.py +0 -0
  93. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_winconsole.py +0 -0
  94. {witbitz_code-1.3.9 → witbitz_code-1.5.0}/tests/test_windows_files.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.3.9
3
+ Version: 1.5.0
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -56,6 +56,13 @@ Poppler, Tesseract, … — with winget, Homebrew or one `sudo apt-get` line; it
56
56
  starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C
57
57
  stops it); next time, `witbitz-code serve` is enough.
58
58
 
59
+ **Or in your browser:** `witbitz-code ui` (on Windows: Start menu → Witbitz Code) opens a page for this computer — whether
60
+ it is online, which account it is paired with and this month's spend, Pair (the code is shown large on the page), Unpair,
61
+ Pair again, Start / Stop, and Start when I log in (Windows and WSL). It serves while its window is open; closing that window
62
+ stops it, and `witbitz-code ui` again just opens the page. Each copy has its own page — on one laptop Windows and WSL each
63
+ get theirs, titled with the computer it controls. The page answers only this computer, and only the browser the link
64
+ opened (a one-time key traded for a private cookie; no other website can drive it).
65
+
59
66
  **No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
60
67
  providers (today up to $50 of models and $5 of tools a month per computer). They are sent sealed to this computer, kept in
61
68
  `~/.witbitz/code/account-keys.json` (0600) and never given to OpenCode: the local proxy puts them on each call. Model calls
@@ -77,6 +84,7 @@ The same thing one step at a time: install OpenCode (`curl -fsSL https://opencod
77
84
 
78
85
  | command | what it does |
79
86
  |---|---|
87
+ | `ui [--no-browser]` | The page above. `--no-browser` serves without opening it (what a login start runs). |
80
88
  | `setup [--port <n>]` | The walk-through above. With `--port`, a pairing made for another port moves to it (no new scan). |
81
89
  | `uninstall [--yes] [--remove-keys] [--remove-notes] [--remove-sessions] [--remove-opencode]` | Remove witbitz-code from this computer: its coding keys go back to each account, each account lets go of it, `~/.witbitz/code` and what `serve` added to OpenCode are deleted, and the pipx install removes itself. Asks first (Enter keeps) before your saved keys, project notes, OpenCode's sessions and OpenCode itself; never deletes sessions an OpenCode still has open. `--yes` skips the questions and keeps all four unless their flag is given. |
82
90
  | `pair [--name <name>] [--port <n> \| --opencode-url <url>]` | Show the QR code. The account that scans it gets this computer. `--name` sets the label your devices show (default: the hostname). |
@@ -90,7 +98,10 @@ The same thing one step at a time: install OpenCode (`curl -fsSL https://opencod
90
98
 
91
99
  **Windows and WSL on one computer** are two computers to Spaces, and both can run: WSL shares Windows' ports, so the second
92
100
  one `setup` meets gets its own (it asks — 4097, and 4197 for its model proxy) and `serve` remembers it. WSL's is named
93
- "<computer> (WSL)"; one paired before 1.3.8 is renamed by its next `setup`.
101
+ "<computer> (WSL)"; one paired before 1.3.8 is renamed by its next `setup`. In WSL, `setup` also offers to start witbitz-code
102
+ when you log in to Windows (WSL runs only once Windows starts it): a shortcut in the Windows Startup folder opens a minimized
103
+ window running it (`ui --no-browser`: serving, the page there when you want it) — closing that window stops it, and
104
+ `witbitz-code uninstall` removes the shortcut.
94
105
 
95
106
  **Any folder, any drive.** New session in Code opens at your home folder; ‹ Up goes past it (on Windows up to "This
96
107
  computer", which lists the drives), and a folder can be typed (`D:\projects`). witbitz-code lists the folder names itself
@@ -30,6 +30,13 @@ Poppler, Tesseract, … — with winget, Homebrew or one `sudo apt-get` line; it
30
30
  starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C
31
31
  stops it); next time, `witbitz-code serve` is enough.
32
32
 
33
+ **Or in your browser:** `witbitz-code ui` (on Windows: Start menu → Witbitz Code) opens a page for this computer — whether
34
+ it is online, which account it is paired with and this month's spend, Pair (the code is shown large on the page), Unpair,
35
+ Pair again, Start / Stop, and Start when I log in (Windows and WSL). It serves while its window is open; closing that window
36
+ stops it, and `witbitz-code ui` again just opens the page. Each copy has its own page — on one laptop Windows and WSL each
37
+ get theirs, titled with the computer it controls. The page answers only this computer, and only the browser the link
38
+ opened (a one-time key traded for a private cookie; no other website can drive it).
39
+
33
40
  **No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
34
41
  providers (today up to $50 of models and $5 of tools a month per computer). They are sent sealed to this computer, kept in
35
42
  `~/.witbitz/code/account-keys.json` (0600) and never given to OpenCode: the local proxy puts them on each call. Model calls
@@ -51,6 +58,7 @@ The same thing one step at a time: install OpenCode (`curl -fsSL https://opencod
51
58
 
52
59
  | command | what it does |
53
60
  |---|---|
61
+ | `ui [--no-browser]` | The page above. `--no-browser` serves without opening it (what a login start runs). |
54
62
  | `setup [--port <n>]` | The walk-through above. With `--port`, a pairing made for another port moves to it (no new scan). |
55
63
  | `uninstall [--yes] [--remove-keys] [--remove-notes] [--remove-sessions] [--remove-opencode]` | Remove witbitz-code from this computer: its coding keys go back to each account, each account lets go of it, `~/.witbitz/code` and what `serve` added to OpenCode are deleted, and the pipx install removes itself. Asks first (Enter keeps) before your saved keys, project notes, OpenCode's sessions and OpenCode itself; never deletes sessions an OpenCode still has open. `--yes` skips the questions and keeps all four unless their flag is given. |
56
64
  | `pair [--name <name>] [--port <n> \| --opencode-url <url>]` | Show the QR code. The account that scans it gets this computer. `--name` sets the label your devices show (default: the hostname). |
@@ -64,7 +72,10 @@ The same thing one step at a time: install OpenCode (`curl -fsSL https://opencod
64
72
 
65
73
  **Windows and WSL on one computer** are two computers to Spaces, and both can run: WSL shares Windows' ports, so the second
66
74
  one `setup` meets gets its own (it asks — 4097, and 4197 for its model proxy) and `serve` remembers it. WSL's is named
67
- "<computer> (WSL)"; one paired before 1.3.8 is renamed by its next `setup`.
75
+ "<computer> (WSL)"; one paired before 1.3.8 is renamed by its next `setup`. In WSL, `setup` also offers to start witbitz-code
76
+ when you log in to Windows (WSL runs only once Windows starts it): a shortcut in the Windows Startup folder opens a minimized
77
+ window running it (`ui --no-browser`: serving, the page there when you want it) — closing that window stops it, and
78
+ `witbitz-code uninstall` removes the shortcut.
68
79
 
69
80
  **Any folder, any drive.** New session in Code opens at your home folder; ‹ Up goes past it (on Windows up to "This
70
81
  computer", which lists the drives), and a folder can be typed (`D:\projects`). witbitz-code lists the folder names itself
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "witbitz-code"
7
- version = "1.3.9"
7
+ version = "1.5.0"
8
8
  description = "Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -43,7 +43,7 @@ where = ["src"]
43
43
 
44
44
  # Our OpenCode plugins, installed by `serve` (plugins.py) — copies of tools/opencode-plugins, kept current by embed.mjs
45
45
  [tool.setuptools.package-data]
46
- witbitz_code = ["opencode_plugins/*", "confidential_models.json", "offered_models.json", "tools_catalog.json"]
46
+ witbitz_code = ["opencode_plugins/*", "confidential_models.json", "offered_models.json", "tools_catalog.json", "ui_static/*"]
47
47
 
48
48
  [tool.pytest.ini_options]
49
49
  testpaths = ["tests"]
@@ -4,4 +4,4 @@ The Python implementation of tools/witbitz-code.mjs (docs/opencode-relay.md phas
4
4
  vectors and cross-implementation tests.
5
5
  """
6
6
 
7
- __version__ = "1.3.9"
7
+ __version__ = "1.5.0"
@@ -35,6 +35,8 @@ HELP = f"""witbitz-code {__version__} — reach OpenCode on this computer from t
35
35
 
36
36
  setup [--port <n>] start here: installs OpenCode if needed, pairs this computer — which gives it its own
37
37
  keys from your Spaces account — then serves
38
+ ui [--no-browser] the same in a page in your browser: status, pair / unpair / pair again, start and stop,
39
+ start when I log in (Windows and WSL); it serves while its window is open
38
40
  pair [--name <name>] show a QR code; scan it in Spaces (Settings → Back up & recovery → Add a device)
39
41
  serve [--port <n>] [--no-opencode]
40
42
  start OpenCode on 127.0.0.1 with your account's keys, and the connector
@@ -149,7 +151,9 @@ async def _stop_opencode(child: asyncio.subprocess.Process) -> None:
149
151
  await child.wait()
150
152
 
151
153
 
152
- async def serve(args: list[str]) -> int:
154
+ async def serve(args: list[str], *, stop: Any = None, live_out: dict | None = None) -> int:
155
+ """`stop` and `live_out`: the page's manager (ui.py) runs serve inside itself — it stops it with its own flag instead
156
+ of a signal, and reads the running connector (each pairing's relay state) from live_out."""
153
157
  port_n = to_number(_flag(args, "--port", "4096"))
154
158
  port = int(port_n) if truthy(port_n) and port_n == port_n and float(port_n).is_integer() else 4096
155
159
  everything = pp.load_pairings(log=lambda _m: None)
@@ -207,7 +211,7 @@ async def serve(args: list[str]) -> int:
207
211
  # Billed to the account whose prompt started the turn (payers.py); a call nobody here started is refused.
208
212
  return live_keys.credential(payers.payer(req.header("x-session-id")[:100], req.header("x-parent-session-id")[:100]))
209
213
 
210
- stop = _Stop()
214
+ stop = stop or _Stop()
211
215
  child = None
212
216
  proxy = None
213
217
  live: dict = {"connector": None}
@@ -257,6 +261,8 @@ async def serve(args: list[str]) -> int:
257
261
  connector = await start_connector(pairings, payers=payers)
258
262
  live["status"] = asyncio.ensure_future(_check_keys_every(live_keys, keys_http))
259
263
  live["connector"] = connector
264
+ if live_out is not None:
265
+ live_out["connector"] = connector
260
266
  if proxy:
261
267
  connector.set_confidential_sink(proxy.add_confidential) # the page hands down what the account offers today
262
268
  served = ", ".join(f'"{p.get("name")}" → {p.get("account") or "account"}' for p in pairings)
@@ -265,6 +271,8 @@ async def serve(args: list[str]) -> int:
265
271
  return await _wait_for_stop(stop, child)
266
272
  finally:
267
273
  live["status"].cancel()
274
+ if live_out is not None:
275
+ live_out.pop("connector", None)
268
276
  await connector.aclose()
269
277
  finally:
270
278
  if child is not None:
@@ -379,6 +387,19 @@ async def uninstall(args: list[str]) -> int:
379
387
  return 0 if r.get("done") else 1
380
388
 
381
389
 
390
+ async def ui_command(args: list[str]) -> int:
391
+ """This computer's witbitz-code in a page in your browser (ui.py): status, pair / unpair / pair again, start and stop,
392
+ start when I log in. It runs serve inside itself; --no-browser serves without opening the page (a login start)."""
393
+ from . import net, ui # noqa: PLC0415
394
+
395
+ http = net.new_client()
396
+ keys = pair_cmd._KeyCalls(http)
397
+ try:
398
+ return await ui.run(args, serve=serve, is_listening=is_listening, keys_line=lambda p: pair_cmd._keys_line(p, keys))
399
+ finally:
400
+ await http.aclose()
401
+
402
+
382
403
  async def _main(argv: list[str]) -> int:
383
404
  cmd, rest = (argv[0], argv[1:]) if argv else ("", [])
384
405
  if cmd == "uninstall":
@@ -392,6 +413,8 @@ async def _main(argv: list[str]) -> int:
392
413
  return await pair_cmd.main(rest)
393
414
  if cmd == "serve":
394
415
  return await serve(rest)
416
+ if cmd == "ui":
417
+ return await ui_command(rest)
395
418
  if cmd == "status":
396
419
  return await pair_cmd.main(["--status"])
397
420
  if cmd == "rotate":
@@ -194,6 +194,7 @@ class PairingServer:
194
194
  # and run again. Each socket therefore announces a fresh random nonce inside its sealed hello, and a request
195
195
  # must carry the CURRENT one: a recording from before a restart or reconnect carries a nonce nobody accepts.
196
196
  self.nonce = ""
197
+ self.relay_state = "connecting" # the relay socket's last state — the page (ui.py) shows "online" when every one is open
197
198
  self.peer = RelayPeer(secret=pairing["secret"], role="computer", relay=pairing.get("relay") or RELAY_URL,
198
199
  on_state=self._on_state, on_peers=self._on_peers, on_message=self._on_message, connect=connect,
199
200
  # A page socket fell out of the replay window: its recorded frames would open again — rotate the
@@ -250,6 +251,7 @@ class PairingServer:
250
251
 
251
252
  # ── relay events ──────────────────────────────────────────────────────────────────────────────────────────────────
252
253
  def _on_state(self, s: str) -> None:
254
+ self.relay_state = s
253
255
  self._log(f"witbitz-code: {self.name} · relay {s}")
254
256
  if s == "open":
255
257
  self._rotate()