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:
9fec9d1af4
resource: repo://src/app.rsb55a21a31e
resource: repo://src/main.rsac7a3eaa39
resource: repo://src/web_page.html7b75006986
resource: repo://src/web.rs
generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
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, 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).
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).
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).
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./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).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;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: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.
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:
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);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;X-Wh-Request: 1; the chat toggle sends the JID as a
text/plain body (buildHeaders, src/web_page.html:445-450);applyState, runToggle, src/web_page.html:647-701);textContent only;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;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).<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.web_port = 0 is rejected at startup; values above 65535 or negative are
rejected by TOML parsing.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.