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.factory-reset" => self.handle_system_factory_reset(params).await,
|
||||||
"system.settings.get" => self.handle_system_settings_get(params).await,
|
"system.settings.get" => self.handle_system_settings_get(params).await,
|
||||||
"system.settings.set" => self.handle_system_settings_set(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
|
// Opt-in anonymous analytics
|
||||||
"analytics.get-status" => self.handle_analytics_get_status().await,
|
"analytics.get-status" => self.handle_analytics_get_status().await,
|
||||||
|
|||||||
@ -719,4 +719,94 @@ impl RpcHandler {
|
|||||||
_ => anyhow::bail!("Unknown setting: {}", key),
|
_ => 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">
|
<script setup lang="ts">
|
||||||
import InterfaceModeSection from '@/views/settings/InterfaceModeSection.vue'
|
import InterfaceModeSection from '@/views/settings/InterfaceModeSection.vue'
|
||||||
|
import KioskDisplaySection from '@/views/settings/KioskDisplaySection.vue'
|
||||||
import ClaudeAuthSection from '@/views/settings/ClaudeAuthSection.vue'
|
import ClaudeAuthSection from '@/views/settings/ClaudeAuthSection.vue'
|
||||||
import AIDataAccessSection from '@/views/settings/AIDataAccessSection.vue'
|
import AIDataAccessSection from '@/views/settings/AIDataAccessSection.vue'
|
||||||
import WebhookSection from '@/views/settings/WebhookSection.vue'
|
import WebhookSection from '@/views/settings/WebhookSection.vue'
|
||||||
@ -10,6 +11,7 @@ import SystemDangerZone from '@/views/settings/SystemDangerZone.vue'
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<InterfaceModeSection />
|
<InterfaceModeSection />
|
||||||
|
<KioskDisplaySection />
|
||||||
<ClaudeAuthSection />
|
<ClaudeAuthSection />
|
||||||
<AIDataAccessSection />
|
<AIDataAccessSection />
|
||||||
<WebhookSection />
|
<WebhookSection />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user