diff --git a/kiosk/Cargo.toml b/kiosk/Cargo.toml index 026a588..dc9e3cc 100644 --- a/kiosk/Cargo.toml +++ b/kiosk/Cargo.toml @@ -32,3 +32,4 @@ hostname = "0.4" tokio-tungstenite = { version = "0.24", features = ["native-tls"] } futures-util = "0.3" url = "2" +webkit6 = "0.4" diff --git a/kiosk/src/server.rs b/kiosk/src/server.rs index 5d209e5..501c081 100644 --- a/kiosk/src/server.rs +++ b/kiosk/src/server.rs @@ -148,14 +148,18 @@ pub fn fetch_bundle(server: &str, key: &str) -> KioskBundle { resp.json().expect("bad bundle JSON") } -/// Send heartbeat. -pub fn heartbeat(server: &str, key: &str) { +/// Send heartbeat with display geometry. +pub fn heartbeat(server: &str, key: &str, displays: &[(String, u32, u32)]) { let client = reqwest::blocking::Client::new(); + let display_info: Vec<_> = displays.iter().map(|(name, w, h)| { + serde_json::json!({ "name": name, "width_px": w, "height_px": h }) + }).collect(); let _ = client .post(format!("{server}/api/kiosk/heartbeat")) .header("Authorization", format!("Bearer {key}")) .json(&serde_json::json!({ "kiosk_app_version": env!("CARGO_PKG_VERSION"), + "displays": display_info, })) .timeout(Duration::from_secs(5)) .send(); diff --git a/kiosk/src/ui.rs b/kiosk/src/ui.rs index 26fdb70..12e8b5e 100644 --- a/kiosk/src/ui.rs +++ b/kiosk/src/ui.rs @@ -92,10 +92,11 @@ fn activate(app: &Application) { } }); - // Heartbeat loop + // Heartbeat loop — also reports display geometry loop { std::thread::sleep(std::time::Duration::from_secs(60)); - server::heartbeat(&server, &key); + let displays = query_displays(); + server::heartbeat(&server, &key, &displays); } }); @@ -117,6 +118,33 @@ enum WorkerMsg { RenderBundle(KioskBundle), } +/// Query connected HDMI displays from sysfs. Returns (name, width, height). +/// Reads /sys/class/drm/*/status and /sys/class/drm/*/modes. +fn query_displays() -> Vec<(String, u32, u32)> { + let mut out = Vec::new(); + let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return out }; + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + // Skip non-HDMI connectors and the "card" parents + if !name.contains("-HDMI-") && !name.contains("-DP-") { continue; } + let path = entry.path(); + let status = std::fs::read_to_string(path.join("status")).unwrap_or_default(); + if status.trim() != "connected" { continue; } + let modes = std::fs::read_to_string(path.join("modes")).unwrap_or_default(); + // First line = preferred mode + let mode = modes.lines().next().unwrap_or(""); + let parts: Vec<&str> = mode.split('x').collect(); + if parts.len() != 2 { continue; } + let w: u32 = parts[0].parse().unwrap_or(0); + let h: u32 = parts[1].trim().parse().unwrap_or(0); + if w == 0 || h == 0 { continue; } + // Strip "cardN-" prefix for cleaner name + let clean_name = name.split_once('-').map(|(_, rest)| rest.to_string()).unwrap_or(name); + out.push((clean_name, w, h)); + } + out +} + fn show_pairing_code(window: &ApplicationWindow, code: &str) { let vbox = GtkBox::new(Orientation::Vertical, 20); vbox.set_valign(gtk::Align::Center); @@ -212,16 +240,20 @@ fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) { } } "html" => { - let html = cell.html_content.as_deref().unwrap_or("HTML"); - let label = Label::new(Some(&html.chars().take(100).collect::())); - add_css(&label, "label { color: #888; background-color: #111; }"); - label.set_vexpand(true); - label.set_hexpand(true); - label.upcast() + let html = cell.html_content.as_deref().unwrap_or(""); + let webview = webkit6::WebView::new(); + webkit6::prelude::WebViewExt::load_html(&webview, html, None); + webview.set_vexpand(true); + webview.set_hexpand(true); + webview.upcast() } "web" => { let url = cell.web_url.as_deref().unwrap_or("about:blank"); - placeholder(&format!("Web: {url}")) + let webview = webkit6::WebView::new(); + webkit6::prelude::WebViewExt::load_uri(&webview, url); + webview.set_vexpand(true); + webview.set_hexpand(true); + webview.upcast() } _ => placeholder("Unknown content"), }; diff --git a/server/src/plugins/service-api-http/index.ts b/server/src/plugins/service-api-http/index.ts index c74a367..1c95e69 100644 --- a/server/src/plugins/service-api-http/index.ts +++ b/server/src/plugins/service-api-http/index.ts @@ -226,6 +226,7 @@ function registerKioskRoutes( bundle_version?: string; kiosk_app_version?: string; os_version?: string; + displays?: Array<{ name: string; width_px: number; height_px: number }>; }>(event); repo.touchKiosk(kiosk.id, { @@ -234,6 +235,29 @@ function registerKioskRoutes( os_version: body?.os_version ?? null, }); + // Sync displays reported by the kiosk + if (Array.isArray(body?.displays)) { + const existing = repo.listDisplaysForKiosk(kiosk.id); + for (const reported of body.displays) { + const match = existing.find((d) => d.name.endsWith(reported.name)); + if (match) { + if (match.width_px !== reported.width_px || match.height_px !== reported.height_px) { + repo.updateDisplay(match.id, { + width_px: reported.width_px, + height_px: reported.height_px, + } as any); + } + } else { + // New display — create it + repo.createDisplayForKiosk(kiosk.id, { + name: reported.name, + width_px: reported.width_px, + height_px: reported.height_px, + }); + } + } + } + return { ok: true, now: new Date().toISOString() }; });