Files
archy/neode-ui/src/views/settings/KioskDisplaySection.vue
T
archipelagoandClaude Fable 5 9243babcdb
Demo images / Build & push demo images (push) Successful in 3m51s
feat(kiosk): graphics tiers + Settings knob; network map kiosk mode
The animated federation map froze the framework-pt 4K TV: the launcher
held every machine to the HD 5500-era choppy-audio flags (single raster
thread, GpuRasterization banned) while the map wrote SVG attrs at 60fps.

- Launcher: two flag tiers. legacy = the proven conservative set; modern
  (Intel gen8+, 'NNth Gen' models, AMD Ryzen) = default raster threads +
  GPU rasterization. Classified from /proc/cpuinfo (11 model strings
  covered by tests in-session); KIOSK_GRAPHICS=performance|quality in
  kiosk-display.conf overrides; headless unchanged. Reaches deployed
  kiosks via the include_str! self-heal, same as the vsync fix.
- system.kiosk-display.get/set: carries a 'graphics' field alongside
  'preset'; setting one no longer clobbers the other.
- Settings → Display: Graphics picker (Auto / Compatibility / Quality).
- NetworkMap3D: kiosks default to the 2D projection (remembered toggle
  still works) and tick at half rate with carried-over deltas — same
  spin speed, half the paint cost.
- Changelog: curated Unreleased notes for all of the above + the gate
  frame-embedding fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 09:23:07 -04:00

143 lines
4.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 graphics = 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 screens full native resolution 1:1 — the most content, the smallest UI.',
},
]
// Graphics tier: which browser rendering flags the on-screen display runs
// with. Auto classifies the hardware (older kiosk boxes keep the proven
// conservative flags; modern chips get GPU rendering); the two overrides
// exist for troubleshooting and unclassified hardware.
const graphicsModes = [
{
id: 'auto',
label: 'Auto (recommended)',
description: 'Detect this machines graphics hardware and pick the right rendering mode for it.',
},
{
id: 'performance',
label: 'Compatibility',
description: 'Most conservative rendering — use if the screen stutters, tears, or the audio crackles.',
},
{
id: 'quality',
label: 'Quality',
description: 'Full GPU rendering — smoothest animations on capable hardware. If unsure, use Auto.',
},
]
onMounted(async () => {
try {
const res = await rpcClient.call<{ has_kiosk: boolean; preset: string; graphics?: string }>({ method: 'system.kiosk-display.get' })
hasKiosk.value = res.has_kiosk
preset.value = res.preset
// Older backend without the graphics field: hide nothing, default Auto.
graphics.value = res.graphics ?? 'auto'
} 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
}
}
async function applyGraphics(id: string) {
if (applying.value || id === graphics.value) return
applying.value = true
error.value = ''
const prev = graphics.value
graphics.value = id
try {
await rpcClient.call({ method: 'system.kiosk-display.set', params: { graphics: id }, timeout: 20000 })
} catch (e: unknown) {
graphics.value = prev
error.value = e instanceof Error ? e.message : 'Failed to apply graphics 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>
<h3 class="text-lg font-semibold text-white/96 mt-8 mb-2">Graphics</h3>
<p class="text-sm text-white/60 mb-6">
How the on-screen display uses this machine&rsquo;s graphics hardware. Changing this restarts the on-screen display.
</p>
<div data-controller-container tabindex="0" class="grid grid-cols-1 md:grid-cols-3 gap-4">
<button
v-for="g in graphicsModes"
:key="g.id"
:disabled="applying"
@click="applyGraphics(g.id)"
class="path-option-card text-left p-5 disabled:opacity-60"
:class="{ 'path-option-card--selected': graphics === g.id }"
>
<div class="font-medium text-white/90 mb-1">{{ g.label }}</div>
<p class="text-sm text-white/60">{{ g.description }}</p>
</button>
</div>
<div v-if="error" class="mt-4 alert-error text-sm">{{ error }}</div>
</div>
</template>