web-control-panel.md 9.4 KB


type: integration title: Web control panel description: The optional HTTP control panel in src/web.rs and src/web_page.html, enabled by web_port — routes (including POST /api/relink), the custom-header guard on POST, the request/reply bridge to App through AppEvent::Web, the state with screen and pairing QR as SVG, the quit and relink flows, the browser page with its pairing card and its security caveats. tags: [web, http, tiny_http, control-panel, security, autoreply, qr-code, relink] sources:

  • id: openwiki-source-9fec9d1af4 resource: repo://src/app.rs
  • id: openwiki-source-b55a21a31e resource: repo://src/main.rs
  • id: openwiki-source-ac7a3eaa39 resource: repo://src/web_page.html
  • id: openwiki-source-7b75006986 resource: repo://src/web.rs generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" } verified:
  • by: openwiki/0.7.2 at: 2026-10-23T21:26:24.156Z ---

Web control panel

src/web.rs is an optional, minimal HTTP server that lets a browser (for example a phone on the same network) see the chats, toggle the global and per-chat AI autoreply, pair WhatsApp by scanning the QR code shown in the page, request a new pairing, or quit the bot. It is off unless web_port is set in config.toml (see Configuration and data). It has no authentication and listens on all interfaces (0.0.0.0, src/web.rs:17-19).

Startup and threading

main calls start_web_panel, which calls web::start(port, app_tx) only when web_port is set (Runtime and event loop). start (src/web.rs:191-203) binds a tiny_http::Server on 0.0.0.0:<port> and spawns a detached OS thread named wh-web. A bind failure aborts startup with cannot start web server on port <port>. The port is read once: unlike the other settings, a changed web_port is not applied by the configuration live reload and needs a restart.

serve (src/web.rs:205-217) handles requests one at a time on that thread and stops after a request once the application channel is closed. tiny_http is the only extra dependency for the server; the qrcode crate (already used by the TUI) is built with its svg feature for the pairing QR (Cargo.toml).

Routes

resolve_route (src/web.rs:352-367) matches method and path; the query string is ignored (path_without_query, src/web.rs:344-348):

Method Path Effect Response
GET / Serves the embedded page (include_str!("web_page.html")) 200 HTML
GET /api/state WebCommand::State 200 JSON state
POST /api/global WebCommand::ToggleGlobal 200 JSON state
POST /api/chat WebCommand::ToggleChat(<body>); body = chat JID as plain text 200 JSON state, 400 on bad body
POST /api/relink WebCommand::Relink 200 JSON state
POST /api/quit WebCommand::Quit 200 {"status":"Bot in chiusura"}
other other — 404 non trovato

The JSON state is WebState (src/web.rs:125-136):

{ "global_autoreply": false,
  "chats": [ { "jid": "...", "name": "...", "autoreply": true } ],
  "status": "<TUI status line>",
  "screen": "connecting | pairing | main",
  "qr_svg": "<svg ...>...</svg>" }

screen mirrors the TUI screen (SCREEN_CONNECTING, SCREEN_PAIRING, SCREEN_MAIN, src/web.rs:35-39). qr_svg is null unless the app holds a pairing QR payload; then qr_svg (src/web.rs:369-384) renders it as an SVG document with 6-pixel black modules on a white quiet zone. A payload that cannot be encoded yields null (the TUI pairing screen shows the encoding error).

Request checks

  • Custom header on POST — every POST route (global, chat, relink, quit) must carry the header X-Wh-Request: 1 (name case-insensitive, value exact); otherwise the server answers 403 richiesta non autorizzata and forwards nothing (requires_request_header, handle_request, has_request_header, src/web.rs:171-174, src/web.rs:219-236, src/web.rs:386-395). A cross-site page cannot add a custom header without a CORS preflight, which the server never approves, so this blocks simple cross-site form/fetch requests (CSRF). It is not authentication: anyone who can reach the port can send the header.
  • Body limit — /api/chat reads at most 1024 bytes (MAX_BODY_BYTES); an oversized, empty (after trimming), unreadable or non-UTF-8 body yields 400 corpo della richiesta non valido (read_limited_body, src/web.rs:397-414).

