web-control-panel.md 7.0 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, the custom-header guard on POST, the request/reply bridge to App through AppEvent::Web, the quit flow, the browser page and its security caveats. tags: [web, http, tiny_http, control-panel, security, autoreply] 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: "claude-code", at: "2026-10-20T20:27:06.515Z" } verified:
  • by: openwiki/0.7.2 at: 2026-10-21T21:49:12.291Z ---

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 and toggle the global and per-chat AI autoreply, 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:1-2, src/web.rs:16-17).

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:165-173) 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>.

serve (src/web.rs:179-186) 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 (Cargo.toml:24-25).

Routes

resolve_route (src/web.rs:325-334) matches method and path; the query string is ignored (path_without_query):

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/quit WebCommand::Quit 200 {"status":"Bot in chiusura"}
other other — 404 non trovato

The JSON state is WebState (src/web.rs:92-112):

{ "global_autoreply": false,
  "chats": [ { "jid": "...", "name": "...", "autoreply": true } ],
  "status": "<TUI status line>" }

Request checks

  • Custom header on POST — every POST route must carry the header X-Wh-Request: 1 (name case-insensitive, value exact); otherwise the server answers 403 richiesta non autorizzata and forwards nothing (handle_request, has_request_header, src/web.rs:193-205, src/web.rs:341-345). 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:352-364).

Bridge to the application

The server never touches application state. forward (src/web.rs:252-261) 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);
  • 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:234-246) 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, one switch per chat, a quit button and a status bar:

  • it polls GET /api/state every 5 s while the tab is visible and refreshes when it becomes visible again (src/web_page.html:330, src/web_page.html:612-635);
  • every POST carries X-Wh-Request: 1; the chat toggle sends the JID as a text/plain body (buildHeaders, src/web_page.html:377-382);
  • 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 (src/web_page.html:534-577);
  • names are inserted with textContent only;
  • 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:417-439, src/web_page.html:597-610).

Security and operations

  • No authentication and no TLS: anyone who can reach <host>:<web_port> can read chat names and JIDs, toggle autoreplies and stop the bot. 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.

Tests

src/web.rs:366-445 covers query stripping, route resolution (known routes, wrong method or path), the custom header check (any case accepted, missing or wrong value rejected), which routes require the header, 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.