feat: allow HASS to close its own page via POST /close-hass and make Chromium fullscreen - #22
Conversation
…Chromium fullscreen - Add HTTP listener on configurable port (default 8321) accepting POST /close-hass - Add public Slint function close-hass-remote() for Rust to trigger page navigation - Replace --window-size with --start-fullscreen for Chromium - Update HA page UI to show 'Home Assistant Opened!' status with troubleshooting hints - Add hass_api_port to Config - Add python3 to shell.nix for skia-bindings build
There was a problem hiding this comment.
Pull request overview
This PR adds a small remote-control mechanism so the Home Assistant (HASS) page can dismiss itself (addressing the “kiosk escape” issue) and adjusts the kiosk UX by running Chromium fullscreen and updating the on-screen HASS status panel.
Changes:
- Add a TCP-based HTTP listener that accepts
POST /close-hassand triggers UI navigation back to the main page. - Add a public Slint function (
close-hass-remote()) that Rust can invoke to close the HASS page. - Switch Chromium launch flags to fullscreen and update the HASS status/troubleshooting UI.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/pages/home_assistant.slint | Updates HASS page UI to a status/troubleshooting panel behind fullscreen Chromium. |
| ui/main_window.slint | Adds a public Slint function callable from Rust to close HASS and navigate home. |
| src/main.rs | Wires the close-listener signal into the Slint UI (currently via polling timer). |
| src/home_assistant.rs | Switches Chromium to fullscreen; introduces the POST /close-hass listener implementation. |
| src/config.rs | Adds hass_api_port config with a default value. |
| shell.nix | Adds python3 to dev shell inputs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub fn start_close_listener(port: u16, tx: Sender<()>) { | ||
| let addr = format!("0.0.0.0:{}", port); | ||
| let listener = match TcpListener::bind(&addr) { | ||
| Ok(l) => l, | ||
| Err(e) => { | ||
| error!("Failed to bind HASS close listener on {}: {}", addr, e); | ||
| return; | ||
| } | ||
| }; | ||
| info!("🏠 Home Assistant close listener on port {}", port); |
There was a problem hiding this comment.
The close listener binds to 0.0.0.0, exposing POST /close-hass on all interfaces with no authentication. This allows any host that can reach the device to remotely navigate the kiosk UI. If Home Assistant is the only intended caller, bind to 127.0.0.1/localhost by default and/or require a shared secret (header/token) before accepting the request.
|
|
||
| if first_line.starts_with("POST /close-hass") { | ||
| info!("🏠 Received remote close-hass request"); | ||
| let _ = tx.send(()); | ||
| let _ = stream.write_all( | ||
| b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: 2\r\n\r\nOK", | ||
| ); | ||
| } else if first_line.starts_with("OPTIONS") { |
There was a problem hiding this comment.
Request routing is currently based on first_line.starts_with("POST /close-hass"), which will also match unintended paths like /close-hass-foo and doesn’t validate the HTTP method/path tokens. Parse the first line into METHOD path VERSION and require an exact POST + /close-hass match (and similarly scope OPTIONS handling to the same path).
| if first_line.starts_with("POST /close-hass") { | |
| info!("🏠 Received remote close-hass request"); | |
| let _ = tx.send(()); | |
| let _ = stream.write_all( | |
| b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: 2\r\n\r\nOK", | |
| ); | |
| } else if first_line.starts_with("OPTIONS") { | |
| let mut parts = first_line.split_whitespace(); | |
| let request_line = match (parts.next(), parts.next(), parts.next(), parts.next()) { | |
| (Some(method), Some(path), Some(version), None) => Some((method, path, version)), | |
| _ => None, | |
| }; | |
| if let Some(("POST", "/close-hass", _version)) = request_line { | |
| info!("🏠 Received remote close-hass request"); | |
| let _ = tx.send(()); | |
| let _ = stream.write_all( | |
| b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: 2\r\n\r\nOK", | |
| ); | |
| } else if let Some(("OPTIONS", "/close-hass", _version)) = request_line { |
| for stream in listener.incoming() { | ||
| let Ok(mut stream) = stream else { | ||
| continue; | ||
| }; | ||
| let mut buf = [0u8; 512]; | ||
| let Ok(n) = stream.read(&mut buf) else { | ||
| continue; | ||
| }; |
There was a problem hiding this comment.
The listener processes connections sequentially and does a blocking stream.read(...) without a timeout. A client that connects and never sends data can stall the entire listener (preventing legitimate close requests). Consider setting a short read timeout and/or handling each connection in a separate thread/task so one slow client can’t block the service.
| Text { | ||
| text: "if hass did not started,\nconfigure the iframe URL in .config/dramma.toml\nor install chromium huh"; | ||
| font-size: 12px; | ||
| text: "• Make sure chromium is installed\n• Check .config/dramma.toml has the correct home_assistant_url\n• You can close this page with POST /close-hass"; |
There was a problem hiding this comment.
The troubleshooting text says the page can be closed with POST /close-hass, but the listener is on a configurable port (hass_api_port, default 8321). Consider including the full endpoint (e.g., POST http://localhost:<port>/close-hass) or at least mention the port/config key so the hint is actionable when debugging.
| text: "• Make sure chromium is installed\n• Check .config/dramma.toml has the correct home_assistant_url\n• You can close this page with POST /close-hass"; | |
| text: "• Make sure chromium is installed\n• Check .config/dramma.toml has the correct home_assistant_url\n• You can close this page with POST http://localhost:<hass_api_port>/close-hass (default port 8321; configurable via hass_api_port)"; |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
closes #11