Bridge to the application

The server never touches application state. forward (src/web.rs:279-292) sends AppEvent::Web(WebRequest { command, reply }) with a tokio oneshot sender and blocks the web thread (blocking_recv) until App answers. App::handle_web_request runs inside the normal event loop, so web commands are serialised with keys and WhatsApp events (Application state):

  • ToggleGlobal follows the same rules as F4 (enabling needs a configured AI client; the flag is persisted in the history store);
  • ToggleChat(jid) is accepted only for JIDs present in the panel's chat list — otherwise only the status becomes Chat non trovata — and then follows the same rules as F3 (AI configured, private chat or group);
  • Relink starts the session deletion and re-pairing at once (the browser asks for confirmation instead of the TUI's double F8); it is ignored while a relink is already running. See WhatsApp relink and pairing;
  • the reply is always the state after the command; refusals are visible only through status.

The panel's chat list (web_chats) is the TUI chat list in the same order (most recent first), restricted to chats that support autoreply (private chats and groups). The autoreply field is the per-chat flag; the global flag is reported separately.

If the event channel is closed or the reply is dropped, the server answers 503 bot non disponibile; a serialisation failure gives 500.

Quit

handle_quit (src/web.rs:261-277) first writes the 200 response, then sends WebCommand::Quit without waiting for a reply, because the process exits as soon as App sets should_quit and the event loop ends. The TUI terminal is restored as on F10.

Browser page

src/web_page.html is a self-contained page (inline CSS and JS, Italian UI) with a global "AI tutte" switch, a "Nuovo QR" button, a "Chiudi bot" button, a pairing card, one switch per chat and a status bar:

  • it polls GET /api/state every 5 s while the tab is visible — every 2 s while screen is not main, so a rotated QR shows up quickly (adjustPolling) — and refreshes when it becomes visible again (src/web_page.html:382-383, src/web_page.html:623-629, src/web_page.html:742-763);
  • the pairing card (renderPairing, src/web_page.html:604-622) is hidden on main; otherwise it shows "Connessione a WhatsApp in corso...", "In attesa del codice QR..." or, with a QR, "Collega WhatsApp: scansiona il codice QR", the QR image (the SVG as a data: URL, replaced only when it changes) and the WhatsApp linking steps;
  • every POST carries X-Wh-Request: 1; the chat toggle sends the JID as a text/plain body (buildHeaders, src/web_page.html:445-450);
  • while a toggle is in flight its switch is disabled and keeps the user's position; polling is paused, and responses are applied in request order through a ticket counter so a stale poll cannot overwrite a newer toggle result (applyState, runToggle, src/web_page.html:647-701);
  • names are inserted with textContent only;
  • "Nuovo QR" (relinkBot, src/web_page.html:707-718) asks for confirmation (the device will be unlinked from the phone), posts /api/relink, applies the returned state (the pairing card appears) and keeps the button disabled while the request runs;
  • quit asks for confirmation (Chiudere il bot?) and then shows a full-screen "Bot chiuso" overlay and stops polling; a network error after a quit request is treated as the bot having exited (src/web_page.html:485-511, src/web_page.html:724-737).

Security and operations

  • No authentication and no TLS: anyone who can reach <host>:<web_port> can read chat names and JIDs, toggle autoreplies, stop the bot, unlink the WhatsApp device with /api/relink and, while pairing, read the QR code and link their own phone. Enable it only on a trusted network, or restrict the port with the host firewall.
  • The server binds to every interface; there is no setting for the bind address.
  • web_port = 0 is rejected at startup; values above 65535 or negative are rejected by TOML parsing.
  • Requests are served sequentially and each API request waits for the event loop, so a slow request delays the others. A relink request returns as soon as the relink task is spawned, not when it completes.

Tests

src/web.rs:415-502 covers query stripping, route resolution (known routes including /api/relink, wrong method or path, GET /api/relink → not found), the custom header check (any case accepted, missing or wrong value rejected), which routes require the header (relink included), qr_svg producing an SVG document, and body reading (trimming, empty/invalid UTF-8 rejection, the 1024-byte limit). web_chats filtering is tested in src/app.rs. See Unit tests.