From 6318f0c770ad261e88df2c31249ca66d953544b7 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Tue, 1 Sep 2026 22:34:02 +0200 Subject: [PATCH] Unify browser connection controls (Connect / Disconnect) --- docs/agent/architecture.md | 2 +- docs/agent/code-map.md | 2 +- docs/command_reference.md | 2 +- docs/roadmap.md | 2 +- src/web_ui.c | 25 ++++++++++++++----------- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/agent/architecture.md b/docs/agent/architecture.md index 30e63bc..1b6c1ca 100644 --- a/docs/agent/architecture.md +++ b/docs/agent/architecture.md @@ -102,7 +102,7 @@ HTTP Basic authentication uses `user_database`. Before administrator bootstrap, The boot-local Basic-authentication cache has four RAM entries and a five-minute sliding lifetime. It stores a keyed digest of the complete `Authorization` header rather than the raw header, and every hit revalidates principal currentness. Its current lack of locking relies on the single-HTTPD-owner execution model. -A WebSocket connection requires a one-time, principal-bound ticket with a maximum 30-second lifetime. Only four tickets can be outstanding; minting another evicts the live entry with the earliest expiry. Ticket issuance and upgrade also validate a supplied `Origin` against `https://`; absence of `Origin` is accepted for non-browser clients. Tickets are stored as digests, consumed before currentness validation, and are never persisted. An admitted session starts the serial service if necessary, creates a broker client, and opportunistically requests writer ownership. The web transport has two fixed session slots. Binary frames carry serial data; small text messages request or release writer ownership. HTTPD owns socket send/close operations, while the web transport task mediates broker work through bounded scheduling. Browser Disconnect closes the WebSocket and pauses automatic reconnect until the user explicitly selects Reconnect. +A WebSocket connection requires a one-time, principal-bound ticket with a maximum 30-second lifetime. Only four tickets can be outstanding; minting another evicts the live entry with the earliest expiry. Ticket issuance and upgrade also validate a supplied `Origin` against `https://`; absence of `Origin` is accepted for non-browser clients. Tickets are stored as digests, consumed before currentness validation, and are never persisted. An admitted session starts the serial service if necessary, creates a broker client, and opportunistically requests writer ownership. The web transport has two fixed session slots. Binary frames carry serial data; small text messages request or release writer ownership. HTTPD owns socket send/close operations, while the web transport task mediates broker work through bounded scheduling. The browser's combined Connect/Disconnect control closes the WebSocket and pauses automatic reconnect; after a user-paused disconnect it changes to Connect, which resumes connection attempts. Web serial initialization is failure-isolated from the base HTTPS service: if the transport cannot initialize, `web_server_init()` can still succeed and serve authenticated non-WebSocket routes. diff --git a/docs/agent/code-map.md b/docs/agent/code-map.md index 49f55cd..801d793 100644 --- a/docs/agent/code-map.md +++ b/docs/agent/code-map.md @@ -68,7 +68,7 @@ This is a semantic map, not a complete file inventory. Start here, then read the - Dependencies: user database, secure random, broker, successful Wi-Fi manager initialization at boot, mbedTLS/HTTPS server; actual network reachability is an operational prerequisite, not an initializer invariant - Flow: `browser -> HTTPS Basic auth -> ticket -> WebSocket -> web transport -> broker` - Ownership: HTTPD owns socket send/close work; transport task owns broker mediation; two fixed WebSocket slots and four outstanding tickets. -- Security constraints: Basic-auth cache hits still revalidate principal currentness; browser Disconnect closes the WebSocket and pauses automatic reconnect until Reconnect is selected. Changes to the authored inline loader must update its hard-coded CSP hash in the same change. +- Security constraints: Basic-auth cache hits still revalidate principal currentness; the browser's combined Connect/Disconnect control closes the WebSocket and pauses automatic reconnect until Connect is selected. Changes to the authored inline loader must update its hard-coded CSP hash in the same change. - Asset constraint: `web_assets_data.c` is checked-in generated input to the build; do not hand-edit or regenerate casually. ## SSH diff --git a/docs/command_reference.md b/docs/command_reference.md index 1a7dae7..c6c598e 100644 --- a/docs/command_reference.md +++ b/docs/command_reference.md @@ -133,7 +133,7 @@ When the Wi-Fi station receives an IPv4 address, the Wi-Fi manager announces `sa | `web certificate rotate --force` | Replace the HTTPS certificate and private key. | | `web reset --force` | Explicitly replace missing, incompatible, or damaged legacy credentials and web material. | -HTTPS listens on port 443 only. Authenticate with any current user-database username/password; both `user` and `admin` roles receive the existing status and browser-terminal interface. The device serves vendored xterm.js without Internet access. Browser sessions use one-time account-bound tickets, binary WebSocket frames, and the broker's one-writer rule. Selecting **Disconnect** closes the current WebSocket and pauses automatic reconnect; select **Reconnect** to resume connection attempts. Account mutations revoke only that account's tickets and sessions. +HTTPS listens on port 443 only. Authenticate with any current user-database username/password; both `user` and `admin` roles receive the existing status and browser-terminal interface. The device serves vendored xterm.js without Internet access. Browser sessions use one-time account-bound tickets, binary WebSocket frames, and the broker's one-writer rule. The combined **Connect**/**Disconnect** control closes the current WebSocket and pauses automatic reconnect when active; after a user-paused disconnect, it changes to **Connect** to resume connection attempts. Account mutations revoke only that account's tickets and sessions. ## SSH serial transport diff --git a/docs/roadmap.md b/docs/roadmap.md index b0d5b3e..832742c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -141,7 +141,7 @@ Implemented and hardware-validated: - Viewport-constrained terminal fitting without recursive page growth. - Validation with multiple clients, ANSI colors, advanced escape sequences, and full-screen terminal software. -Post-validation enhancement implemented; browser regression validation pending: Disconnect explicitly closes the terminal WebSocket and pauses automatic reconnect until Reconnect is selected. +Post-validation enhancement implemented; browser regression validation pending: the combined Connect/Disconnect control explicitly closes the terminal WebSocket and pauses automatic reconnect until Connect is selected. ### Phase 6 — Authenticated SSH serial transport diff --git a/src/web_ui.c b/src/web_ui.c index 1fbfdfe..c2f75dc 100644 --- a/src/web_ui.c +++ b/src/web_ui.c @@ -119,8 +119,7 @@ static const char s_index_html[] = "
\n" "\n" "\n" - "\n" - "\n" + "\n" "
\n" "

