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:
9fec9d1af4
resource: repo://src/app.rsb55a21a31e
resource: repo://src/main.rsac7a3eaa39
resource: repo://src/web_page.html7b75006986
resource: repo://src/web.rs
generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
verified: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).
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).
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>" }
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./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).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);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.
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.
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:
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);X-Wh-Request: 1; the chat toggle sends the JID as a
text/plain body (buildHeaders, src/web_page.html:377-382);src/web_page.html:534-577);textContent only;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).<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.web_port = 0 is rejected at startup; values above 65535 or negative are
rejected by TOML parsing.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.