diff --git a/core/archipelago/src/api/rpc/dispatcher.rs b/core/archipelago/src/api/rpc/dispatcher.rs index 8396e501..7a22b8b3 100644 --- a/core/archipelago/src/api/rpc/dispatcher.rs +++ b/core/archipelago/src/api/rpc/dispatcher.rs @@ -456,6 +456,8 @@ impl RpcHandler { "system.factory-reset" => self.handle_system_factory_reset(params).await, "system.settings.get" => self.handle_system_settings_get(params).await, "system.settings.set" => self.handle_system_settings_set(params).await, + "system.kiosk-display.get" => self.handle_system_kiosk_display_get().await, + "system.kiosk-display.set" => self.handle_system_kiosk_display_set(params).await, // Opt-in anonymous analytics "analytics.get-status" => self.handle_analytics_get_status().await, diff --git a/core/archipelago/src/api/rpc/system/handlers.rs b/core/archipelago/src/api/rpc/system/handlers.rs index 1ac91390..22e5a5c6 100644 --- a/core/archipelago/src/api/rpc/system/handlers.rs +++ b/core/archipelago/src/api/rpc/system/handlers.rs @@ -719,4 +719,94 @@ impl RpcHandler { _ => anyhow::bail!("Unknown setting: {}", key), } } + + /// system.kiosk-display.get — Current kiosk display preset + whether this + /// node has a kiosk at all (no kiosk unit -> the Settings section hides). + pub(in crate::api::rpc) async fn handle_system_kiosk_display_get( + &self, + ) -> Result { + let has_kiosk = tokio::fs::metadata("/etc/systemd/system/archipelago-kiosk.service") + .await + .is_ok(); + let conf = tokio::fs::read_to_string(KIOSK_DISPLAY_CONF) + .await + .unwrap_or_default(); + let preset = if conf.contains("ARCHIPELAGO_KIOSK_SCALE=1") { + "native" + } else if conf.contains("ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH=1920") { + "balanced" + } else if conf.contains("ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH=1280") { + "large" + } else { + "auto" + }; + Ok(serde_json::json!({ "has_kiosk": has_kiosk, "preset": preset })) + } + + /// system.kiosk-display.set — Write the kiosk display preset and restart + /// the kiosk (only if it is running) so it takes effect immediately. The + /// launcher sources /etc/archipelago/kiosk-display.conf at startup. + pub(in crate::api::rpc) async fn handle_system_kiosk_display_set( + &self, + params: Option, + ) -> Result { + let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; + let preset = params + .get("preset") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing preset"))?; + + let conf = match preset { + // Resolution-derived default: 4K -> 2.0 (1920-wide layout), + // 1080p TV -> 1.5, laptop panels -> 1.0. + "auto" => String::new(), + // Biggest UI: every panel targets a 1280-wide layout. + "large" => "ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH=1280\n".to_string(), + // Full-HD layout on any panel that can carry it. + "balanced" => "ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH=1920\n".to_string(), + // No scaling: native CSS viewport, most content, smallest UI. + "native" => "ARCHIPELAGO_KIOSK_SCALE=1\n".to_string(), + other => anyhow::bail!("Unknown display preset: {other}"), + }; + + host_sudo(&["/usr/bin/mkdir", "-p", "/etc/archipelago"]).await?; + if conf.is_empty() { + let _ = host_sudo(&["/usr/bin/rm", "-f", KIOSK_DISPLAY_CONF]).await; + } else { + // tee via sudo — the backend runs unprivileged and /etc is root's. + let mut child = tokio::process::Command::new("/usr/bin/sudo") + .args(["-n", "/usr/bin/tee", KIOSK_DISPLAY_CONF]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()) + .spawn() + .context("spawn sudo tee for kiosk display conf")?; + use tokio::io::AsyncWriteExt; + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(conf.as_bytes()).await?; + } + let out = child.wait_with_output().await?; + if !out.status.success() { + anyhow::bail!( + "writing {} failed: {}", + KIOSK_DISPLAY_CONF, + String::from_utf8_lossy(&out.stderr).trim() + ); + } + } + + // try-restart: only bounces a kiosk that is actually running, so this + // never starts a kiosk an operator disabled. + let _ = host_sudo(&[ + "/usr/bin/systemctl", + "try-restart", + "archipelago-kiosk.service", + ]) + .await; + + info!(preset, "Kiosk display preset applied"); + Ok(serde_json::json!({ "preset": preset, "applied": true })) + } } + +const KIOSK_DISPLAY_CONF: &str = "/etc/archipelago/kiosk-display.conf"; diff --git a/neode-ui/src/views/settings/KioskDisplaySection.vue b/neode-ui/src/views/settings/KioskDisplaySection.vue new file mode 100644 index 00000000..3c762431 --- /dev/null +++ b/neode-ui/src/views/settings/KioskDisplaySection.vue @@ -0,0 +1,82 @@ + + + diff --git a/neode-ui/src/views/settings/SystemSection.vue b/neode-ui/src/views/settings/SystemSection.vue index a917e114..abc794ae 100644 --- a/neode-ui/src/views/settings/SystemSection.vue +++ b/neode-ui/src/views/settings/SystemSection.vue @@ -1,5 +1,6 @@