" "Observer mode — terminal input is disabled.

\n" @@ -152,8 +151,7 @@ static const char s_app_js[] = "const connectionDetail = element('connection-detail');\n" "const requestControl = element('request-control');\n" "const releaseControl = element('release-control');\n" - "const disconnectButton = element('disconnect');\n" - "const reconnectButton = element('reconnect');\n" + "const connectionToggle = element('connection-toggle');\n" "const terminalHost = element('terminal');\n" "const terminal = new Terminal({\n" " allowProposedApi: false, convertEol: false, cursorBlink: true, disableStdin: true,\n" @@ -196,7 +194,10 @@ static const char s_app_js[] = " terminal.options.disableStdin = !writer;\n" " requestControl.disabled = !socketOpen() || writer;\n" " releaseControl.disabled = !socketOpen() || !writer;\n" - " disconnectButton.disabled = unloading || (!reconnectEnabled && socket === null && ticketAbort === null);\n" + " const connectionActive = reconnectEnabled || socket !== null || ticketAbort !== null || reconnectTimer !== null;\n" + " connectionToggle.disabled = unloading;\n" + " connectionToggle.textContent = connectionActive ? 'Disconnect' : 'Connect';\n" + " connectionToggle.classList.toggle('danger', connectionActive);\n" " inputState.dataset.enabled = writer ? 'true' : 'false';\n" " inputState.textContent = writer\n" " ? 'Writer mode — terminal input is enabled.'\n" @@ -335,7 +336,14 @@ static const char s_app_js[] = "releaseControl.addEventListener('click', () => {\n" " if (role === 'writer' && socketOpen()) socket.send('release-writer');\n" "});\n" - "disconnectButton.addEventListener('click', () => {\n" + "connectionToggle.addEventListener('click', () => {\n" + " const connectionActive = reconnectEnabled || socket !== null || ticketAbort !== null || reconnectTimer !== null;\n" + " if (!connectionActive) {\n" + " reconnectEnabled = true;\n" + " reconnectDelay = 1000;\n" + " connect();\n" + " return;\n" + " }\n" " reconnectEnabled = false;\n" " ++connectionGeneration;\n" " clearReconnectTimer();\n" @@ -346,11 +354,6 @@ static const char s_app_js[] = " setRole('observer');\n" " setConnection('Disconnected', 'warn', 'Disconnected by user. Automatic reconnect is paused.');\n" "});\n" - "reconnectButton.addEventListener('click', () => {\n" - " reconnectEnabled = true;\n" - " reconnectDelay = 1000;\n" - " connect();\n" - "});\n" "const fitTerminal = () => {\n" " fitFrame = 0;\n" " const bounds = terminalHost.getBoundingClientRect();\n"