feat(kiosk): display-size presets in Settings (Auto/Large/Balanced/Native)
system.kiosk-display.get/set RPCs write /etc/archipelago/kiosk-display.conf (sourced by the kiosk launcher) and try-restart the kiosk so the choice applies immediately. The Settings section only appears on nodes that have the kiosk unit installed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1b78da1d7a
commit
97358d2314
@ -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,
|
||||
|
||||
@ -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<serde_json::Value> {
|
||||
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<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
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";
|
||||
|
||||
82
neode-ui/src/views/settings/KioskDisplaySection.vue
Normal file
82
neode-ui/src/views/settings/KioskDisplaySection.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
// Kiosk display resolution/scaling presets. Only shown on nodes that actually
|
||||
// have a kiosk display (has_kiosk from the backend).
|
||||
const hasKiosk = ref(false)
|
||||
const preset = ref('auto')
|
||||
const applying = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const presets = [
|
||||
{
|
||||
id: 'auto',
|
||||
label: 'Auto (recommended)',
|
||||
description: 'Pick a comfortable size for the detected screen — 4K TVs get a Full-HD-sized layout at double sharpness.',
|
||||
},
|
||||
{
|
||||
id: 'large',
|
||||
label: 'Large UI',
|
||||
description: 'Biggest text and buttons — easiest to read from across the room, fits less on screen.',
|
||||
},
|
||||
{
|
||||
id: 'balanced',
|
||||
label: 'Balanced',
|
||||
description: 'Desktop-sized layout on any screen that can carry it — more on screen, still sharp.',
|
||||
},
|
||||
{
|
||||
id: 'native',
|
||||
label: 'Native (no scaling)',
|
||||
description: 'Use the screen’s full native resolution 1:1 — the most content, the smallest UI.',
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await rpcClient.call<{ has_kiosk: boolean; preset: string }>({ method: 'system.kiosk-display.get' })
|
||||
hasKiosk.value = res.has_kiosk
|
||||
preset.value = res.preset
|
||||
} catch { /* backend without the RPC — leave the section hidden */ }
|
||||
})
|
||||
|
||||
async function apply(id: string) {
|
||||
if (applying.value || id === preset.value) return
|
||||
applying.value = true
|
||||
error.value = ''
|
||||
const prev = preset.value
|
||||
preset.value = id
|
||||
try {
|
||||
await rpcClient.call({ method: 'system.kiosk-display.set', params: { preset: id }, timeout: 20000 })
|
||||
} catch (e: unknown) {
|
||||
preset.value = prev
|
||||
error.value = e instanceof Error ? e.message : 'Failed to apply display setting'
|
||||
} finally {
|
||||
applying.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Kiosk Display Section — only on nodes with an attached kiosk screen -->
|
||||
<div v-if="hasKiosk" class="glass-card px-6 py-6 mb-6">
|
||||
<h2 class="text-xl font-semibold text-white/96 mb-2">Display</h2>
|
||||
<p class="text-sm text-white/60 mb-6">
|
||||
How big the interface renders on the screen attached to this node. Changing this restarts the on-screen display.
|
||||
</p>
|
||||
<div data-controller-container tabindex="0" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
v-for="p in presets"
|
||||
:key="p.id"
|
||||
:disabled="applying"
|
||||
@click="apply(p.id)"
|
||||
class="path-option-card text-left p-5 disabled:opacity-60"
|
||||
:class="{ 'path-option-card--selected': preset === p.id }"
|
||||
>
|
||||
<div class="font-medium text-white/90 mb-1">{{ p.label }}</div>
|
||||
<p class="text-sm text-white/60">{{ p.description }}</p>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="error" class="mt-4 alert-error text-sm">{{ error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import InterfaceModeSection from '@/views/settings/InterfaceModeSection.vue'
|
||||
import KioskDisplaySection from '@/views/settings/KioskDisplaySection.vue'
|
||||
import ClaudeAuthSection from '@/views/settings/ClaudeAuthSection.vue'
|
||||
import AIDataAccessSection from '@/views/settings/AIDataAccessSection.vue'
|
||||
import WebhookSection from '@/views/settings/WebhookSection.vue'
|
||||
@ -10,6 +11,7 @@ import SystemDangerZone from '@/views/settings/SystemDangerZone.vue'
|
||||
|
||||
<template>
|
||||
<InterfaceModeSection />
|
||||
<KioskDisplaySection />
|
||||
<ClaudeAuthSection />
|
||||
<AIDataAccessSection />
|
||||
<WebhookSection />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user